NavigationExperimental Premier: A Simple Recipe
Thanks to Nader Dabit’s post, I wasted no time when learning about navigation in React Native. The Navigator
will be soon deprecated and it will be replaced by a different approach, currently called NavigationExperimental
. However, I haven’t found much documentation that is easy to follow for a newbie (me) who doesn’t have much experience in Flux, Redux etc.
I decided to write a simpler tutorial to help myself (and hopefully you too) understand how things work.
In a nutshell, NavigationExperimental
handles navigation with three things:
NavigationState
: exactly as its name saysNavigationStateUtils
: helper functions for manipulating navigation statesNavigationCardStack
: the container that renders scenes based on its navigation state
Let me expand each of these while building some simple navigation like this:
NavigationState
According to the proposal (RFC) of the new navigation system, the old Navigator
“owns navigation state and has an imperative API, which goes against the React philosophy of single-directional data flow”. The fix is to define the navigation states declaratively, basically the same way as other things in React: declaring it as a state on the top level component and pass it down in the hierarchy as props.
A typical navigation state looks something like this:
{
index: 1, // => index of the active scene
routes: [ // => navigation stack
{key: 'screen1'},
{key: 'screen2'},
]
}
The NavigationCardStack
is aware of this NavigationState
(actually passed in as a prop) and renders scenes accordingly.
In our simple example app, we can assign the initial navigation state like so:
class NavExample extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
navigation: {
index: 0,
routes: [
{key: 'screen1'},
]
}
...
}
...
This means that the entry of our app is screen1
, and there is initially only one screen in the stack.
BTW: One thing I’m not too sure here - why is the stack called routes
instead of scenes
? There seems indeed a difference between scenes
and routes
if you look at the sceneProps
passed into the renderScene
function.
Manipulating States with NavigationStateUtils
Now if we click “Go screen2” and then click “Go screen3”, two scenes will be pushed into the stack. The state will look like this:
{
navigation: {
index: 2,
routes: [{key: 'screen1'}, {key: 'screen2'}, {key: 'screen3'}]
}
}
At this point if we hit the “Go Back” button, screen3
will be popped off from the stack, making screen2
active (on the top of the stack).
{
navigation: {
index: 1,
routes: [{key: 'screen1'}, {key: 'screen2'}]
}
}
These stack pushing/popping operations can be done with the helper functions in the NavigationStateUtils
module (push()
, pop()
and jumpTo()
).
Below are the (flow) type definitions of these functions in the source code:
function push(state: NavigationState, route: NavigationRoute): NavigationState {
...
}
function pop(state: NavigationState): NavigationState {
...
}
function jumpTo(state: NavigationState, key: string): NavigationState {
...
}
These functions return a new NavigationState
which we can use to make the navigation happen by calling this.setState()
:
_navigate(action) {
// NavigationStateUtils is used in reduceNavState()
const newNavState = reduceNavState(this.state.navigation, action)
if (newNavState !== this.state.navigation) {
this.setState({
navigation: newNavState,
})
}
}
Here this reduceNavState
simply uses NavigationStateUtils
to push or pop routes from the stack, but more complex navigation mechanisms can be implemented as needed (e.g. independent navigation stacks within tabs as in one of the official examples).
const reduceNavState = (navState, action) => {
const {type, key} = action;
switch (type) {
case 'push':
const route = {key}
return NavStateUtils.push(navState, route)
case 'pop':
return NavStateUtils.pop(navState)
default:
return navState
}
}
With the _navigate()
function in hand, it’s now fairly easy to move around in our app, for example, creating a button that links to screen2
:
<Button onPress={this._navigate.bind(this, {type: 'push', key: 'screen2'})}
.../>
If you are not familiar with bind()
, it’ll create a new function that executes as if we are calling _navigate({type: 'push', key: 'screen2'})
. See here for more information about bind
.
NavigationCardStack
The last piece of the puzzle is NavigationCardStack
, a component acting as a container that renders scenes depending on the navigation state.
<NavigationCardStack
renderScene={this._renderScene} // a function that performs the rendering
navigationState={this.state.navigation} // exactly as its name says
renderOverlay={this._renderHeader} // renders something that's always on the screen
onNavigate={this._goBack} // this has been renamed to onNavigateBack on master
/>
When the renderScene
function is called, it’ll be provided with a sceneProps
parameter which looks something like this:
{
"layout": {
"height": 568,
"initHeight": 568,
"initWidth": 360,
"isMeasured": true,
"width": 360
},
"navigationState": {
"index": 1,
"routes": [
{
"key": "screen1"
},
{
"key": "screen2"
}
]
},
"position": 0,
"progress": 1,
"scene": {
"index": 1,
"isStale": false,
"key": "scene_screen2",
"route": {
"key": "screen2"
}
},
"scenes": [
{
"index": 0,
"isStale": false,
"key": "scene_screen1",
"route": {
"key": "screen1"
}
},
{
"index": 1,
"isStale": false,
"key": "scene_screen2",
"route": {
"key": "screen2"
}
}
]
}
There’s a lot of stuff here, but it looks like scene.route.key
here refers to the key of the active scene (I’ll update this after they finalize the API). We can then decide what screen to render depending on its value, typically in a switch
:
_renderScene(sceneProps) {
const scene = sceneProps.scene.route.key
switch (scene) {
case 'screen1':
return (
<Screen1 goScreen2={this._goScreen2}
goScreen3={this._goScreen3}
{...sceneProps} />
)
case 'screen2':
return (
<Screen2 goBack={this._goBack}
goScreen3={this._goScreen3}
{...sceneProps} />
)
case 'screen3':
return ...
...
}
Of course you have the freedom of rendering anything you like, for example, you can do a pattern matching on the route key and render the same screen for a class of scenes.
Support for Back button on Android
On Android, there is a Back button that needs some special treatment when implementing navigation. The standard behavior is to go back to the previous screen until the app exits. We can use the BackAndroid
module to pop up scenes from the stack like so:
class NavExample extends React.Component {
constructor(props, context) {
...
if (Platform.OS === 'android') {
BackAndroid.addEventListener('hardwareBackPress', () => {
if (this.state.navigation.index > 0) {
this._goBack()
return true
} else {
return false
}
})
}
}
Source Code
You can find the source code of the example app here. Make sure you use React Native 0.28.0+: react-native -v
. You can upgrade it using npm install --save react-native@0.28.0
(or the latest version).
Conclusion
So here you go, three parts of NavigationExperimental
work together to complete the story: NavigationState
, NavigationStateUtils
and NavigationCardStack
. Navigation states are now good React citizens as they are just normal states or props defined declaratively. This makes the navigation code easier to follow/debug and also open some interesting opportunities, for example, the navigation state can be persisted to the disk during app refreshes.
Did you find this post useful? Do you have other things about navigation to share with us? Chime in below in the comments! Thanks for reading.