How to Use Redux Hooks in a React Native App
Learn how to use Redux Hooks in a simple shopping cart app created with React Native and Crowdbotics.
6 October 2021
The react-redux
library now has support for Hooks in React and React Native apps that make use of Redux as the state management library. With React Hooks’ growing usage, the ability to handle a component’s state and side effects is now a common pattern in a functional component. React Redux offers a set of Hook APIs as an alternative to the omnipresent connect()
Higher-Order Component.
In this post, let’s explore how to build a React Native app that uses Redux to manage app level state. We will cover:
react-redux
to access state in componentsuseSelector
useDispatch
10.x.x
installed0.60.x
or aboveTo generate a new React Native project you can use the react-native cli tool. Or, if you want to follow along, I am going to generate a new app using the Crowdbotics app building platform.
Register either using your GitHub credentials or your email. Once logged in, you can click the Create App
button to create a new app. The next screen is going to prompt you as to what type of application you want to build. Choose Mobile App
.
Enter the name of the application and click the button Create App
. After you link your GitHub account from the dashboard, you are going to have access to the GitHub repository for the app. This repo generated uses the latest react-native
version and comes with built-in components and complete examples that can be the base foundation for your next app.
You can now clone or download the GitHub repo that is generated by the Crowdbotics App Builder. Once you have access to the repo on your local development environment, make sure to navigate inside it. You will have to install the dependencies for the first time using the command yarn install
. Then, to make it work on the iOS simulator/devices, make sure to install pods using the following commands from a terminal window.
# navigate inside iOS
cd ios/
# install pods
pod install
That’s it. It’s an easy process. Now, let us get back to our tutorial.
To start, let us install the dependencies that are required in order to build this app. The app is going to contain two screens. The first is a home screen showing a list of items. From this first screen, the user can choose to add a number of items in the cart. The second screen is going display the items user adds inside the cart. This demo is a minimal shopping cart app that we’re creating to understand the concepts.
To navigate between these two screens, a navigation pattern is required. You can use a Stack Navigation pattern for this use case. Open the terminal window and install the react-navigation
library and its peer dependencies. Since react-navigation
library released its 5th version, I am going to use that.
At this point, also install redux and react-redux
library as well.
yarn add @react-navigation/native @react-navigation/stack react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view redux react-redux
In the previous section, we discussed that the app is going to have two screens. Create a new directory called src/screens
and then create two new files:
BooksScreen.js
CartScreen.js
Each of the screen files is going to have some random data to display until the stack navigator is set up.
// BooksScreen.js
import React from 'react'
import { View, Text } from 'react-native'
function BookScreen() {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>BookScreen</Text>
</View>
)
}
export default BookScreen
// CartScreen.js
import React from 'react'
import { View, Text } from 'react-native'
function CartScreen() {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Cart Screen</Text>
</View>
)
}
export default CartScreen
Create a new file called AppNavigator.js
inside the src/navigation
directory. This file is going to contain all the configuration to create and set up a Stack Navigator.
Since the release of react-navigation
version 5, the configuration process has changed. Some of the highlights, which the team of maintainers enumerated in a blog post, are that the navigation patterns are now more component-based, common use cases can now be handled with pre-defined Hooks, a new architecture allows you to configure and update a screen from the component itself, and a few other changes, as well.
The major highlight of these new changes is the component-based configuration. If you have experience developing with web-based libraries such as ReactJS in combination with react-router, you won’t experience much of a learning curve here.
Back to the app. A Stack navigator is a way to provide app transition between screens and manage navigation history. This is exactly what you are going to use it for.
Start by importing NavigationContainer
, createStackNavigator
and the two screens you created in the previous section.
import * as React from 'react'
import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'
import BookScreen from '../screens/BooksScreen'
import CartScreen from '../screens/CartScreen'
The NavigationContainer
is a component that manages the navigation tree. It also contains the navigation
state and has to wrap all the navigator’s structure.
The createStackNavigator
is a function that implements a stack navigation pattern. This function returns two React components: Screen
and Navigator
, which allows you to configure each component screen.
const Stack = createStackNavigator()
function MainStackNavigator() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name='Books' component={BookScreen} />
<Stack.Screen name='Cart' component={CartScreen} />
</Stack.Navigator>
</NavigationContainer>
)
}
export default MainStackNavigator
In the above snippet, there are two required props with each Stack.Screen
. The prop name
refers to the name of the route, and the prop component
specifies which screen to render at the particular route.
Don’t forget to export the MainStackNavigator
since it’s going to be imported in the root of the app. Open App.js
and modify it as shown below:
import React from 'react'
import MainStackNavigator from './src/navigation/AppNavigator'
export default function App() {
return <MainStackNavigator />
}
Run the React Native app in a simulator or a real device and you are going to see the following result. The BookScreen
component is going to be displayed.
In this section, let us create a cart icon that is going to be displayed in the header bar of the BookScreen
component. This icon button is going to transport the user from BookScreen
to the CartScreen
component.
Create a new file called ShoppingCartIcon.js
inside src/components
directory.
To create a touchable button, import TouchableOpacity
as well as the Ionicons
package from @expo/vector-icons
. This function is going to be a simple one.
import React from 'react'
import { TouchableOpacity } from 'react-native'
import { Ionicons } from '@expo/vector-icons'
function ShoppingCartIcon(props) {
return (
<TouchableOpacity
onPress={() => alert('Press me')}
style={{ marginRight: 10 }}>
<Ionicons name='ios-cart' size={32} color='#101010' />
</TouchableOpacity>
)
}
export default ShoppingCartIcon
To display it on the right side of the header of BookScreen
, let’s use the headerRight
option. Open src/navigation/AppNavigator.js
and add the prop options
at Stack.Screen
for BookScreen
.
First, import the component inside the navigation config file.
import ShoppingCartIcon from '../components/ShoppingCartIcon'
The prop options
accepts a JavaScript object at its value.
<Stack.Screen
name='Books'
component={BookScreen}
options={{ headerRight: props => <ShoppingCartIcon {...props} /> }}
/>
Passing props
as the only parameter to the ShoppingCartIcon
custom component will allow you to access navigator props in case you need them later.
Going back to the simulator, you are going to see the icon in right side of the header.
But it doesn’t perform its function, which is to navigate from BookScreen
to CartScreen
component. Let us add that in the next section.
The react-navigation
library provides a pre-defined hook called useNavigation
that can be utilized inside a React Native component that is not part of the Navigation structure.
In our case, the custom ShoppingCartIcon
component is not a screen, thus, it is not part of the Stack Navigator architecture. However, this touchable button’s only functionality for now is to navigate between two screens. Utilizing useNavigation
, you can add this navigation.
Inside the file src/components/ShoppingCartIcon.js
, after other statements, import the following statement:
import { useNavigation } from '@react-navigation/native'
Then, use useNavigation
to provide the navigation
prop automatically inside the functional component ShoppingCartIcon
.
function ShoppingCartIcon() {
const navigation = useNavigation()
// ...
}
Lastly, on the TouchableOpacity
prop onPress
, you can use navigation.navigate
and pass on the name
of the screen to navigate to as shown below:
<TouchableOpacity
onPress={() => navigation.navigate('Cart')}
style={{ marginRight: 10 }}>
<Ionicons name='ios-cart' size={32} color='#101010' />
</TouchableOpacity>
Now, go back to the simulator and click the icon.
Create a new directory called src/redux/
and inside it a new file called CartItem.js
. This file is going to have the definition of action types and the only reducer we are going to create in this app.
When using Redux to manage the state of the whole application, the state itself is represented by one JavaScript object. Think of this object as read-only, since you cannot make changes to this state (which is represented in the form of a tree) directly. It requires actions to do so.
Actions are like events in Redux. They can be triggered by button presses, timers, or network requests.
Start by defining two action types as following:
export const ADD_TO_CART = 'ADD_TO_CART'
export const REMOVE_FROM_CART = 'REMOVE_FROM_CART'
Then, define an initial state which is going to be an empty array as well as cartItemReducer
. Whenever an action is triggered, the state of the application changes. The handling of the application’s state is done by the reducers.
There are going to be two actions:
ADD_TO_CART
is going to be triggered whenever the user adds a new item to the cart from a list of books.REMOVE_FROM_CART
is going to be triggered whenever the user removes a book item from the cart.const initialState = []
const cartItemsReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TO_CART:
return [...state, action.payload]
case REMOVE_FROM_CART:
return state.filter(cartItem => cartItem.id !== action.payload.id)
}
return state
}
export default cartItemsReducer
A store is an object that brings actions and reducers together. It provides and holds state at the application level instead of individual components. Redux is not an opinionated library in terms of which framework or library should use it or not.
With the creation of reducer done, create a new file called store.js
inside src/redux/
. Import the function createStore
from redux
as well as the only reducer in the app for now.
import { createStore } from 'redux'
import cartItemsReducer from './CartItems'
const store = createStore(cartItemsReducer)
export default store
To bind this Redux store in the React Native app, open the entry point file App.js
and import the store
as well as the Higher-Order Component Provider
from the react-redux
npm package. This HOC helps you to pass the store down to the rest of the components of the current app.
import React from 'react'
import MainStackNavigator from './src/navigation/AppNavigator'
import { Provider as StoreProvider } from 'react-redux'
import store from './src/redux/store'
export default function App() {
return (
<StoreProvider store={store}>
<MainStackNavigator />
</StoreProvider>
)
}
That’s it! The Redux store is now configured and ready to use.
Create a new file called Data.js
inside src/utils/
with the following contents.
export const books = [
{
id: 1,
name: 'The Book Thief',
author: 'Markus Zusak',
imgUrl:
'https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1522157426l/19063._SY475_.jpg'
},
{
id: 2,
name: 'Sapiens',
author: 'Yuval Noah Harari',
imgUrl:
'https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1420585954l/23692271.jpg'
},
{
id: 3,
name: 'Crime and Punishment',
author: 'Fyodor Dostoyevsky',
imgUrl:
'https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1382846449l/7144.jpg'
},
{
id: 4,
name: 'No Longer Human',
author: 'Osamu Dazai',
imgUrl:
'https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1422638843l/194746.jpg'
},
{
id: 5,
name: 'Atomic Habits',
author: 'James Clear',
imgUrl:
'https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1535115320l/40121378._SY475_.jpg'
},
{
id: 7,
name: 'Dune',
author: 'Frank Herbert',
imgUrl:
'https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1434908555l/234225._SY475_.jpg'
},
{
id: 8,
name: 'Atlas Shrugged',
author: 'Ayn Rand',
imgUrl:
'https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1405868167l/662.jpg'
}
]
This file contains an array of objects that have unique properties, and each object represents one book item.
You are going to use this data file inside the BookScreen
component to display them. From this list of data, the user is going to choose a book item to add to the cart.
The data is going to be displayed as a list of items inside the BookScreen.js
screen component. The list of items is going to be used as the single source of truth inside a FlatList
component which, in return, is going to render each item from the data array. Start by importing the necessary components from react-native
core as well as the array books
from utils/data.js
.
import React from 'react'
import {
View,
Text,
FlatList,
Image,
TouchableOpacity,
StyleSheet
} from 'react-native'
import { books } from '../utils/Data'
Next, create another functional component called Separator
. This component is going to separate two items in the list.
function Separator() {
return <View style={{ borderBottomWidth: 1, borderBottomColor: '#a9a9a9' }} />
}
Here is the complete snippet for the BookScreen
component:
function BookScreen() {
return (
<View style={styles.container}>
<FlatList
data={books}
keyExtractor={item => item.id.toString()}
ItemSeparatorComponent={() => Separator()}
renderItem={({ item }) => (
<View style={styles.bookItemContainer}>
<Image source={{ uri: item.imgUrl }} style={styles.thumbnail} />
<View style={styles.bookItemMetaContainer}>
<Text style={styles.textTitle} numberOfLines={1}>
{item.name}
</Text>
<Text style={styles.textAuthor}>by {item.author}</Text>
<View style={styles.buttonContainer}>
<TouchableOpacity
onPress={() => alert('Add to cart')}
style={styles.button}>
<Text style={styles.buttonText}>Add +</Text>
</TouchableOpacity>
</View>
</View>
</View>
)}
/>
</View>
)
}
Last, add the corresponding styles:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff'
},
bookItemContainer: {
flexDirection: 'row',
padding: 10
},
thumbnail: {
width: 100,
height: 150
},
bookItemMetaContainer: {
padding: 5,
paddingLeft: 10
},
textTitle: {
fontSize: 22,
fontWeight: '400'
},
textAuthor: {
fontSize: 18,
fontWeight: '200'
},
buttonContainer: {
position: 'absolute',
top: 110,
left: 10
},
button: {
borderRadius: 8,
backgroundColor: '#24a0ed',
padding: 5
},
buttonText: {
fontSize: 22,
color: '#fff'
}
})
export default BookScreen
Once you have the component file set up, go back to the simulator. You are going to get a list of book items displayed as below.
To notify the user that the items in the cart are being updated when browsing the list of items from BookScreen
component, let us add a badge-like notification next to the shopping cart icon.
This badge is going to display the number of items that are in the cart at any given point. If the cart is empty, it is going to display the number 0
.
To check the number of items in the current cart, you are going to use useSelector
from react-redux
library. This hook is similar to mapStateToProps
argument that is passed inside the connect()
in previous versions of react-redux
. It allows you to extract data from the Redux store state using a selector function.
The major difference between the hook and the argument is that the selector may return any value as a result, not just an object.
Open src/components/ShoppingCartIcon.js
and modify it as below:
import React from 'react'
import { TouchableOpacity, View, Text, StyleSheet } from 'react-native'
import { Ionicons } from '@expo/vector-icons'
import { useNavigation } from '@react-navigation/native'
import { useSelector } from 'react-redux'
function ShoppingCartIcon() {
const navigation = useNavigation()
const cartItems = useSelector(state => state)
return (
<TouchableOpacity
onPress={() => navigation.navigate('Cart')}
style={styles.button}>
<View style={styles.itemCountContainer}>
<Text style={styles.itemCountText}>{cartItems.length}</Text>
</View>
<Ionicons name='ios-cart' size={32} color='#101010' />
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
button: {
marginRight: 10
},
itemCountContainer: {
position: 'absolute',
height: 30,
width: 30,
borderRadius: 15,
backgroundColor: '#FF7D7D',
right: 22,
bottom: 10,
alignItems: 'center',
justifyContent: 'center',
zIndex: 2000
},
itemCountText: {
color: 'white',
fontWeight: 'bold'
}
})
export default ShoppingCartIcon
In the above snippet, cartItems
is the state that holds how many items are in the cart. As of now, there no items in the cart so it is going to be displayed as below:
useDispatch
to trigger an actionIn this section, let’s update the BookScreen
component to let the user add the item to the cart and update the badge in the header.
To start, import the action creator ADD_TO_CART
from the reducer file redux/CartItem.js
and the hook useDispatch
from react-redux
.
The useDispatch()
hook completely refers to the dispatch function from the Redux store. This hook is used only when there is a need to dispatch an action.
The advantage that the useDispatch()
hook provides is that it replaces mapDispatchToProps
and there is no need to write boilerplate code to bind action creators with this hook now.
Add the following inside the BookScreen
component:
const dispatch = useDispatch()
const addItemToCart = item => dispatch({ type: ADD_TO_CART, payload: item })
Next, update the onPress
prop of TouchableOpacity
by passing the helper method addItemToCart
. This will actually update the cart.
<TouchableOpacity onPress={() => addItemToCart(item)} style={styles.button}>
<Text style={styles.buttonText}>Add +</Text>
</TouchableOpacity>
Now, go back to the simulator and try adding a few items to the cart. The badge will update.
Even though the cart is getting updated (since the initial state of the Redux store is being updated whenever a user adds an item), there is no way to show these items on the actual CartScreen
.
Let’s modify the CartScreen.js
file to display:
Let’s also add the ability to remove an item from the cart.
To begin, import the following statements first. Apart from React Native core components, both hooks from react-redux
as well as the action REMOVE_FROM_CART
are going to be imported.
import React from 'react'
import {
View,
Text,
TouchableOpacity,
FlatList,
Image,
StyleSheet
} from 'react-native'
import { useSelector, useDispatch } from 'react-redux'
import { REMOVE_FROM_CART } from '../redux/CartItems'
Next, create a Separator
function similar to the one we made in the BookScreen
component.
function Separator() {
return <View style={{ borderBottomWidth: 1, borderBottomColor: '#a9a9a9' }} />
}
Inside the CartScreen
component, using the useSelector
hook, fetch the current state. Also, create a helper method called removeItemFromCart
that accepts a book item as its parameter. This method is going to trigger the action type to remove an item from the cart.
On the basis of the size of the cart, you can display the message when the cart is empty, and when it is not, render the list of items it contains.
Here is the complete snippet of the CartScreen
component along with the corresponding styles.
function CartScreen() {
const cartItems = useSelector(state => state)
const dispatch = useDispatch()
const removeItemFromCart = item =>
dispatch({
type: REMOVE_FROM_CART,
payload: item
})
return (
<View
style={{
flex: 1
}}>
{cartItems.length !== 0 ? (
<FlatList
data={cartItems}
keyExtractor={item => item.id.toString()}
ItemSeparatorComponent={() => Separator()}
renderItem={({ item }) => (
<View style={styles.bookItemContainer}>
<Image source={{ uri: item.imgUrl }} style={styles.thumbnail} />
<View style={styles.bookItemMetaContainer}>
<Text style={styles.textTitle} numberOfLines={1}>
{item.name}
</Text>
<Text style={styles.textAuthor}>by {item.author}</Text>
<View style={styles.buttonContainer}>
<TouchableOpacity
onPress={() => removeItemFromCart(item)}
style={styles.button}>
<Text style={styles.buttonText}>Remove -</Text>
</TouchableOpacity>
</View>
</View>
</View>
)}
/>
) : (
<View style={styles.emptyCartContainer}>
<Text style={styles.emptyCartMessage}>Your cart is empty :'(</Text>
</View>
)}
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff'
},
bookItemContainer: {
flexDirection: 'row',
padding: 10
},
thumbnail: {
width: 100,
height: 150
},
bookItemMetaContainer: {
padding: 5,
paddingLeft: 10
},
textTitle: {
fontSize: 22,
fontWeight: '400'
},
textAuthor: {
fontSize: 18,
fontWeight: '200'
},
buttonContainer: {
position: 'absolute',
top: 110,
left: 10
},
button: {
borderRadius: 8,
backgroundColor: '#ff333390',
padding: 5
},
buttonText: {
fontSize: 22,
color: '#fff'
},
emptyCartContainer: {
marginTop: 250,
justifyContent: 'center',
alignItems: 'center'
},
emptyCartMessage: {
fontSize: 28
}
})
export default CartScreen
There is nothing new here. It is quite similar to the BookScreen
component when rendering a list of items using FlatList
.
Last, go back to the simulator, add some items to the cart, and go the cart screen to see them.
Using Redux hooks offers advantages over previous syntax related to connect()
. Hooks make component code files less verbose and allow you to use side effects in functional components. For more information, you can check out the official documentation for Redux hooks here.