React Native
In this chapter, we're going to walk through how to build your first React Native app. We won't get to cover React Native in-depth in this chapter. React Native is a huge topic that warrants its own book.
React Native book
For more in-depth review of React Native, check out our book on React Native at www.fullstackreact.com/react-native/.
We're going to explain React Native for the React Developer. By the end of this chapter you'll be able to take your React web app and have the foundation to turn it into a React Native app.
As we've seen so far, React is both fun and powerful. In this chapter, the goal is to get an idea of how everything we love about React can be used to build a native iOS and Android application using React Native.
Without getting too deep into the technicalities of how React Native works under the hood, the high-level summary looks like this:
When we build React components, we're actually building representations of our React components, not actual DOM elements using the Virtual DOM. In the browser, React takes this virtual DOM and pre-computes what the elements should look like in a web browser and then hands it over to the browser to handle the layout.
The idea behind React Native is that we can take the tree of object representations from our React components and rather than mapping it to the web browser's DOM, we map it to iOS's UIView or Android’s android.view. Theoretically, we can use the same idea behind React in the web browser to build native iOS and Android applications. This is the fundamental idea behind React-Native.
In this section, we're going to work through building React components for the native renderers and highlight the differences between React for Native vs. React for the web.
Before we dive into the details, let’s start off high level. It's crucial to understand that we're no longer building for the web environment, which means we have different UX, UI, and no URL locations. Despite the fact that the thought processes we've built around building UIs for the web help, they aren't enough to translate to building a great user experience in the mobile native environment.
Let's take a second and look at some of the most popular native applications and play around with them. Rather than use the application from a user perspective, try imagining building the application, taking note of the details. For instance, look for the micro-animations, how the views are setup, the page transitions, the caching, how data is passed around from screen to screen, what happens when we're waiting for information to load.
In general, we can get away with less deliberately designed UI on the web. On mobile however, where our screen-size and data details are more constrained and focused, we are forced to focus on the experience of our application. Building a great UX for a native app requires deliberate execution and attention to detail.
In this chapter one of our goals is to highlight both the syntactic differences as well as the aesthetic differences that exist between both platforms.
Init
As we're focused on getting directly to the code as we work through this section, we'll want to have an application bootstrapped for us so we can experiment and test out building native applications. We'll need to bootstrap our application so we have a basic application we can work with as we are learning the different components of React-Native.
In order to bootstrap a React-Native application, we can use the react-native-cli
tool. We'll need to install the React-Native cli tool by using the Node Package Manager (npm
). Let's install the react-native-cli
tool globally so we can access it anywhere on our system:
npm i -g [email protected]
Installation documentation
For formal instructions on getting set up with React Native for your specific development environment, check out the official Getting started docs.
With the react-native-cli
tool installed, we will have the react-native
command available in our terminal. If this returns a "not recognized" error, check the getting started docs from above for your development platform.
$ react-native
To create a new React-Native project, we'll run the react-native init
command with the name of the application we want to generate. For example, let's generate a React-Native application called Playground
:
$ react-native init Playground
Your output of the command might look a bit different from above, but as long as you see the instructions on how to launch it. If you see an error, check the Troubleshooting documentation for more instructions on launching the application.
Now we can use our Playground
app we just created to test and run the code we'll create throughout this section.
Routing
Let’s start off our application with routing as it’s foundational to nearly every application.
When we implement routing in the web, we're typically mapping a URL to a particular UI. For example, let's take a game that might be written for the web with multiple screens written using the React Router V2 API:
When we implement routing on the web, we're typically mapping some URL to a specific UI. Here’s how that looks with React Router V2 with the URLs mapped to the active components:
export const Routes = (
<Router history={hashHistory}>
<Route path='/' component={Main}>
<IndexRoute component={Home} />
<Route
path='players/:playerOne?'
component={PromptContainer}
/>
<Route
path='battle'
component={ConfirmBattleContainer}
/>
<Route
path='results'
component={ResultsContainer}
/>
</Route>
</Router>
);
URL | Active Component |
---|---|
foo.com | Main -> Home |
foo.com/players | Main -> PromptContainer |
foo.com/players/ari | Main -> PromptContainer |
foo.com/battle | Main -> ConfirmBattleContainer |
foo.com/results | Main -> ResultsContainer |
React Router lays out this mapping between URL and UI beautifully (i.e. declaratively).
As our user navigates to different URLs in our applications, different components become active. The URL is so foundational to React Router that it’s mentioned twice in the opening description:
React Router keeps your UI in sync with the URL. ...Make the URL your first thought, not an after-thought.
This is a clean, well-understood paradigm and one that has made React Router as popular as it is. However, what if we’re developing for a native app? There is no URL we can refer. There’s not really even a similar conceptual mapping of a URL in a native application.
Paradigm shift #1 — when building a React Native app, we need to think of our routes in terms of layers rather than URL to UI mappings.
The routing layers in a native application structure themselves in a stack format, which roughly speaking is basically an array. When we transition between routes in React Native, we're simply pushing (navigating into a view) or popping (navigating out of a view) a route onto or from our route stack.
What is a route stack?
A route stack refers to an array of views a user visits throughout the application. When the user first opens the application, the route stack is going to have a length of 1 (the initial screen). Let's say that the user navigates to a my account screen next. The route stack will have two entries, the initial screen and the account screen.
When the user navigates back to the home screen in our fictitious application, the route stack will pop (or remove the latest) view leaving the route stack to contain one entry again: the initial screen.
Coming from the web browser, this is a different paradigm than mapping a discrete URL. Instead of mapping to a unique URL, we will be managing an array mapping to a route stack.
In addition, we're not just mapping view components to a view, we'll also be working through how to make transitions between routes. Unlike the web, where we typically have independent route transitions, native apps generally need to know about each other as we navigate from one screen to the next.
For example, we'll usually have animations once our view renders, but not have a transition between routes themselves. However, in a native application, we'll still have these animations once our view renders as well as needing to devise a method that one route transitions into the next in the navigation stack.
Most route transitions on native devices are naturally hierarchical and should generally appropriately reflect our user's journey through each phase of the app.
In React Native, we can configure these route transitions through scene configurations. We'll take a look at how we can customize our scene transitions a little bit later, but for now we'll note that different platforms require different route transitions.
On the iOS platform, we're usually handling one of three different transitions between routes:
- Right to left
- Left to right
- Bottom to top
Typically when we push a route on iOS from the right when we're drilling deeper into the same hierarchy, we'll present a view from the bottom when it's a modal screen that is in a different hierarchy. We'll usually push
a new view in the same hierarchy with the left to right transition and pop
from the right to left transition after leaving a previous route.
The Android paradigm is slightly different, however. We'll still have the idea of drilling into more content, but rather than a hard left to right transition, we'll have a concept of creating an elevation change.
Play around with it
Regardless of the device we're building for, it's a good idea to open popular apps for the platform we're working with and taking note of what the route transitions look like between screens.
Enough theory. Let's get back to implementation. React Native routing has several options. At the time of writing, Facebook has indicated it's working on a new router component for React Native. For now, we'll stick with the tried and tested Navigator
component to handle routing in our application.
<Navigator />
Just like the web version of the router, where we have React components that render to handle routing, the <Navigator />
component is just a React component. This means we'll use it just like any other component in the view.
<Navigator />
Knowing the <Navigator />
component is just a react component, we'll want to know:
- What
props
does it accept? - Is it necessary to wrap it in a Higher Order Component?
- How do we navigate with a single component?
Let’s start off with #1, what props
does it accept?
The <Navigator />
only requires two props
, both of them are functions:
configureScene()
renderScene()
Every time we change routes in our application, both the configureScene()
and renderScene()
functions will be called.
The renderScene()
function is responsible for determining which UI (or component) to render next, while the configureScene()
function is responsible for detailing out which transition type to make (as we looked at earlier).
In order to do their job, we'll need to receive some information about the specific route change that is about to occur. Naturally, since these are functions, they will be called with this information as an argument to the function.
Rather than talk about it, let's look at what this looks like in code. Starting with the boilerplate, we know now we'll be rendering a component:
export default class App extends Component {
configureScene(route) {
// Handle configuring scene
}
renderScene(route, navigator) {
// handle rendering scene
}
render() {
return (
<Navigator
renderScene={this.renderScene}
configureScene={this.configureScene}
/>
);
}
}
Beautiful. We'll notice that this answers our second question from above. Typically when we're using the <Navigator />
component, we'll wrap it in a Higher Order Function in order to handle our renderScene()
and configureScene()
methods.
renderScene()
Now let's take a look at how the renderScene()
method needs to be implemented. We currently know two things about the renderScene()
function:
- It's purpose is to figure out which UI (or component) to render when transitioning into a new scene.
- It receives an object which represent the route change that is going to occur.
With these two things in mind, let's look at an example of how a renderScene()
might be implemented:
renderScene(route) {
if (route.home === true) {
return <HomeContainer />
} else if (route.notifications === true) {
return <NotificationsContainer />
} else {
return <FooterTabsContainer />
}
}
Since the entire purpose of the renderScene()
function is to determine which UI (or component) to render next, it makes sense that the renderScene()
function is just one big if statement which renders different components based on a property of the route
object.
The next natural question is "where is the route
object coming from and how did it get the home
and notifications
properties on it?
This is where the react native configuration things get a little bit weird. Remember from above, we mentioned every time we change routes in our applications, both configureScene()
and renderScene()
will be called? Well, we still haven't answered just how we change routes in our applications. To answer that question, we'll need to look at the second argument the renderScene()
function receives: the navigator
:
renderScene(route, navigator) {
// render scene
}
The navigator
object is an instance object we can use to manipulate our current routing from within our application. The object itself contains some pretty handy methods, including the push()
and pop()
methods. The push()
and pop()
methods are how we're actually handling manipulating our route changes from within our app by pushing to or popping from the route stack.
Notice that we're receiving the navigator
object inside our renderScene()
method here, but the renderScene()
method is for picking the next scene, not invoking route changes. In order to actually use the navigator
object in our rendered scene, we'll need to pass it along to our new route as a prop
:
renderScene(route, navigator) {
if (route.home === true) {
return <HomeContainer navigator={navigator} />
} else if (route.notifications === true) {
return <NotificationsContainer navigator={navigator} />
} else {
return <FooterTabsContainer navigator={navigator} />
}
}
With this update, the <HomeContainer />
, <NotificationsContainer />
, and the <FooterTabsContainer />
all have access to the navigator
instance and can each call out to push()
or pop()
from the route stack using this.props.navigator
.
For instance, let's say that we're working on our home route and we want to navigate to the notifications route. We can push to the navigation's route in a method like so: