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:

  • Using hooks from react-redux to access state in components
  • using useSelector
  • using useDispatch

Requirements

  • Nodejs version >= 10.x.x installed
  • watchman installed
  • have access to one package manager such as npm or yarn
  • use react native version 0.60.x or above

Getting started with the Crowdbotics App Builder

To 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.

Crowdbotics App Builder dashboard

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.

App type selection screen in Crowdbotics

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.

Default React Native app generated by Crowdbotics

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.

Installing dependencies

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

Creating two mock screens

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

Set up a Stack Navigator

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.

Mobile app displaying "BookScreen"

Create a custom icon

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.

The BookScreen component now has a shopping cart icon in the top right corner

But it doesn't perform its function, which is to navigate from BookScreen to CartScreen component. Let us add that in the next section.

Access the navigation prop from any component

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.

The app switches to the CartScreen component

Creating a root reducer

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

Configuring a redux store

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.

Add mock data

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.

Display the mock data

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.

A mobile app scrolls through a list of book items

Add a badge to the cart icon in the header

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:

A red badge with the number 0 hovers above the cart icon

Use useDispatch to trigger an action

In 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.

The badge updates the number it displays with each item added to the cart

Updating the cart

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:

  • a message when there are no items in the cart or when the cart is empty
  • each item in the cart when user adds an item

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.

The cart correctly renders its contents in real time

Conclusion

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.