React Native for Beginners – Navigation & Redux (Part 2)
This is Part 2 of my series React Native for Beginners. In part 1 (Getting started) I looked at simply getting a project started by contrasting the two most popular methods of initialising a new project. I concluded that using React Native CLI was the superior choice. I also came to realise that unit testing is a little more tricky than with a ReactJS project.
In this part we’ll be delving into some real app code and getting some screens in place with basic navigation between them.
But first, I’d like to address something I consider to be quite important: how to go about updating to the latest version of React Native in an existing project.
Upgrading React Native
Luckily, the RN team haven’t left us out in the cold with this one— it’s not too painful to upgrade React Native. At least, for me it wasn’t. Your mileage may vary given that different RN versions may introduce more or less breaking changes.
For my project, I’m moving from React Native v0.54 to v0.55. Before starting the upgrade, I made sure to commit all my project’s changes to my remote repo, just in case things went pear-shaped during the upgrade.
First I headed to the official React Native upgrade guide page in the docs. They outline how to go about updating an app that’s been created via Create React Native App or React Native CLI.
I won’t touch on the CRNA path, but for React Native CLI it’s as simple as grabbing an updater NPM package and running it, like this:
npm i -g react-native-git-upgrade
cd my-rn-app
react-native-git-upgrade
The tool will take care of not only updating key project dependencies such as React Native and ReactJS, but also making any required changes to the Android and iOS source files. Don’t be alarmed if you have git conflicts to deal with after the upgrade completes— this is to be expected because the updates may affect files that have already been modified since your project was first created.
For me, I was lucky enough to only have 1 conflict to resolve inside my modified .gitignore
file. If you’re less than lucky, you may need to delve into some native files to fix conflicts. Even these shouldn’t be too painful to resolve, though, based on the example provided in the upgrading documentation page.
Once all conflicts are resolved, double check your dependencies inside package.json
to ensure all is well there. I noticed my upgrade had bumped react
a version higher to 15.3.1
, so I also needed to bump react-test-renderer
to the same version to avoid potential unit test issues.
Finally, try running your app again. To recap, my package.json
contains these scripts:
{
"scripts": {
"rn": "node node_modules/react-native/local-cli/cli.js",
"start": "npm run rn start",
"ios": "npm run rn run-ios",
"android": "npm run rn run-android",
"test": "jest"
}
}
Therefore to start the app in an iOS simulator, I run these two commands in two separate terminal windows:
yarn start # 1st terminal window
yarn run ios # 2nd terminal window
One last thing: if the
yarn run ios
command fails, you probably just need to check that Xcode is up to date and load it up once to ensure it’s got the required tools installed— it’ll inform you with a pop-up immediately after it loads.
Right. Enough about upgrades, let’s get stuck in with some coding.
A little about navigation
If you’ve worked with web libraries like ReactJS, Vue or Angular, you’ll know that to build an app you’ll usually require separate pages that will usually be related to their own routes. Routes in web applications can be thought of as pathways that the application will take to execute specific parts of the code.
It’s almost always a given that each route will also be associated with a specific URL path. For example, a page called Dashboard might be associated with this URL path: www.mywebapp.com/dashboard
.
How do you create pages in those popular web app JS libraries? By creating components!
The concept is exactly the same in React Native too, except that they’re usually referred to as screens instead of pages. It’s the same concept, though.
Adding some screens
You might want to put all components into their own folder, regardless of their type or function. That’s fine— you can structure your app however makes sense for you or your project.
Given the choice however, I prefer to separate components based on their broad purpose (in terms of what responsibility they have in the app as a whole). My project structure typically tends towards something like this:
App.js
App.spec.js
package.json
src
├── components
├── containers
└── screens
├── DashboardScreen
└── WelcomeScreen
As you can see, I have a screens directory— this will hold subdirectories inside it that will each contain one component. In my example, I just have a WelcomeScreen
directory inside.
I’ve already decided that my app will start by displaying the Welcome screen, followed by the Dashboard screen after the user taps a button.
I won’t include whole code snippets here for the sake of space, but let’s say my WelcomeScreen
component looks like this:
// WelcomeScreen.js
import React from 'react';
import { View, Button, Text } from 'react-native';
const WelcomeScreen = () => (
<View>
<Text>Welcome!</Text>
<Text>Click below to continue...</Text>
<Button
title="Let's go!"
onPress={() => {}}
/>
</View>
);
export default WelcomeScreen;
This isn’t styled, but it’s easy to do so by adding a StyleSheet
from react-native
to style each node being rendered. Check out the style docs for information on that. If you’re familiar with CSS, then you’ll have an easy time getting into styling.
The Button
doesn’t have anything going on inside its onPress
prop yet, because we don’t yet know how navigation will be handled. Later, we’ll add the required logic here to be able to navigate to another screen.
I also created my Dashboard screen and just added a Text
node inside so that I’ll know when the navigation worked correctly:
// DashboardScreen.js
import React from 'react';
import { View, Text } from 'react-native';
const DashboardScreen = () => (
<View>
<Text>Here's the Dashboard!</Text>
</View>
);
export default DashboardScreen;
Adding navigation
When I came to adding navigation I realised I had a little research to do, because there’s quite a number of different modules available that handle it.
My go-to solution for ReactJS web applications is the highly popular react-router
library. They also support React Native, too, so this was an option. However, I chose to look elsewhere because there appear to be more popular choices for React Native apps in particular.
I didn’t keep track of all the sites I visited when I was doing my research, but the short of it is that I chose to use the react-navigation NPM package. It looked the easiest to get on with, is super popular and has good support (via other NPM packages) with Redux, which for me was a must as I knew I’d be adding that later, too.
I decided to start out I’d just use one file to define all my app’s routes. This might become cumbersome later on, but simplicity is what I want for now. Here’s my routes.js
file, placed inside the src
directory:
// routes.js
import { StackNavigator } from 'react-navigation';
import WelcomeScreen from './screens/WelcomeScreen';
import DashboardScreen from './screens/DashboardScreen';
export default StackNavigator(
{
Welcome: { screen: WelcomeScreen },
Dashboard: { screen: DashboardScreen }
},
{
initialRouteName: 'Welcome',
headerMode: 'none'
}
);
One thing to note about native mobile apps is that there’s usually more than one type of navigation possible. One kind is stack navigation— this is the kind we see when an app replaces the entire screen with another one. Other kinds of navigation are tabs and drawer.
In my routes.js
file, I’m making use of stack navigation, because I know I won’t be using any common visual elements between my Welcome & Dashboard screens.
The routes are all specified inside an object— the first argument passed to StackNavigator
. The name of the property each route is added to is the name that will be used later for navigating between screens. In my case, Welcome
and Dashboard
are the names of the available routes, so I’ll be using these names when I add navigation buttons.
The initialRouteName
specifies which route this stack of routes will initially render. I also chose to add headerMode: 'none'
. This turns off a header bar that’s added to each page by default.
In the future I may need to add new navigation patterns using one of the other kinds of navigations available. I haven’t yet decided how best to structure the app with this in mind, though one option could be to create a new file for each set of routes based on the type of navigation. It might even make sense later to place screen components inside the same directory as the navigation file that relates to them. Just some food for thought.
Loading the navigator into the app to get it working is a simple matter of just rendering the output from StackNavigator
, like this:
// App.js
import { AppRegistry } from 'react-native';
import React from 'react';
import Router from './src/routes.js';
const App = () => (
<Router />
);
AppRegistry.registerComponent('myapp', () => App);
Start up the app and the Welcome screen should be shown!
Fixing that Welcome screen button
Remember that I left my Welcome screen Button
in a sorry state? Let’s fix up that poor, empty onPress
prop that’s currently good for nothing.
Once a screen is loaded via a navigation route, it will be passed a special prop called navigation
. This is what makes it possible for components to have access to the methods required to move the app from screen to screen.
Here’s how I added it to WelcomeScreen
to allow the user to move to the Dashboard screen:
// WelcomeScreen.js
import React from 'react';
import { View, Button, Text } from 'react-native';
const WelcomeScreen = ({ navigation }) => (
<View>
<Text>Welcome!</Text>
<Text>Click below to continue...</Text>
<Button
title="Let's go!"
onPress={
() => navigation.navigate('Dashboard')
}
/>
</View>
);
export default WelcomeScreen;
As you can see, it’s really simple. All I have to do is call the navigate
method on the passed prop and provide it with the name of the route I want to take the user to.
Note the uppercase D on Dashboard— that exactly matches the name I gave it inside routes.js
.
Refresh the app and tap on the “Let’s go!” button and the dashboard screen should now be revealed!
Adding a modal screen (nested navigators)
My plan now is to nest another level of navigation inside the Dashboard route. The result will be a set of new routes that are only accessible after the app has loaded the Dashboard route. The new navigation hierarchy will look something like this:
Welcome - WelcomeScreen (default)
Dashboard
├─ Dashboard - DashboardScreen (default)
└─ NewItem - NewItemScreen
Happily, the way react-navigation
works is that when a navigator is created, such as the StackNavigator
, it outputs a React component. This means we can use it as the component for a route rendered by another navigator!
// routes.js
import { StackNavigator } from 'react-navigation';
import WelcomeScreen from './screens/WelcomeScreen';
import DashboardScreen from './screens/DashboardScreen';
import NewItemModalScreen from './screens/NewItemModalScreen';
const modalNav = StackNavigator(
{
Dashboard: { screen: DashboardScreen },
NewItemModal: { screen: NewItemModalScreen }
},
{
initialRouteName: 'Dashboard',
headerMode: 'none',
mode: 'modal'
}
);
export const rootNav = StackNavigator(
{
Welcome: { screen: WelcomeScreen },
Dashboard: modalNav
},
{
initialRouteName: 'Welcome',
headerMode: 'none'
}
);
Here’s my new routes.js
file with the new navigator added. It replaces the previous Dashboard
route with the new modalNav
. Note that I added mode: 'modal'
to the modalNav
. This changes the reveal/hide animation for the route transitions to make it look more like a modal.
It won’t look the same as a typical web app modal (you know, where the background is semi-transparent to partially show the page underneath), but it will animate the next screen to make it look like it’s covering the previous one.
No change is required to the previous code added for the button onPress
prop:
onPress={
() => navigation.navigate('Dashboard')
}
It’s the same route name (Dashboard
) inside the rootNav
, so when navigating to that route, it’ll know to render the default route defined inside modalNav
.
Additionally, opening NewListModalScreen
from the DashboardScreen
is just as easy:
onPress={
() => navigation.navigate('NewListModal')
}
Adding Redux (using Rematch)
You might want it to add a state management library to your app if you think you might want to use a single place to keep all of your app’s state to be used in different components. I’m going to proceed to add Redux as my state management library because I feel it’s right for my app to keep it maintainable as it grows more complex.
I’m actually going to take a slight detour, however, and use another library called Rematch. It’s still Redux under the hood, but it wraps it in a leaner API. I could happily dedicate a whole post to covering Rematch, because I really like what it does. It vastly simplifies Redux store setup, reduces boilerplate and is much easier to unit test than typical Redux reducers and action creators.
Here’s my newly created store.js
file that is responsible for creating my app’s central Redux store:
// store.js
import { init } from '@rematch/core';
import createReactNavigationPlugin from '@rematch/react-navigation';
import routes from './routes';
const { Navigator, reactNavigationPlugin } = createReactNavigationPlugin({
Routes: routes,
initialScreen: 'Welcome'
});
const plugins = [
reactNavigationPlugin
];
export const Navigation = Navigator;
export const store = init({
plugins,
models: {
// add any app rematch models here
}
});
I’ve left out adding any custom models for now to focus on just getting the store created. Notice that I used the @rematch/react-navigation
package, which allows me to create a rematch-friendly plugin with no trouble. I just need to pass it my existing navigation routes from routes.js
and the initial screen name to display.
It’s not a hard requirement to add react-navigation
into the Redux store and in fact I don’t recommend doing so unless you know you’ll use it. Why? Because you can use the methods that react-navigation
exposes regardless of your store. The only benefit you’ll receive by adding it to your store is that you’ll have easier access to the current state of your app’s navigation from the store, which is only necessary inside action creators or reducers.
The createReactNavigationPlugin
produces two things: Navigator
and reactNavigationPlugin
. The latter is a plugin that should be added to the array of store plugins. Navigator
is a React component— this is going to replace the Router
component that’s rendered inside App.js
.
Here’s the new complete example of App.js
, including the required Provider
component imported from the react-redux
package:
// App.js
import { AppRegistry } from 'react-native';
import React from 'react';
import { Provider } from 'react-redux';
import { store, Navigator } from './src/store.js';
const App = () => (
<Provider store={store}>
<Navigator />
</Provider>
);
AppRegistry.registerComponent('myapp', () => App);
Once Provider
is added, the connect
function can be imported from react-redux
to connect a component to the Redux store:
// ListContainer.js
import { connect } from 'react-redux';
import List from '../../components/List';
export default connect(
({ ListStore }) => ({
data: ListStore.data
}),
({ ListStore }) => ({
startFetch: ListStore.startFetch
})
)(List);
That’s it for now!
I hope some of this has been helpful to share for your own learning or just your curiosity. I would say that so far neither react-navigation
or rematch
have thrown up any potholes for me to circumnavigate in my slowly growing React Native app and I’m glad I settled on using these two libraries. If you like using Rematch for state management and are feeling confident, I’d also recommend adding Rematch Select to add selectors to make it easier to pluck out the exact pieces of state you need for your scenarios.
Am I still enjoying React Native? Definitely, although I’m itching to do more with it. This won’t be the last part of this React Native for Beginners series of posts for sure.
There’s one major elephant in the room that I haven’t touched upon yet that I’d like to cover in the next part, which is how to produce production-ready app builds for both iOS and Android. In fact, so far I’ve not been able to produce an Android binary to install and run on my phone due to command-line errors thrown when I attempt to build following the steps given in the official React Native build guide. I’ll be persisting with this and reporting back once I’ve got some builds and ironed out the kinks!
Interested in getting started with React Native app development? Have a look at Part 1 of React Native for Beginners