The smallest logical unit in a React application is the component: a function that transforms input into a nested set of views rendered based on a set of parameters. The React ecosystem is overflowing with these components; oftentimes we import them from external libraries.
This chapter will introduce you to the mechanics involved in importing components, building your own components, and using JavaScript libraries that support the React approach to building complex applications.
React applications with lots of components that do one thing are easier to compose, organize, and maintain.
Cut down the repetition by implementing your own <ScreenHeader />
component.
In this example, I’m using the react-native-elements
component library to
render a <Header />
component. See Recipe 2.3 for an
example of how to import a custom component.
You will notice in this example the references to colors
and globalStyles
.
These were imported from an external file at the top of the file:
import { colors, globalStyles } from '../styles';
.
See Chapter 3 for more details on defining global colors and styles.
A Home screen has the following JSX inside the render()
function:
<
View
style
=
{
globalStyles
.
headerContainer
}
>
<
Header
leftComponent
=
{
<
Button
icon
=
{{
name
:
'arrow-back'
}}
buttonStyle
=
{{
backgroundColor
:
null
,
padding
:
0
,
}}
title
=
''
color
=
{
colors
.
WHITE
}
onPress
=
{
this
.
backPressed
}
/>
}
centerComponent
=
{
<
Text
style
=
{
globalStyles
.
siteHeaderText
}
>
{
this
.
props
.
data
.
me
.
first_name
}
<
/Text>
}
rightComponent
=
{
<
Button
icon
=
{{
name
:
'home'
}}
buttonStyle
=
{{
backgroundColor
:
null
,
padding
:
0
,
}}
title
=
''
color
=
{
colors
.
WHITE
}
onPress
=
{
this
.
goHome
}
/>
}
/>
<
/View>
A Course screen has something that looks very similar:
<
View
style
=
{
globalStyles
.
headerContainer
}
>
<
Header
leftComponent
=
{
<
Button
icon
=
{{
name
:
'arrow-back'
}}
buttonStyle
=
{{
backgroundColor
:
null
,
padding
:
0
,
}}
title
=
''
color
=
{
colors
.
WHITE
}
onPress
=
{
this
.
back
}
/>
}
centerComponent
=
{
<
Text
style
=
{
globalStyles
.
siteHeaderText
}
>
{
this
.
course
().
name
}
<
/Text>
}
rightComponent
=
{
<
Button
icon
=
{{
name
:
'settings'
}}
buttonStyle
=
{{
backgroundColor
:
null
,
padding
:
0
,
}}
title
=
''
color
=
{
colors
.
WHITE
}
onPress
=
{
this
.
goHome
}
/>
}
/>
<
/View>
I see a lot of repetition, especially given that every single screen will have
some variation of this <Header />
. Ideally, I would be able to reference a
component that emphasizes the differences and hides the complexity:
<
ScreenHeader
leftComponentIcon
=
'arrow-back'
leftOnPress
=
{
this
.
back
}
centerText
=
{
this
.
course
().
name
}
rightIcon
=
'settings'
rightOnPress
{
this
.
goHome
}
/>
Create a new file in your project in a components folder—components/screenHeader.js:
import
React
,
{
Component
}
from
'react'
;
import
{
Text
,
View
,
}
from
'react-native'
;
import
{
Button
,
Header
,
}
from
'react-native-elements'
;
import
{
colors
,
globalStyles
}
from
'../styles'
;
import
PropTypes
from
'prop-types'
;
class
ScreenHeader
extends
Component
{
render
()
{
return
<
View
style
=
{
globalStyles
.
headerContainer
}
>
<
Header
leftComponent
=
{
<
Button
icon
=
{{
name
:
this
.
props
.
leftIcon
}}
buttonStyle
=
{{
backgroundColor
:
null
,
padding
:
0
,
}}
title
=
''
color
=
{
colors
.
WHITE
}
onPress
=
{
this
.
props
.
leftOnPress
}
/>
}
centerComponent
=
{
<
Text
style
=
{
globalStyles
.
siteHeaderText
}
>
{
this
.
props
.
centerText
}
<
/Text>
}
rightComponent
=
{
<
Button
icon
=
{{
name
:
this
.
props
.
rightIcon
}}
buttonStyle
=
{{
backgroundColor
:
null
,
padding
:
0
,
}}
title
=
''
color
=
{
colors
.
WHITE
}
onPress
=
{
this
.
props
.
rightOnPress
}
/>
}
/>
<
/View>
}
}
ScreenHeader
.
propTypes
=
{
leftIcon
:
PropTypes
.
string
,
rightIcon
:
PropTypes
.
string
,
centerText
:
PropTypes
.
string
,
leftOnPress
:
PropTypes
.
func
,
rightOnPress
:
PropTypes
.
func
,
};
export
default
ScreenHeader
;
We can now keep our screen code focused on the different implementations and expose an API with a
handful of PropTypes
that the developer can pass to <ScreenHeader />
.
Almost all applications rely on activities that require the user to wait for an operation to complete. In some cases this can simply be the time required for a client to receive a message from a web server or third-party API. Another example might be waiting for an image to be processed in a background thread on the device.
How do we communicate to users that they need to wait?
Let’s add a progress bar. This is a great task to introduce the steps required
to import React Native components. Here we will import the component and discuss
linking the libART.a
library to our project. In Recipe 3.4 we will
create an indeterminate progress animation.
Most open source React Native components have comprehensive README.md files that describe how to include the component and whether it’s been designed to work in iOS, Android, or both.
Make sure the development server isn’t running when you add new packages using
Yarn or Node. The React Packager may not pick up the new libraries and you will
probably need to run react-native link
and rebuild the project binary.
Start by adding react-native-progress
to your project:
$>
npm install react-native-progress --save$>
react-native link
Usually calling react-native link
is all that’s required to add the necessary
iOS or Android libraries to the project build process. In this case,
react-native-progress
relies on a special library for iOS called ReactART
for drawing pie charts.
Let’s link the ReactART library manually after calling react-native link
. Figure 2-1
shows a project I created called RNScratchPad in Xcode.
Expand the Libraries folder in the project view, as shown in Figure 2-2.
Start by adding a reference to the ART.xcodeproject file included with React Native in node_modules/react-native/Libraries/ART (Figure 2-3).
Under Linked Frameworks and Libraries, find the + symbol. libART.a should be available as a library to add to your project (Figure 2-4).
Your project configuration should now include this reference (Figure 2-5).
Now rebuild the project and deploy the app on your Simulator or development device. Let’s add a simple progress bar to one of our components:
import
React
,
{
Component
}
from
'react'
;
import
{
View
}
from
'react-native'
;
import
*
as
Progress
from
'react-native-progress'
;
export
default
class
App
extends
Component
<
{}
>
{
render
()
{
return
(
<
View
style
=
{{
flex
:
1
,
justifyContent
:
'center'
,
alignItems
:
'center'
}}
>
<
Progress
.
Pie
progress
=
{
0.2
}
size
=
{
50
}
color
=
"#2245FF"
/>
<
/View>
);
}
}
You should see something like this in the Simulator:
Notice that by changing the progress
attribute, the progress bar changes.
We can animate progress changes by relying on a local this.state.progress
variable. Here is a more complete example:
import
React
,
{
Component
}
from
'react'
;
import
{
Text
,
TouchableHighlight
,
View
}
from
'react-native'
;
import
*
as
Progress
from
'react-native-progress'
;
export
default
class
App
extends
Component
<
{}
>
{
constructor
(
props
)
{
super
(
props
);
this
.
state
=
{
progress
:
0.2
}
}
randomProgress
=
()
=>
{
const
progress
=
Math
.
random
();
this
.
setState
({
progress
});
}
render
()
{
return
(
<
View
style
=
{{
flex
:
1
,
justifyContent
:
'center'
,
alignItems
:
'center'
}}
>
<
View
style
=
{{
marginBottom
:
10
}}
>
<
Progress
.
Pie
borderWidth
=
{
2
}
borderColor
=
'#62321B'
unfilledColor
=
'#F5F5F5'
progress
=
{
this
.
state
.
progress
}
size
=
{
100
}
color
=
'#D6C598'
/>
<
/View>
<
TouchableHighlight
onPress
=
{
this
.
randomProgress
}
style
=
{{
padding
:
10
,
backgroundColor
:
'#CACACA'
,
borderRadius
:
5
}}
>
<
Text
style
=
{{
fontSize
:
18
,
fontWeight
:
'bold'
}}
>
Apple
Pie
Me
!<
/Text>
<
/TouchableHighlight>
<
/View>
);
}
}
Tapping <TouchableHighlight />
will result in different pie servings!
Learn how to animate the progress bar in Recipe 3.4.
You have a collection of components that are worth using on multiple projects. Copying and pasting them between projects is not going to cut it.
How do you reuse a whole section of your React Native application in another
project? For example, you might have created a component library that includes
all of the visual identity requirements for your product. Naturally, you want to
share this across multiple projects and only have to make visual changes for these
components in one place. This approach enables reuse and also means that you can
version portions of your application more easily and reinforce your product’s
architectural boundaries. In my case, I’ve built a <PastryPicker />
component—critical to visualizing
the relative amount of flour, sugar, butter, and eggs across baked goods (Figure 2-6).
The sample project includes one component that I will separate into its own NPM package, pastryPicker.js. See Recipe 1.1 for details.
The main application, App.js, references <PastryPicker />
:
// App.js
import
React
,
{
Component
}
from
'react'
;
import
{
Text
,
TouchableHighlight
,
View
}
from
'react-native'
;
import
{
PastryPicker
}
from
'./pastryPicker'
;
export
default
class
App
extends
Component
{
render
()
{
return
(
<
View
style
=
{{
flex
:
1
,
justifyContent
:
'center'
,
alignItems
:
'center'
}}
>
<
PastryPicker
/>
<
/View>
);
}
};
The PastryPicker component lives in one file (note that the pastry icon characters pictured in Figure 2-6 have been omitted from the code for font reasons):
// pastryPicker.js
import
React
,
{
Component
}
from
'react'
;
import
{
Animated
,
StyleSheet
,
Text
,
TouchableHighlight
,
View
,
}
from
'react-native'
;
const
PASTRIES
=
{
croissant
:
{
label
:
'Croissants'
,
flour
:
0.7
,
butter
:
0.5
,
sugar
:
0.2
,
eggs
:
0
},
cookie
:
{
label
:
'Cookies'
,
flour
:
0.5
,
butter
:
0.4
,
sugar
:
0.5
,
eggs
:
0.2
},
pancake
:
{
label
:
'Pancakes'
,
flour
:
0.7
,
butter
:
0.5
,
sugar
:
0.3
,
eggs
:
0.3
},
doughnut
:
{
label
:
'Dougnuts'
,
flour
:
0.5
,
butter
:
0.2
,
sugar
:
0.8
,
eggs
:
0.1
},
};
export
default
class
PastryPicker
extends
Component
{
constructor
(
props
)
{
super
(
props
);
this
.
state
=
{
selectedPastry
:
'croissant'
}
}
setPastry
=
(
selectedPastry
)
=>
{
this
.
setState
({
selectedPastry
});
}
renderIngredient
(
backgroundColor
,
flex
,
label
)
{
return
<
View
style
=
{
styles
.
ingredientColumn
}
>
<
View
style
=
{
styles
.
bar
}
/>
<
View
style
=
{{
backgroundColor
,
flex
}}
/>
<
View
style
=
{
styles
.
label
}
><
Text
>
{
label
}
<
/Text></View>
<
/View>
}
render
()
{
const
{
flour
,
butter
,
sugar
,
eggs
}
=
PASTRIES
[
this
.
state
.
selectedPastry
];
return
<
View
style
=
{
styles
.
pastryPicker
}
>
<
View
style
=
{
styles
.
buttons
}
>
{
Object
.
keys
(
PASTRIES
).
map
(
(
key
)
=>
<
View
key
=
{
key
}
style
=
{
styles
.
buttonContainer
}
>
<
TouchableHighlight
style
=
{[
styles
.
button
,
{
backgroundColor
:
key
===
this
.
state
.
selectedPastry
?
'#CD7734'
:
'#54250B'
}
]}
underlayColor
=
'CD7734'
onPress
=
{()
=>
{
this
.
setPastry
(
key
)
}
}
>
<
Text
style
=
{
styles
.
buttonText
}
>
{
PASTRIES
[
key
].
label
}
<
/Text>
<
/TouchableHighlight>
<
/View>)
}
<
/View>
<
View
style
=
{
styles
.
ingredientContainer
}
>
{
this
.
renderIngredient
(
'#F2D8A6'
,
flour
,
'Flour'
)}
{
this
.
renderIngredient
(
'#FFC049'
,
butter
,
'Butter'
)}
{
this
.
renderIngredient
(
'#CACACA'
,
sugar
,
'Sugar'
)}
{
this
.
renderIngredient
(
'#FFDE59'
,
eggs
,
'Eggs'
)}
<
/View>
<
/View>
}
}
const
styles
=
StyleSheet
.
create
({
pastryPicker
:
{
flex
:
1
,
flexDirection
:
'column'
,
margin
:
20
,
},
ingredientContainer
:
{
flex
:
1
,
flexDirection
:
'row'
,
},
ingredientColumn
:
{
flexDirection
:
'column'
,
flex
:
1
,
justifyContent
:
'flex-end'
,
},
buttonContainer
:
{
margin
:
10
,
},
bar
:
{
alignSelf
:
'flex-start'
,
flexGrow
:
0
,
},
button
:
{
padding
:
10
,
minWidth
:
140
,
justifyContent
:
'center'
,
backgroundColor
:
'#5A8282'
,
borderRadius
:
10
,
},
buttonText
:
{
fontSize
:
18
,
color
:
'#FFF'
,
},
buttons
:
{
flexDirection
:
'column'
,
flexWrap
:
'wrap'
,
paddingRight
:
20
,
paddingLeft
:
20
,
flex
:
0.3
,
},
label
:
{
flex
:
0.2
,
},
});
Let’s go through the steps required to pull a collection of components into a separate project where they can be included in multiple React Native projects.
In Recipe 2.2 we referenced an external NPM package for rendering progress bars. Our component is much simpler: it relies entirely on existing React Native components, which means that in our case we can simply create an NPM package with the correct dependencies.
Assuming you have NPM correctly installed, you should be able to create a new
package from the command line. Create a folder for the package and run npm
init
inside it:
$>
mkdir react-native-pastry-picker$>
cd
react-native-pastry-picker$>
npm init This utility will walk you through creating a package.json file. It only covers the most common items, and tries to guess sensible defaults. See`
npmhelp
json`
for
definitive documentation on these fields and exactly what theydo
. Use`
npm install <pkg>`
afterwards to install a package and save it as a dependency in the package.json file. package name:(
projects)
react-native-pastry-picker ...
You will be presented with a series of questions (package name, version, main entry point, etc.). Use the defaults for now; you can change them later. Only the package name is important since that will be the package folder and the reference for the main application.
An emerging convention in the React Native community is to prefix component
libraries with react-native-
and host them on GitHub.
If the command is successful, a package.json file should be automatically created. Let’s add React as a development dependency—a required package for development purposes:
$>
npm i --save-dev react
You should now have a node_modules folder and a package.lock file in the project file. Your package.json file should look something like this:
{
"name"
:
"react-native-pastry-picker"
,
"version"
:
"1.0.0"
,
"description"
:
""
,
"main"
:
"index.js"
,
"scripts"
:
{
"test"
:
"echo "Error: no test specified" && exit 1"
},
"author"
:
"Jon Lebensold"
,
"license"
:
"MIT"
,
"devDependencies"
:
{
"react"
:
"^16.0.0"
}
}
You will notice that key main
points to index.js. The index.js file should serve as a manifest for all
public components. Let’s do a sanity check of our component by creating an index.js file that wraps a simple
<Text />
component:
import
React
,
{
Component
}
from
'react'
;
import
{
Text
,
View
,
}
from
'react-native'
;
export
class
SanityCheck
extends
Component
{
render
()
{
return
<
View
><
Text
>
I
am
an
externally
referenced
component
!<
/Text></View>
}
}
We can now add the component to our main project with a relative reference and restart our development server. Once the package is ready to be published, we can change our package.json file to reference the published name on npmjs.com.
$
>
npm
install
--
save
..
/
react
-
native
-
pastry
-
picker
$
>
yarn
start
--
reset
-
cache
Referencing packages locally from package.json sometimes causes the React Native
Packager to forget to refresh the internal cache. I recommend using Yarn instead of
NPM or react-native start
when relying on a locally referenced dependency.
Learn how to install Yarn at https://yarnpkg.com/en/docs/install.
We can adjust our App.js file to reference the new dependency:
import
React
,
{
Component
}
from
'react'
;
import
{
View
}
from
'react-native'
;
import
{
SanityCheck
}
from
'react-native-pastry-picker'
export
default
class
App
extends
Component
<
{}
>
{
render
()
{
return
(
<
View
style
=
{{
flex
:
1
,
justifyContent
:
'center'
,
alignItems
:
'center'
}}
>
<
SanityCheck
/>
<
/View>
);
}
}
The main application should render <SanityCheck />
as though it was part of the local library. You can now
safely move the components out of the main project and update the index.js in react-native-pastry-picker
to
reference the components internally like this:
export
{
default
as
PastryPicker
}
from
'./pastryPicker'
;
Once your component library is taking shape, make sure you update the package.json file with the appropriate metadata fields. You will probably want to publish the project to NPM so that it can be referenced like any other React Native package.
If you need to call native libraries, then more setup will be required. I recommend looking at
well-supported packages like
react-native-camera
.
Remember that you can use this same approach for sharing application constants, stylesheets, and default
typography or image assets as well!
Most mobile applications need to provide a mechanism for someone to travel between screens seamlessly. The classic example is a list of items, where tapping any item allows the user to drill into the list element. It’s also often the case that there is a portion of the application that is available to someone logged in.
How do we maintain all these different screens without losing track of the global state of our application? How do we ensure seamless transitions between pages? The React Navigation community project aims to address these challenges by providing a set of nesting navigator components.
Start by adding react-navigation
to your project:
$>
npm install --save react-navigation
Let’s break out our application into three navigators:
The top-level navigator for the application.
Provides screen navigation before a user is logged in.
Provides screen navigation inside the application. The root navigator is
passed by reference via screenProps
.
See how the navigators deliver the Login, About, Profile, and Dashboard screens in Figure 2-7.
This example uses two navigators, one of which relies on tab navigation at the bottom of the screen in iOS. See Recipe 3.3 for more information on dealing with vector images.
The styles were pulled into a styles.js file in order to keep the navigation code focused on the problem at hand:
// styles.js
import
{
StyleSheet
}
from
'react-native'
;
export
const
styles
=
StyleSheet
.
create
({
container
:
{
paddingTop
:
30
,
flex
:
1
},
paragraphText
:
{
fontSize
:
16
,
lineHeight
:
20
,
},
titleText
:
{
fontSize
:
24
,
lineHeight
:
30
,
},
primaryButton
:
{
padding
:
20
,
backgroundColor
:
'#124473'
},
primaryButtonText
:
{
color
:
'#FFF'
,
textAlign
:
'center'
,
},
altButton
:
{
padding
:
20
,
backgroundColor
:
'#23CdA4'
},
altButtonText
:
{
color
:
'#FFF'
,
textAlign
:
'center'
,
}
});
There are four screens in this example: AboutScreen
, LoginScreen
,
DashboardScreen
, and ProfileScreen
. Each screen has its own file and is referenced in App.js. The flow through the different screens can be seen in Figure 2-8.
// About Screen
import
React
,
{
Component
}
from
'react'
;
import
{
TouchableHighlight
,
View
,
Text
}
from
'react-native'
;
import
{
styles
}
from
'./styles'
;
export
default
class
AboutScreen
extends
Component
<
{}
>
{
render
()
{
return
<
View
style
=
{
styles
.
container
}
>
<
Text
style
=
{
styles
.
titleText
}
>
About
Screen
<
/Text>
<
TouchableHighlight
style
=
{
styles
.
primaryButton
}
onPress
=
{
this
.
props
.
navigation
.
goBack
}
<
Text
style
=
{
styles
.
primaryButtonText
}
>
Go
Back
<
/Text>
<
/TouchableHighlight>
<
/View>
}
}
See Figure 2-9 for an example of the LoginScreen
component.
// Login Screen
import
React
,
{
Component
}
from
'react'
;
import
{
TouchableHighlight
,
View
,
Text
}
from
'react-native'
;
import
{
styles
}
from
'./styles'
;
export
default
class
LoginScreen
extends
Component
<
{}
>
{
about
=
()
=>
{
const
{
navigate
}
=
this
.
props
.
navigation
navigate
(
'about'
);
}
login
=
()
=>
{
const
{
navigate
}
=
this
.
props
.
navigation
;
// some login code here...
navigate
(
'user'
,
{
user
:
{
name
:
'Sam Smith'
,
:
'[email protected]'
}
})
}
render
()
{
return
<
View
style
=
{
styles
.
container
}
>
<
Text
style
=
{
styles
.
titleText
}
>
Login
Screen
<
/Text>
<
TouchableHighlight
style
=
{
styles
.
primaryButton
}
onPress
=
{
this
.
about
}
>
<
Text
style
=
{
styles
.
primaryButtonText
}
>
About
<
/Text>
<
/TouchableHighlight>
<
TouchableHighlight
style
=
{[
styles
.
altButton
,
{
marginTop
:
20
}
]}
onPress
=
{
this
.
login
}
>
<
Text
style
=
{
styles
.
altButtonText
}
>
Login
<
/Text>
<
/TouchableHighlight>
<
/View>
}
}
The Dashboard Screen component extracts user()
state from the RootNavigator
(Figure 2-10).
// Dashboard Screen
import
React
,
{
Component
}
from
'react'
;
import
{
View
,
Text
}
from
'react-native'
;
import
{
styles
}
from
'./styles'
;
import
Icon
from
'react-native-vector-icons/FontAwesome'
;
export
default
class
Screen
extends
Component
{
static
navigationOptions
=
{
title
:
'Dashboard'
,
tabBarIcon
:
({
tintColor
})
=>
<
Icon
name
=
'home'
color
=
{
tintColor
}
/>
}
user
()
{
const
{
rootNavigation
}
=
this
.
props
.
screenProps
;
return
rootNavigation
.
state
.
params
.
user
;
}
render
()
{
const
{
name
,
}
=
this
.
user
();
return
<
View
style
=
{
styles
.
container
}
>
<
Text
style
=
{
styles
.
titleText
}
>
{
`Welcome,
${
name
}
<
${
}
>,
let's get cooking!`
}
<
/Text>
<
/View>
}
}
The Profile Screen (seen in Figure 2-11) demonstrates resetting the navigation state with the logout()
.
// Profile Screen
import
React
,
{
Component
}
from
'react'
;
import
{
TouchableHighlight
,
View
,
Text
}
from
'react-native'
;
import
Icon
from
'react-native-vector-icons/FontAwesome'
;
import
{
styles
}
from
'./styles'
;
export
default
class
Screen
extends
Component
<
{}
>
{
static
navigationOptions
=
{
title
:
'Profile'
,
tabBarIcon
:
({
tintColor
})
=>
<
Icon
name
=
'user'
color
=
{
tintColor
}
/>
}
logout
=
()
=>
{
const
{
rootNavigation
}
=
this
.
props
.
screenProps
;
rootNavigation
.
goBack
()
}
render
()
{
return
<
View
style
=
{
styles
.
container
}
>
<
Text
style
=
{
styles
.
titleText
}
>
Profile
Screen
<
/Text>
<
TouchableHighlight
style
=
{
styles
.
primaryButton
}
onPress
=
{
this
.
logout
}
>
<
Text
style
=
{
styles
.
primaryButtonText
}
>
Log
Out
<
/Text>
<
/TouchableHighlight>
<
/View>
}
}
Finally, App.js ties the whole thing together with three navigators:
// App.js
import
React
,
{
Component
}
from
'react'
;
import
{
StackNavigator
,
TabNavigator
}
from
'react-navigation'
;
// Screens
import
DashboardScreen
from
'./dashboardScreen'
;
import
ProfileScreen
from
'./profileScreen'
;
import
LoginScreen
from
'./loginScreen'
;
import
AboutScreen
from
'./aboutScreen'
;
// Navigators
const
GuestRouteConfig
=
{
login
:
{
screen
:
LoginScreen
},
about
:
{
screen
:
AboutScreen
},
}
const
GuestNavigator
=
StackNavigator
(
GuestRouteConfig
,
{
headerMode
:
'none'
}
);
const
UserRouteConfig
=
{
dashboard
:
{
screen
:
DashboardScreen
},
profile
:
{
screen
:
ProfileScreen
},
}
const
UserNavigator
=
TabNavigator
(
UserRouteConfig
,
{
activeTintColor
:
'#125000'
});
// Pass the RootNavigator down to the UserNavigator:
const
WrappedNavigator
=
({
navigation
})
=>
<
UserNavigator
screenProps
=
{
{
rootNavigation
:
navigation
}
}
/>
const
RootRouteConfig
=
{
guest
:
{
screen
:
GuestNavigator
},
user
:
{
screen
:
WrappedNavigator
},
}
export
default
StackNavigator
(
RootRouteConfig
,
{
headerMode
:
'none'
});
Even though this is a lengthy example, it is a very common pattern and worth
exploring. You will notice that the UserNavigator
is actually wrapped in a
higher order component, which passes the RootNavigator
down as an additional
screenProp
called rootNavigation
. This parameter is critical for
passing successful login parameters down to the UserNavigator
and enables
the ProfileScreen
to trigger a logout
, resetting the RootNavigator
to a
default state.
React Navigation works very well with libraries like Redux and the ApolloClient for handling client/server interactions. The React Navigation Redux Integration guide provides a starting point. React Navigation isn’t the only navigation library available to React Native developers. React Native Navigation is a well-maintained alternative.
The moment you find yourself with more than one screen, state management
decisions will need to be made. Whether you decide to follow a flux-inspired
architecture like Redux or to implement your own global storage
with AsyncStorage
, the question of how to keep the data that matters locally
decoupled from broader state management will enter the picture.
How do you manage state components without creating bidirectional dependencies? These problems are everywhere in application design. A common case is a long-running task that can be interrupted by a user, but also must announce its completion. Enter global state management with Redux. This example app will store a password based on four word-tiles. Once logged in, users will be able to set some secret text. This app enables a user to:
Set a tile-based password and log in (like a pin-pad)
Set some secret text
Log out
Log in with the password
Reset the application state
Correct their login attempt and retry
First we need a few libraries for Redux and React to work together. I also use
redux-logger
in development mode to log all state transitions in the React
remote debugger.
Install react-redux
, redux
, and redux-logger
(optional):
$>
npm i --save react-redux$>
npm i --save redux$>
npm i --save redux-logger
The project folder structure looks like this:
App.js reduxStore.js ... src ├── actions.js ├── appContainer.js ├── components │ ├── tile.js │ └── tileMap.js ├── constants.js ├── loginForm.js ├── myHome.js ├── reducers.js ├── setPassword.js ├── styles.js └── types.js
See Recipe 1.3 for examples on organizing your project files. Given that this example focuses on Redux, I’ve tried to limit the number of files and folders. In a larger application, screen-based (e.g., home/, login/) or type (e.g., reducers/, actions/) folders are more appropriate.
The same TileMap component can be used to set a password, as in Figure 2-12.
The App.js file is devoted entirely to the redux integration:
// App.js
import
React
,
{
Component
}
from
'react'
;
import
AppContainer
from
'./src/appContainer'
;
import
{
Provider
}
from
'react-redux'
;
import
store
from
'./reduxStore'
;
export
default
class
App
extends
Component
<
{}
>
{
render
()
{
return
<
Provider
store
=
{
store
}
><
AppContainer
/><
/Provider>
}
}
The store
is defined in a separate file so that it can be referenced globally. This is not
commonly required, but in some exceptional circumstances (particularly when there is no remote
backend store), access to the state from actions can be necessary. The
redux-logger
is configured as middleware in the store. This library is an optional
piece of additional functionality that will log all state and action changes to
the web browser debugger console:
// reduxStore.js
import
*
as
reducers
from
'./src/reducers'
import
{
createStore
,
applyMiddleware
,
combineReducers
,
compose
}
from
'redux'
;
import
logger
from
'redux-logger'
;
export
default
createStore
(
combineReducers
(
reducers
),
applyMiddleware
(
logger
)
);
The AppContainer
relies on the appState
reducer to determine which screens to render:
// src/appContainer.js
import
ActionCreators
from
'./actions'
;
import
{
bindActionCreators
}
from
'redux'
;
import
{
connect
}
from
'react-redux'
;
import
SetPassword
from
'./setPassword'
;
import
LoginForm
from
'./loginForm'
;
import
MyHome
from
'./myHome'
;
import
{
styles
}
from
'./styles'
;
class
AppContainer
extends
Component
{
renderLoginMessage
()
{
return
<
Text
style
=
{
styles
.
loginMessage
}
>
{
this
.
props
.
appState
.
loginMessage
}
<
/Text>
}
render
()
{
const
{
isLoggedIn
,
loginMessage
,
isPasswordSet
}
=
this
.
props
.
appState
;
return
<
View
style
=
{
styles
.
container
}
>
{
isLoggedIn
&&
<
MyHome
/>
}
{
!
isLoggedIn
&&
!
isPasswordSet
&&
<
SetPassword
/>
}
{
!
isLoggedIn
&&
isPasswordSet
&&
<
LoginForm
/>
}
{
loginMessage
&&
this
.
renderLoginMessage
()
}
<
/View>
}
}
export
default
connect
(
({
appState
})
=>
{
return
{
appState
}
},
(
dispatch
)
=>
bindActionCreators
(
ActionCreators
,
dispatch
)
)(
AppContainer
);
Redux applications naturally produce a listing of supported events that the application must support. There are a number of libraries that aim to reduce the amount of boilerplate, but in the interest of simplicity, I’ve decided to rely on the minimum number of external dependencies:
// src/types.js
export
const
LOGIN
=
'LOGIN'
;
export
const
LOGOUT
=
'LOGOUT'
;
export
const
RESET
=
'RESET'
;
export
const
SET_PASSWORD_AND_LOGIN
=
'SET_PASSWORD_AND_LOGIN'
;
export
const
SET_SECRET
=
'SET_SECRET'
;
export
const
SET_LOGIN_MESSAGE
=
'SET_LOGIN_MESSAGE'
;
These actions are exposed to the entire application as ActionCreators, which can be used
to dispatch events that the reducers can choose to respond to. ActionCreators
can
sometimes also handle some delegation to global business logic. Instead of relying on
a backend service for user authentication, I’ve referred to the store in order to
extract the user state and trigger the correct action. This example demonstrates how actions
don’t always map one-to-one with types and stores:
// src/actions.
import
*
as
types
from
'./types'
;
// Used for authentication
import
store
from
'../reduxStore'
;
function
setSecret
(
secret
)
{
return
{
type
:
types
.
SET_SECRET
,
secret
}
}
function
setPasswordAndLogin
(
password
)
{
return
{
type
:
types
.
SET_PASSWORD_AND_LOGIN
,
password
}
}
function
attemptLogin
(
password
)
{
const
{
user
}
=
store
.
getState
();
return
(
user
.
password
===
password
)
?
{
type
:
types
.
LOGIN
}
:
{
type
:
types
.
SET_LOGIN_MESSAGE
,
loginMessage
:
"Login Incorrect"
}
}
function
reset
()
{
return
{
type
:
types
.
RESET
,
}
}
function
logout
()
{
return
{
type
:
types
.
LOGOUT
,
}
}
function
setLoginMessage
(
message
)
{
return
{
type
:
types
.
SET_LOGIN_MESSAGE
,
message
}
}
export
default
ActionCreators
=
{
setSecret
,
setPasswordAndLogin
,
attemptLogin
,
reset
,
logout
,
setLoginMessage
,
}
We will rely on a single store with two reducers, an appState
and a user
reducer. Unlike a more common TODO example, this example
demonstrates multiple reducers and how actions can be used for global state
management.
Both reducers are exported from src/reducers.js. A createReducer()
function provides some
syntactic sugar for avoiding pure case
statements in the reducer. Notice how the appState
and
user
reducers both respond to types.RESET
and types.SET_PASSWORD_AND_LOGIN
. Also consider
that the reducers do not determine whether the person should log in; they merely process the event
and return the appropriate state transformation to their part of the store
:
// src/reducers.js
import
*
as
types
from
'./types'
// Helper function for avoiding switch() statements (commonly viewed
// as a code smell) in reducers:
function
createReducer
(
initialState
,
handlers
)
{
return
function
reducer
(
state
=
initialState
,
action
)
{
if
(
handlers
.
hasOwnProperty
(
action
.
type
))
{
return
handlers
[
action
.
type
](
state
,
action
);
}
else
{
return
state
;
}
}
}
export
const
user
=
createReducer
({
password
:
null
,
secret
:
null
},
{
[
types
.
RESET
](
state
,
{
}
)
{
return
{
password
:
null
,
secret
:
null
}
},
[
types
.
SET_SECRET
](
state
,
{
secret
}
)
{
return
{
...
state
,
secret
}
},
[
types
.
SET_PASSWORD_AND_LOGIN
](
state
,
{
password
}
)
{
return
{
...
state
,
password
};
},
});
const
initialAppState
=
{
loginMessage
:
null
,
isLoggedIn
:
false
,
isPasswordSet
:
false
};
export
const
appState
=
createReducer
(
initialAppState
,
{
[
types
.
LOGOUT
](
state
,
{}
)
{
return
{
...
state
,
isLoggedIn
:
false
}
},
[
types
.
LOGIN
](
state
,
{}
)
{
return
{
...
state
,
isLoggedIn
:
true
,
loginMessage
:
null
}
},
[
types
.
SET_LOGIN_MESSAGE
](
state
,
{
loginMessage
}
)
{
return
{
...
state
,
loginMessage
}
},
[
types
.
RESET
](
state
,
{
}
)
{
return
{
...
initialAppState
};
},
[
types
.
SET_PASSWORD_AND_LOGIN
](
state
,
{
}
)
{
return
{
isLoggedIn
:
true
,
isPasswordSet
:
true
,
loginMessage
:
null
}
},
});
Most of the application styles have been centralized into a global src/styles.js file:
// src/styles.js
import
{
StyleSheet
}
from
'react-native'
;
export
const
styles
=
StyleSheet
.
create
({
loginMessage
:
{
margin
:
10
,
fontSize
:
16
,
padding
:
10
},
rootContainer
:
{
flex
:
1
,
paddingTop
:
30
,
backgroundColor
:
'#FFF'
,
},
buttonGroup
:
{
marginTop
:
10
,
},
container
:
{
paddingTop
:
30
,
flex
:
1
},
title
:
{
fontSize
:
24
,
lineHeight
:
30
,
textAlign
:
'center'
,
},
tileRow
:
{
flexWrap
:
'wrap'
,
flexDirection
:
'row'
,
justifyContent
:
'space-around'
,
},
button
:
{
borderWidth
:
1
,
borderColor
:
'#333'
,
borderStyle
:
'solid'
,
height
:
50
,
},
buttonText
:
{
color
:
'#144595'
,
fontWeight
:
'bold'
,
fontSize
:
16
,
padding
:
10
,
textAlign
:
'center'
,
},
});
The src/constants.js file provides a central list of TILES
that will be used for rendering the
<TileMap />
component, whether for setting a password or for logging in:
// src/constants.js
export
const
TILES
=
{
'Pizza'
:
{
text
:
'Pizza'
,
value
:
'pizza'
,
index
:
null
,
isActive
:
false
},
'Pie'
:
{
text
:
'Pie'
,
value
:
'pie'
,
index
:
null
,
isActive
:
false
},
'Salad'
:
{
text
:
'Salad'
,
value
:
'salad'
,
index
:
null
,
isActive
:
false
},
'Omelette'
:
{
text
:
'Omelette'
,
value
:
'omelette'
,
index
:
null
,
isActive
:
false
},
}
The src/components/ folder contains a few components that were designed to function without any
knowledge of Redux. The <Tile />
component is a pure function that simply returns a JSX
transformation of the tile props
:
// src/components/tile.js
import
React
,
{
Component
}
from
'react'
;
import
{
StyleSheet
,
TouchableHighlight
,
Text
}
from
'react-native'
export
default
function
Tile
({
text
,
id
,
isActive
,
onPress
})
{
const
activeStyle
=
isActive
?
{
borderColor
:
'#F00'
}
:
null
;
return
<
TouchableHighlight
style
=
{[
styles
.
tile
,
activeStyle
]}
onPress
=
{()
=>
onPress
(
id
)
}
>
<
Text
style
=
{
styles
.
tileText
}
>
{
text
}
<
/Text>
<
/TouchableHighlight>
}
const
styles
=
StyleSheet
.
create
({
container
:
{
flex
:
1
,
paddingTop
:
30
,
backgroundColor
:
'#FFF'
,
},
headerText
:
{
color
:
'#144595'
,
fontSize
:
16
,
fontWeight
:
'bold'
,
textAlign
:
'center'
,
},
header
:
{
borderBottomWidth
:
1
,
borderBottomColor
:
'#222'
,
borderStyle
:
'solid'
,
},
tileText
:
{
fontSize
:
16
,
textAlign
:
'center'
,
marginTop
:
60
,
},
tile
:
{
width
:
150
,
height
:
150
,
alignItems
:
'center'
,
backgroundColor
:
'#CCC'
,
borderRadius
:
20
,
borderColor
:
'#222'
,
borderWidth
:
1
,
borderStyle
:
'solid'
,
margin
:
10
,
}
})
The <TileMap />
component renders a collection of <Tile />
components and
orchestrates their state and tap events.
Each <Tile />
provides an onTileChange
handler that returns a password
as a string. <Tile />
will render anything in this.props.children
that the parent component
may want to include, such as special buttons.
Here’s an implementation of the <TileMap />
:
// src/components/tileMap.js
import
React
,
{
Component
}
from
'react'
;
import
{
View
,
TouchableHighlight
,
Text
}
from
'react-native'
;
import
Tile
from
'./tile'
;
import
{
TILES
}
from
'../constants'
;
import
{
styles
}
from
'../styles'
;
function
computePassword
(
tiles
)
{
let
password
=
[]
Object
.
keys
(
tiles
).
forEach
(
(
key
)
=>
{
const
tile
=
tiles
[
key
];
if
(
tile
.
isActive
)
{
password
[
tile
.
index
]
=
tile
.
value
;
}
});
// chop off the 0
return
password
.
slice
(
1
).
join
(
'-'
);
}
export
default
class
TileMap
extends
Component
<
{}
>
{
constructor
(
props
)
{
super
(
props
);
this
.
state
=
{
tiles
:
{...
TILES
},
index
:
0
}
}
reset
=
()
=>
{
this
.
setState
(
{
tiles
:
{...
TILES
},
index
:
0
});
this
.
props
.
onTileChanged
(
computePassword
(
this
.
state
.
tiles
));
}
setPassword
=
()
=>
{
this
.
props
.
setPasswordAndLogin
(
this
.
state
.
tiles
);
}
tilePressed
=
(
id
)
=>
{
if
(
this
.
state
.
tiles
[
id
].
isActive
)
{
return
;
}
this
.
setState
((
prevState
)
=>
{
const
tiles
=
prevState
.
tiles
;
const
newIndex
=
prevState
.
index
+
1
;
const
currentTile
=
tiles
[
id
];
tiles
[
id
]
=
{
...
currentTile
,
index
:
newIndex
,
text
:
`(
${
newIndex
}
) -
${
currentTile
.
text
}
`
,
isActive
:
true
}
return
{...
tiles
,
index
:
newIndex
}
});
this
.
props
.
onTileChanged
(
computePassword
(
this
.
state
.
tiles
));
}
render
()
{
return
<
View
>
<
View
style
=
{
styles
.
tileRow
}
>
{
Object
.
keys
(
this
.
state
.
tiles
).
map
(
(
key
)
=>
{
const
tile
=
this
.
state
.
tiles
[
key
];
return
<
Tile
{...
tile
}
id
=
{
key
}
key
=
{
key
}
onPress
=
{
this
.
tilePressed
}
/>
}
)}
<
/View>
<
View
style
=
{
styles
.
buttonGroup
}
>
<
TouchableHighlight
style
=
{
styles
.
button
}
onPress
=
{
this
.
reset
}
>
<
Text
style
=
{
styles
.
buttonText
}
>
Reset
<
/Text>
<
/TouchableHighlight>
{
this
.
props
.
children
}
<
/View>
<
/View>
}
}
Now that we have all the components and their Redux dependencies, we can look at the screens
that trigger state changes. These screens are considered presentational components, meaning
that they trigger actions and are accepting props from the store. These components
are imported from <AppContainer />
.
The first screen the user sees is the <SetPassword />
screen. Notice that the <TileMap />
is
used and the this.state.password
value is sent as a message to the setPasswordAndLogin()
action creator:
// src/setPassword.js
import
React
,
{
Component
}
from
'react'
;
import
{
View
,
TouchableHighlight
,
Text
}
from
'react-native'
import
ActionCreators
from
'./actions'
import
{
bindActionCreators
}
from
'redux'
import
{
connect
}
from
'react-redux'
import
TileMap
from
'./components/tileMap'
import
{
styles
}
from
'./styles'
class
SetPassword
extends
Component
<
{}
>
{
constructor
(
props
)
{
super
(
props
);
this
.
state
=
{
password
:
null
}
}
onTileChanged
=
(
password
)
=>
{
this
.
setState
(
{
password
});
}
setPassword
=
()
=>
{
this
.
props
.
setPasswordAndLogin
(
this
.
state
.
password
);
}
render
()
{
return
<
View
>
<
Text
style
=
{
styles
.
title
}
>
Set
Password
<
/Text>
<
TileMap
onTileChanged
=
{
this
.
onTileChanged
}
>
<
TouchableHighlight
style
=
{
styles
.
button
}
onPress
=
{
this
.
setPassword
}
>
<
Text
style
=
{
styles
.
buttonText
}
>
Set
Password
and
Login
<
/Text>
<
/TouchableHighlight>
<
/TileMap>
<
/View>
}
}
export
default
connect
(
({
user
})
=>
{
return
{
user
}
},
(
dispatch
)
=>
bindActionCreators
(
ActionCreators
,
dispatch
)
)(
SetPassword
);
When a user isLoggedIn
, the <MyHome />
component is rendered. This may appear to be a
contrived example, but it demonstrates the difference between local and global state. The
user
reducer is maintaining the secret
, but only after setSecret()
is called, triggering
a state transformation in the user reducer
. Notice that the component does not know what
logout()
does; it merely sends the message and relies on the appState
reducer:
// src/myHome.js
import
React
,
{
Component
}
from
'react'
;
import
{
TextInput
,
TouchableHighlight
,
View
,
Text
}
from
'react-native'
;
import
{
bindActionCreators
}
from
'redux'
;
import
{
connect
}
from
'react-redux'
;
import
Tile
from
'./components/tile'
;
import
{
TILES
}
from
'./constants'
;
import
{
styles
}
from
'./styles'
;
class
MyHome
extends
Component
<
{}
>
{
constructor
(
props
)
{
super
(
props
);
this
.
state
=
{
secret
:
props
.
user
.
secret
||
''
}
}
saveSecret
=
()
=>
{
this
.
props
.
setSecret
(
this
.
state
.
secret
);
}
logout
=
()
=>
{
this
.
props
.
logout
();
}
render
()
{
return
<
View
>
<
Text
>
Enter
Your
Secret
:<
/Text>
<
TextInput
value
=
{
this
.
state
.
secret
}
style
=
{{
borderWidth
:
1
,
borderColor
:
"#CCC"
,
padding
:
5
,
}}
onChangeText
=
{(
secret
)
=>
{
this
.
setState
({
secret
})
}}
/>
<
TouchableHighlight
style
=
{
styles
.
button
}
onPress
=
{
this
.
saveSecret
}
>
<
Text
style
=
{
styles
.
buttonText
}
>
Save
<
/Text>
<
/TouchableHighlight>
<
TouchableHighlight
style
=
{
styles
.
button
}
onPress
=
{
this
.
logout
}
>
<
Text
style
=
{
styles
.
buttonText
}
>
Logout
<
/Text>
<
/TouchableHighlight>
<
/View>
}
}
export
default
connect
(
({
user
})
=>
({
user
}),
(
dispatch
)
=>
bindActionCreators
(
ActionCreators
,
dispatch
)
)(
MyHome
);
The <LoginForm />
component is almost identical to the <SetPassword />
component in structure,
but it maps components to a different set of action creators for handling account reset and
user login. This is an example of repurposing the <TileMap />
component for a completely different
use case:
import
React
,
{
Component
}
from
'react'
;
import
{
View
,
TouchableHighlight
,
Text
}
from
'react-native'
;
import
ActionCreators
from
'./actions'
;
import
{
bindActionCreators
}
from
'redux'
;
import
{
connect
}
from
'react-redux'
;
import
TileMap
from
'./components/tileMap'
;
import
{
styles
}
from
'./styles'
;
class
LoginForm
extends
Component
<
{}
>
{
constructor
(
props
)
{
super
(
props
);
this
.
state
=
{
password
:
null
}
}
onTileChanged
=
(
password
)
=>
{
this
.
setState
(
{
password
});
}
resetAccount
=
()
=>
{
this
.
props
.
reset
();
}
login
=
()
=>
{
this
.
props
.
attemptLogin
(
this
.
state
.
password
);
}
render
()
{
return
<
View
>
<
Text
style
=
{
styles
.
title
}
>
Login
<
/Text>
<
TileMap
onTileChanged
=
{
this
.
onTileChanged
}
>
<
TouchableHighlight
style
=
{
styles
.
button
}
onPress
=
{
this
.
login
}
>
<
Text
style
=
{
styles
.
buttonText
}
>
Login
<
/Text>
<
/TouchableHighlight>
<
TouchableHighlight
style
=
{
styles
.
button
}
onPress
=
{
this
.
resetAccount
}
>
<
Text
style
=
{
styles
.
buttonText
}
>
Reset
Account
<
/Text>
<
/TouchableHighlight>
<
/TileMap>
<
/View>
}
}
export
default
connect
(
({
user
})
=>
({
user
}),
(
dispatch
)
=>
bindActionCreators
(
ActionCreators
,
dispatch
)
)(
LoginForm
);
Redux can be intimidating if you are new to JavaScript. This is because the library is simple, but not simplistic: the programming concepts are profound and require some experience to grasp, but there are few of them and they elegantly support one another. It’s helpful to think of Redux as a software design pattern and a JavaScript library at the same time. Adopting one without the other will leave a sour taste in your mouth.
Even if you decide to use another state management library, you will probably
face a library, like react-navigation
, with Redux under the hood. Understanding
the programmer attitudes around mutable state, pure functions, composition, and higher order
functions will bring state management in the React ecosystem into focus.
I would not be able to do justice to the fantastic Redux documentation and the incredible wealth of free video tutorials (including some of my own on YouTube). However, there are three principles worth keeping in mind as we implement Redux in our app:
Single source of truth: The state of your whole application is stored in an object tree within a single store. … State is read-only: The only way to change the state is to emit an action, an object describing what happened. … Changes are made with pure functions: To specify how the state tree is transformed by actions, you write pure reducers.
redux.js.org, Three Principles
18.119.172.61