The React Native NavigationExperimental API has come a long way since it was first announced almost a year ago today.

What we will cover here:

NavigationCardStack

Implementing a reducer to handle route state Adding a navigation header Implementing Redux to handle route state Implementing Tabs

NavigationTransitioner

NavigationTransitioner implementation NavigationCard NavigationCard with header Implementing a reducer to handle route state Implementing Redux to handle route state Implementing custom NavigationCard built from scratch using Animated.View

The main things to keep in mind are that there are two main ways to handle Navigation using NavigationExperimental , and those are the previously mentioned NavigationCardstack and NavigationTransitioner .

Do not let the names worry you, they are very much like the old Navigator and NavigatorIOS in the sense that you return the NavigationCardstack or NavigationTransitioner in the main render method of your component in which you want to implement navigation, and give them a renderSene / render method.

// Example NavigationCardStack implementation

<NavigationCardStack

renderHeader={this._renderHeader}

navigationState={this.state.navState}

renderScene={this._renderScene}

/> // Example NavigationTransitioner implementation

<NavigationTransitioner

navigationState={navState}

render={this._render}

configureTransition={this._configureTransition}

/>

The main difference is how the navigation state is handled and reasoned about.

NavigationCardstack

To get started, we will build a basic navigation using NavigationCardstack . NavigationCardstack is the most like the old Navigator . All we need to do is pass in the navigationState object and a renderScene method and it does the rest.

To follow along, create a new React Native project:

react-native init navexperimentalproject

Now, create a file called NavigationCardstack.js , then replace the code in index.ios.js and index.android.js with the following:

import { AppRegistry } from 'react-native'

import App from './ NavigationCardstack ' AppRegistry.registerComponent('navexperimentalproject', () => App);

In NavigationCardStack.js we will start by creating a very basic navigation using NavigationCardStack :

import NavigationExperimental from React Native destructure NavigationCardStack from NavigationExperimental create a Home route that we will use as the initial route create a reducer that will return our NavigationState object. As you can see, our navigation state object looks like this:

{

index: 0,

routes: [{ key: 'Home'}]

}

The objects in the routes array will be what we use in our renderScene method to return the correct route by switching on the key.

5. we create the initialState of the component and assign navState the returned value of the reducer, which, again, will be the object with and index and an array of routes.

6. renderScene will be invoked with a few properties that we can use to determine what we want to do at this point:

Value of console.log(props) in renderScene

What we need is the value of the scene being rendered, which will be the current route in the route array.

We switch on the scene.route.key and return a component based on the key value.

If you give your route a component property, e.g. { key: ‘Home’, component: Home } you can do away with the switch statement and return the component by doing this:

_renderScene = (props) => {

return <props.scene.route.component />

}

7. return the NavigationCardStack and pass in this.state.navState to the navigationState property and renderScene to the renderScene property.

Now, we should have our initial component loading up:

Now that we have this working, let’s create a way to move forward and backward to new routes.

The first thing we need to do is update the reducer to handle new route changes. To do this, we will need to add two new arguments to the reducer, a route and an action:

function reducer(state, action, route) { ... }

The main functionality we need right now is to add and remove a route to the route stack. In previous APIS this was done by calling push({ route }) and pop() , so we will create similar methods here.

function reducer(state: object, action: string, route: object): object {

if (!state) {

return {

index: 0,

routes: [{key: 'Home'}],

};

}

switch (action) {

case 'push': {

const routes = state.routes.slice();

routes.push(route);

return {

...state,

index: routes.length -1,

routes,

}

}

case 'pop': {

if (state.index <= 0) return state

const routes = state.routes.slice(0, -1);

return {

...state,

index: routes.length - 1,

routes,

}

}

default:

return state

}

}

If you are wondering what (state: object, action: string, route: object) is, it is Flow to specify the type of arguments that this function will take. To learn more, check out Flow.

We add two new parameters to the function, an action (string) and a route (object) if there is no existing state, we return our initial state with a single route object. if the action is 'push' , then we first create a copy of the routes, and then push the new route to the array of routes. We then return a new state object, update the index and return the new routes. if the action is pop , we check to see if the index is zero, and if so we return the state. If it is not, we create a copy of the routes array, removing the last item in the array using slice. We then return a new state, update the index and return the new routes.

