
Mastering React-Native’s new Navigator
For those that have been working with React-Native over the last year since its release, the Navigator API has always seemed a bit ‘anti-react’. By that, I mean that its primarily imperative API seems contrary to the declarative api that react predominantly encourages. And because of that, the team at Facebook has been working on an new Navigator API — one that is declarative in nature, and is inspired by the excellent work of Dan Abramov in the redux state management tool. The new navigator is (as of writing) still at an experimental stage, but the API is publicly accessible, and it certainly seems to be the future of navigation on React-Native. So follow along as I attempt to learn the basic API, and build a few basic navigators.
Some context
First, we need to understand some basic concepts — those that have been introduced by the flux architecture, and more specifically, the flavour of flux introduced by redux. If you’re already familiar with redux and flux, then you may want to skip ahead to the next section.
React offers a declarative way, as opposed to imperative way, to write your code. A react component essentially declares what should be rendered given a particular state, supplied either through props, or through its own internal state. Your component renders in response to changes in state. This is contrasted with an imperative approach to rendering your application — say, using jQuery to manually alter the DOM in response to an event.
Redux offers a way to control and manage the state of your application. Of primary importance are two concepts: actions, and reducers. Actions declare that something should happen — i.e., that a push, or pop is occurring. A reducer manages and alters application state in response to these actions. If we push a new route (the action), the reducer can listen for that action, and add that route to our currently stored stack of routes. It can also update the index of the currently active route. Finally, this state can be passed back to our application, and our react components can update corresponding to the changes in state.
The new NavigatorExperimental follows this same pattern. In fact, you can even use redux to manage your navigator state, if you want, or you can use the supplied reducers available from ReactNative.
A basic example
Let’s start with a simple example that demonstrates how the reducer works, and how you can use it to manage navigation state.
First, let’s create our reducer. For this example, we will use the StackReducer supplied by NavigatorExperimental. Note that you can also use a TabReducer, if you’d like to manage Tab-based navigation.
const {
Reducer: NavigationReducer
} = NavigationExperimentalconst AppReducer = NavigationReducer.StackReducer({
getPushedReducerForAction: (action) => {
if (action.type === ‘push’) {
return (state) => {
return state || { key: action.key }
}
}return null
},
initialState: {
key: ‘MainNavigation’,
index: 0,
children: [
{ key: ‘First Route’ }
]
}})
Let’s look at this code more closely. Our initialState defines the first scene that will be displayed by our navigator. In this case, it will be a child at index 0, with the key ‘First Route’. Of more interest, though, is the other method that we’ve provided. getPushedReducerForAction allows us to define alter our navigation state depending upon the action that has occurred. If the action is of type ‘push’, we return a thunk that allows us to return our newly pushed scene. Our reducer takes this, and pushes it onto our navigation stack. In our case, we return the key provided by the action, which in turn causes our component to re-render, displaying the newly pushed route. Note that for other action types, we return null. The StackReducer will automatically handle ‘back’ action types as you would expect.
Next, let’s create our main component responsible for rendering the navigator state.
export default class BasicExample extends React.Component {constructor(props, context) {
super(props, context)
this.state = AppReducer()
}render() {
const currentChild = this.state.children[this.state.index].keyreturn (
<ScrollView style={styles.container}>
<View><Text>{`Current page ${currentChild}`}</Text></View>
<TouchableOpacity
onPress={() => {
this._handleAction({
type: ‘push’,
key: ‘page #’ + this.state.children.length
})
}}
>
<Text>Push a new route</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
this._handleAction({ type: ‘BackAction’ })
}}>
<Text>Go back</Text>
</TouchableOpacity>
</ScrollView>
)
}
_handleAction(action) {
const newState = AppReducer(this.state, action)
this.setState(newState)
}}
Note how we call our reducer in our component constructor, and assign the state that it returns to our component state. This is a great feature of the navigator. We can store the navigator state wherever we’d like — in component state, in the simplest of cases, or perhaps elsewhere. Because we update our state when handling a push, or pop action in the _handleAction method, our component re-renders with the new navigation state.
Let’s animate this
Let’s build something that you’d actually use in real life — a simple, animated, navigator replicating that which you could easily build with the previous navigator, or NavigatorIOS.
First, we can build off of the last example. We can use the same reducer, and we can reuse the basic _handleAction method, and component constructor. We need to import some additional components provided by NavigationExperimental, namely, AnimatedView, Card, and Header.
const {
AnimatedView,
Card,
Header,
Reducer
} = NavigationExperimentalLet’s now our render function to return an AnimatedView.
render() {
return (
<AnimatedView
navigationState={this.state}
style={styles.animatedView}
onNavigate={(action) => this._handleAction(action)}
renderOverlay={(props) => this.renderHeader(props)}
renderScene={(props) => this.renderCard(props)}
/>
)
}The onNavigate function allows us to pass actions to our reducer which are initiated either by us, or by functionality provided by the animated view, like swiping. renderOverlay allows us to render the heading overlay that is typically seen in mobile applications, with the title, back button, action button, and so on. Finally, the renderScene prop provides us with a way to render the active scene. Let’s look at the renderCard method.
renderCard(props) {
return (
<Card
{…props}
renderScene={(props) => this.renderScene(props)}
/>
)
}Within the renderCard method, we return a Card. This is a component provided by NavigationExperimental that handles various animations, and swiping interactions that we’d rather not have to implement ourselves. The Card also contains a renderScene prop, which allows us to render whatever custom content we want.
renderScene(props) {
return (
<ScrollView style={styles.scrollView}>
<Text>{props.scene.navigationState.key}</Text>
<TouchableOpacity onPress={() => {
props.onNavigate({
type: ‘push’,
key: ‘Route #’+props.scenes.length
})
}}>
<Text>Push new route</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
props.onNavigate({
type: ‘BackAction’
})
}}>
<Text>Back</Text>
</TouchableOpacity>
</ScrollView>
)
}Within this custom view, we can also make calls to `onNavigate` which will allow us to push, pop, etc., onto our navigation stack. We could provide a switch statement here to render completely different components depending upon the navigationState key.
Finally, our renderHeader function tells the animated view how to render the overlay header. It, again, is very simple.
renderHeader(props) {
return (
<Header
{…props}
renderTitleComponent={(props) => {
return (
<Header.Title>
{props.scene.navigationState.key}
</Header.Title>
)
}}
/>
)
}We also have access to the static props Header.BackButton, and Header.HEIGHT. We can also supply renderLeftComponent and renderRightComponent prop callbacks on the Header component which allow us to customize our header more thoroughly.
And there you have it. That’s our basic, fully animated navigator.
In the next post, I’ll be looking at providing alternative animations, hooking up the navigator to redux, and exploring how to compose navigators (i.e., have navigators within navigators).
Follow me on Twitter.