Now that we have the reducer updated and ready to update our navState, let’s create a navigate method that will call the reducer and update the state.

In the NavigationCardStackExample class, let’s add a method called _navigate that will call this reducer and update the state.

_navigate = (action, route) => {

const navState = reducer(this.state.navState, action, route)

this.setState({

navState

})

}

This method will take an action and a route, call the reducer and store the returned value in a variable called navState , then update the state.

W henever the navState changes, renderScene is called with the new navigation state.

Now that we have the new method created, we need to create a new component to navigate to, and also add a button in the Home component to navigate to the new component. Let’s create a new component called About and also update the Home component.

const Home = ({ navigate }) => {

return (

<View style={styles.container}>

<Text>Hello from Home</Text>

<Text onPress={() => navigate('push', { key: 'About' })}>Go To About</Text>

</View>

)

} const About = ({ navigate }) => (

<View style={styles.container}>

<Text>Hello from About</Text>

<Text onPress={() => navigate('pop')}>Back</Text>

</View>

)

Finally, we can add the new About component to the renderScene method and pass the navigate method down as a props to both the About and Home components.

_renderScene = (props) => {

switch(props.scene.route.key) {

case 'Home':

return <Home navigate={this._navigate} />

case 'About':

return <About navigate={this._navigate} />

}

}

Now we should be able to navigate between scenes:

If you think that the reducer is verbose, and that it’s a little much as far as handling all of the logic yourself, there is also a great way to let NavigationExperimental handle this logic for us. It comes with a very nice set of useful methods that we can just import and use. They are the NavigationStateUtils, and can be used like so:

NavigationStateUtils.push(state, route)

NavigationStateUtils has the following available methods:

get(state: NavigationState, key: string)

has(state: NavigationState, key: string)

push(state: NavigationState, route: NavigationRoute)

pop(state: NavigationState)

jumpToIndex(state: NavigationState, index: number)

jumpTo(state: NavigationState, key: string)

back(state: NavigationState)

forward(state: NavigationState)

replaceAt(state: NavigationState, key: string, route: NavigationRoute)

replaceAtIndex(state: NavigationState, index: number, route: NavigationRoute)

reset(state: NavigationState, routes: Array<NavigationRoute>, index?: number)

For now, we are only interested in push and pop , so let’s rewrite our reducer to use these methods:

First, we need to import NavigationStateUtils from NavigationExperimental

const {

CardStack: NavigationCardStack,

StateUtils: NavigationStateUtils,

} = NavigationExperimental

Then, in our reducer

function reducer(state, action, route) {

if (!state) {

return {

index: 0,

routes: [{key: 'Home'}],

};

}

switch (action) {

case 'push':

return NavigationStateUtils.push(state, route)

case 'pop':

return NavigationStateUtils.pop(state)

default:

return state

}

}

Now, everything should work the same with significantly less code. This is how our code should now look:

The next thing we will do is add add a Header Component.

First, we will import the NavigationHeader from 'react-native' .

const {

CardStack: NavigationCardStack,

Header: NavigationHeader,

StateUtils: NavigationStateUtils,

} = NavigationExperimental

Then, we need to create the actual Header component.

class Header extends Component {

_back = () => {

this.props.navigate('pop');

} _renderTitleComponent = (props) => {

return (

<NavigationHeader.Title>

{props.scene.route.key}

</NavigationHeader.Title>

);

} render() {

return (

<NavigationHeader

{...this.props}

renderTitleComponent={this._renderTitleComponent}

onNavigateBack={this._back}

/>

);

}

}

The main things to note here is that we pass all props down to the NavigationHeader as it will use this info to update the title of the header as well as determine whether or not to show a back button.

We also pass a renderTitleComponent method which will return a NavigationHeader.Title with the route key as the title. You could have just as easily passed <Text>{props.scene.route.key}</Text> but NavigationHeader.Title comes with default styling out of the box.

NavigationHeader can take the following as props, allowing you to create custom titles, left, and right buttons based on application and route state:

onNavigateBack: Function

renderLeftComponent: SubViewRenderer

renderRightComponent: SubViewRenderer

renderTitleComponent: SubViewRenderer

style?: any

viewProps?: any

statusBarHeight: number || Animated.Value,

SubViewRenderer is basically a method that returns a React Native component

In the NavigationCardStackExample class, add a new method called _renderHeader:

_renderHeader = (sceneProps) => {

return (

<Header

navigate={this._navigate}

{...sceneProps}

/>

)

}

And finally add the renderHeader method to the navigator:

<NavigationCardStack

renderHeader={this._renderHeader}

navigationState={navState}

renderScene={this._renderScene}

/>

Now, when we navigate to and from the different components, we will see that the Navigation Header is there, and it shows us the title as well as a back button if we move forward from the initial route.

Implementing Redux

As you can see, we are manually having to pass in the navigate method to every component, and then we would theoretically have to pass that method around as props to child components in order to get access to the navigate methods.

It would be much nicer if we could manage all of this through redux actions, which is what we will now do.

First, we need to install everything we will need for Redux:

yarn add redux react-redux

or

npm i redux react-redux --save

Now, we will be overhauling quite a bit of the application.

The first thing we will do is create the new files we will be needing for Redux, and separating out some existing components, such as Home and About. Let’s create the new files we will need:

touch Home.js About.js configureStore.js constants.js navActions.js navReducer.js NavRootContainer.js rootReducer.js

If you’re used to working with Redux, these will all look very familiar to you.

To start things off, let’ go to index.ios.js / index.android.js (depending on what platform you are developing for) and update the code there.

We will be creating a root container for our main application in a file called NavRootContainer , and as you can see we’re returning that as our main component for the provider.

Now, we will create our redux actions, reducer, constants, and store.

In constants.js, create two variables called PUSH_ROUTE and POP_ROUTE :

In navReducer.js, create the following reducer:

As you can see in the above file, we’ve updated our reducer to instead use the constants we create to switch on so we can have consistency with our navActions.

Now, in navActions.js create the following actions:

Next, we’ll take the reducer and create a rootReducer for our configureStore to consume:

And in configureStore:

Now, we need to create the updated Home and About components and allow them to receive props from redux.

Also notice in Home.js and About.js we are no longer using our outdated navigate method but are instead using the push and pop methods available to us through redux:

Next, we will create the NavRootContainer that we imported into the updated index.ios.js / index.android.js files and pass in the redux actions and state:

Finally we can go back into NavigationCardStack and clean it up, and update it to use redux as opposed to state:

Now we have not only a nice navigation implementation ready to go, we also have redux set up!

Implementing Vertical Animation

Now what if we wanted the scene to transition from the bottom instead of from right to left? Well, NavigationCardStack takes a property called direction, and the default is horizontal. We can also pass in vertical.

To create some form of logic, let’s go back into our reducer and start keeping up with the previous route.

To do so, we will need to abandon the NavigationStateUtils functions we have for the push route and go back to our custom reducer.

In NavReducer, update the PUSH_ROUTE case to start keeping up with a prevPushedRoute key:

case PUSH_ROUTE: {

const routes = state.routes.slice();

routes.push(action.route);

return {

...state,

prevPushedRoute : action.route,

index: routes.length - 1,

routes,

}

}

Remember, this route will only get updated if PUSH_ROUTE is called, and we will use it to check to see if the previously pushed route type is of type 'modal' , and if so we will render a vertical transition.

The next thing we will do is go to the render method in NavigationCardStack.js and check to see if the prevPushedRoute type is 'modal' , and if so update the direction:

render() {

const { navState } = this.props

let direction = 'horizontal'

if (navState.prevPushedRoute && navState.prevPushedRoute.type === 'modal') {

direction = 'vertical'

}

return (

<NavigationCardStack

direction={direction}

renderHeader={this._renderHeader}

navigationState={this.props.navState}

renderScene={this._renderScene}

/>

)

}

Now, we can create a new button in Home that calls the About component with a modal:

const Home = ({ push }) => {

return (

<View style={styles.container}>

<Text>Hello from Home</Text>

<Text onPress={() => push({ key: 'About' })}>Go To About</Text>

<Text onPress={() => push({ key: 'About', type: 'modal' })}>Go To About With Modal</Text>

</View>

)

}

Now when we click on the new button, we get the vertical animation

The next thing we will do is add tabs and into the app and also learn about using NavigationTransitioner and NavigationCard , but we will wait for part 2 to implement them while we take some time to wrap our brains around this :) !

The final repo for this project can be found here.