How to use redux-persist in React Native with Asyncstorage

Published on Jan 8, 2021

19 min read

EXPO

Redux persist is a library that allows saving a Redux store in the local storage of an application. In React Native terms, Asyncstorage is a key value-based, unencrypted, asynchronous storage system that is global and can be used as the local storage for the app.

Using a state management library like Redux in a React Native app is beneficial to manage the state of an application from one place. As your application advances in terms of features, you may want to persist some of the information for each user that is local to them.

For example, you are building a shopping cart application and it requires persisting the data related to products a user is adding to the cart before making a purchase order. What if the user closes the application for an arbitrary reason before making that purchase but comes back later and finds that number of items to vanish completely from their cart. This is not a good user experience.

To improve this user experience, you could save the items in their device's local storage. This is where redux-persist along with Asyncstorage comes in handy for a React Native app. In this tutorial, we are going to set up the redux-persist library in a React Native app that uses Redux as its state management library and preserve the data in Asyncstorage for scenarios where the app is closed.

The source code is available at this GitHub repo.

Prerequisites

🔗

To follow this tutorial, please make sure you are familiarized with JavaScript/ES6 and meet the following requirements in your local dev environment:

  • Node.js version >= 12.x.x installed.
  • Have access to one package manager such as npm or yarn or npx.
  • Have a basic understanding of Redux store, actions, and reducers.
  • expo-cli installed, or use npx

Create a React Native app with expo-cli

🔗

Create a new React Native project using expo-cli and then install the dependencies required to build this demo app. Open a terminal window and execute the following commands:

npx expo init redux-persist-asyncstorage-example
# navigate into that directory
cd redux-persist-asyncstorage-example
yarn add @react-navigation/native @react-navigation/bottom-tabs axios@0.21.0
redux@4.0.5 redux-persist@6.0.0 redux-thunk@2.3.0 react-redux@7.2.2
# install dependencies with Expo specific package version
expo install react-native-gesture-handler react-native-reanimated
react-native-screens react-native-safe-area-context @react-native-community/
masked-view @react-native-async-storage/async-storage

After installing these dependencies, let's create two mock screens that are going to be the core screens for the demo app. Create a new screens/ directory and inside it, create the first screen file BooksList.js with the following code snippet:

1import React from 'react';
2import { StyleSheet, Text, View } from 'react-native';
3
4export default function BooksListApp() {
5 return (
6 <View style={styles.container}>
7 <Text>BooksList</Text>
8 </View>
9 );
10}
11
12const styles = StyleSheet.create({
13 container: {
14 flex: 1,
15 backgroundColor: '#fff',
16 alignItems: 'center',
17 justifyContent: 'center'
18 }
19});

Then create the second screen file BookmarksList.js with the following code snippet:

1import React from 'react';
2import { StyleSheet, Text, View } from 'react-native';
3
4export default function BookmarksList() {
5 return (
6 <View style={styles.container}>
7 <Text>BookmarksList</Text>
8 </View>
9 );
10}
11
12const styles = StyleSheet.create({
13 container: {
14 flex: 1,
15 backgroundColor: '#fff',
16 alignItems: 'center',
17 justifyContent: 'center'
18 }
19});

The BooksList screen is going to show a list of books. I am going to fetch the data to display the books and will be using Draftbit's Example API route as the base URL.

Each book item shown on this screen is going to have a functionality for the end-user to bookmark or save it in real-time to view later. All the book items saved by the user are going to be shown in the BookmarksList tab.

Since a Base URL is required to fetch the data, let's add it. Create a new directory called config/ and inside it create a file called index.js and export the following Base URL:

1export const BASE_URL = 'https://example-data.draftbit.com/books?_limit=10';

Now, this Base URL is ready to use to send HTTP requests.

Add tab navigation to switch between the screens

🔗

In this section, let's create a custom tab navigator at the bottom for the app to display the two mock screens created in the previous section. Start by creating a navigation/ directory and inside a new file called RootNavigator.js. Add the following import statements in this file:

1import React from 'react';
2import { NavigationContainer } from '@react-navigation/native';
3import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
4import { MaterialCommunityIcons } from '@expo/vector-icons';
5
6// Import mock screens
7import BooksList from '../screens/BooksList';
8import BookmarksList from '../screens/BookmarksList';
9
10const Tab = createBottomTabNavigator();

To customize the tab bar appearance, let's add some styling and custom icons from the @expo/vector-icons library which comes pre-installed with the expo package.

1const tabBarOptions = {
2 showLabel: false,
3 inactiveTintColor: '#2D3038',
4 activeTintColor: '#FFFFFF',
5 style: {
6 height: '10%',
7 backgroundColor: '#1E1B26'
8 }
9};
10
11const screenOptions = (route, color) => {
12 let iconName;
13
14 switch (route.name) {
15 case 'BooksList':
16 iconName = 'view-dashboard';
17 break;
18 case 'BookmarksList':
19 iconName = 'bookmark-multiple-outline';
20 break;
21 default:
22 break;
23 }
24
25 return <MaterialCommunityIcons name={iconName} color={color} size={24} />;
26};

The tabBarOptions config object is going to customize the appearance of the bottom tab shared between different app screens. The screenOptions are used to add a custom icon for each tab.

Lastly, let's define and export the RootNavigator component that is going to render these two tab screens.

1const RootNavigator = () => {
2 return (
3 <NavigationContainer>
4 <Tab.Navigator
5 initialRouteName="BooksList"
6 tabBarOptions={tabBarOptions}
7 screenOptions={({ route }) => ({
8 tabBarIcon: ({ color }) => screenOptions(route, color)
9 })}
10 >
11 <Tab.Screen name="BooksList" component={BooksList} />
12 <Tab.Screen name="BookmarksList" component={BookmarksList} />
13 </Tab.Navigator>
14 </NavigationContainer>
15 );
16};
17
18export default RootNavigator;

To see the RootNavigator in action, import it inside the App.js file and return it. Add the following code snippet to the App.js file:

1import React from 'react';
2
3import RootNavigator from './navigation/RootNavigator';
4
5export default function App() {
6 return <RootNavigator />;
7}

To run the application, execute the command yarn start from the terminal window.

Here is the output after this step:

ss1

Add action types and creators

🔗

Using Redux to manage the state of the whole application, the state itself is represented by one JavaScript object. This object is read-only which means that manipulation of the state is not done directly. Changes are done by triggering actions.

Let us begin by defining action types. Create a new directory called redux/ and inside it create a new file called actions.js. Add the following action types to it:

1// Define action types
2export const GET_BOOKS = 'GET_BOOKS';
3export const ADD_TO_BOOKMARK_LIST = 'ADD_TO_BOOKMARK_LIST';
4export const REMOVE_FROM_BOOKMARK_LIST = 'REMOVE_FROM_BOOKMARK_LIST';

Action types defined in the above file are self-explanatory. The first one, GET_BOOKS, is going to be used to make the HTTP request to fetch the data from the Base URL. The second, ADD_TO_BOOKMARK_LIST, is going to add each book item to the list of bookmarks. Similarly, the third action type REMOVE_FROM_BOOKMARK_LIST is going to remove the book from the list of bookmarks.

An action type is used to trigger the event to update the state stored using Redux. Each action type has action creators for this purpose. The first action creator required in the demo app is to fetch the data from the Draftbit's Example API.

To fetch data, we will use a library called axios. It has an API of methods such as .get, .put, etc. to make the appropriate HTTP requests.

To make the HTTP request to retrieve the data, a BASE URL of the API is required. Inside the actions.js file, import the axios library and the Base URL:

1import axios from 'axios';
2
3import { BASE_URL } from '../config';

After defining the action types, define a new action creator called getBooks that has the action type of GET_BOOKS with the following code snippet:

1export const getBooks = () => {
2 try {
3 return async dispatch => {
4 const response = await axios.get(`${BASE_URL}`);
5 if (response.data) {
6 dispatch({
7 type: GET_BOOKS,
8 payload: response.data
9 });
10 } else {
11 console.log('Unable to fetch data from the API BASE URL!');
12 }
13 };
14 } catch (error) {
15 // Add custom logic to handle errors
16 console.log(error);
17 }
18};

Add a reducer

🔗

Whenever an action is triggered, the state of the application changes. The handling of the application’s state is done by a reducer.

A reducer is a pure function that calculates the next state based on the initial or previous state. It always produces the same output if the state is unchanged. It takes two inputs—the state and action—and must return the default state.

Create a new file in the redux/ directory called reducers.js. Import the action type GET_BOOKS and then define the initial state with two empty arrays. Then define a booksReducer function that takes initialState as the default value for the first argument, and action as the second argument.

1import { GET_BOOKS } from './actions';
2
3const initialState = {
4 books: [],
5 bookmarks: []
6};
7
8function booksReducer(state = initialState, action) {
9 switch (action.type) {
10 case GET_BOOKS:
11 return { ...state, books: action.payload };
12 default:
13 return state;
14 }
15}
16
17export default booksReducer;

Configure a 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.

Create a new file called store.js inside the redux/ directory. A store in redux is created using a function called createStore that takes the rootReducer as the first argument and middleware or a collection of middleware functions as the second argument.

The rootReducer is a combination of different reducers across the app. In the demo app, there is only one reducer called booksReducer.

The middleware function thunk allows a redux store to make asynchronous AJAX requests such as fetching data from an API URL like in this demo app.

Add the following code snippet to it:

1import { createStore, combineReducers, applyMiddleware } from 'redux';
2import thunk from 'redux-thunk';
3
4import booksReducer from './reducers';
5
6const rootReducer = combineReducers({ booksReducer });
7
8export const store = createStore(rootReducer, applyMiddleware(thunk));

To bind this Redux store in the React Native app, open the entry point file App.js. Inside it, import the store and the High Order Component Provider from the react-redux package. This HOC helps to pass the store down to the rest of the app such as all components, which are now able to access the state. It is also going to wrap the RootNavigator since all screens are children of this custom navigator.

Modify the App.js file as shown below:

1import React from 'react';
2import { Provider } from 'react-redux';
3
4import { store } from './redux/store';
5import RootNavigator from './navigation/RootNavigator';
6
7export default function App() {
8 return (
9 <Provider store={store}>
10 <RootNavigator />
11 </Provider>
12 );
13}

Fetching data from the API

🔗

The BooksList.js file is the tab where the data is going to fetch from the Base URL. Import the following statements. To access the state from a Redux store, the useSelector hook is used. Inside the BooksList component, access the books from the state.

1export default function BooksList() {
2 const { books } = useSelector(state => state.booksReducer);
3
4 //...
5}

To dispatch an action from the Redux store, the useDispatch hook is used. To fetch the books from the API, you need to dispatch the action getBooks. Add the following code snippet after accessing the state.

1const dispatch = useDispatch();
2
3const fetchBooks = () => dispatch(getBooks());
4
5useEffect(() => {
6 fetchBooks();
7}, []);

Next, add return JSX with a FlatList component to render the list of books.

The books fetched from the API is an array and is passed as the value for the data.

1return (
2 <SafeAreaView style={{ flex: 1, backgroundColor: '#1E1B26' }}>
3 <View style={{ flex: 1, paddingHorizontal: 16 }}>
4 <Text style={{ color: 'white', fontSize: 22 }}>Bestsellers</Text>
5 <View style={{ flex: 1, marginTop: 8 }}>
6 <FlatList
7 data={books}
8 keyExtractor={item => item.id.toString()}
9 renderItem={renderItem}
10 showsVerticalScrollIndicator={false}
11 />
12 </View>
13 </View>
14 </SafeAreaView>
15);

The JSX returned from the renderItem contains all the information to display for each book item in the list.

Each book item is going to have:

  • a book cover displayed using the Image component.
  • a book title displayed using the Text component.
  • some meta information such as the number of pages and the average rating of the book item.
  • the touchable button to add the book to the BookmarksList screen.

Add the following renderItem just before the main return function.

1const renderItem = ({ item }) => {
2 return (
3 <View style={{ marginVertical: 12 }}>
4 <View style={{ flexDirection: 'row', flex: 1 }}>
5 {/* Book Cover */}
6 <Image
7 source={{ uri: item.image_url }}
8 resizeMode="cover"
9 style={{ width: 100, height: 150, borderRadius: 10 }}
10 />
11 {/* Book Metadata */}
12 <View style={{ flex: 1, marginLeft: 12 }}>
13 {/* Book Title */}
14 <View>
15 <Text style={{ fontSize: 22, paddingRight: 16, color: 'white' }}>
16 {item.title}
17 </Text>
18 </View>
19 {/* Meta info */}
20 <View
21 style={{
22 flexDirection: 'row',
23 marginTop: 10,
24 alignItems: 'center'
25 }}
26 >
27 <MaterialCommunityIcons
28 color="#64676D"
29 name="book-open-page-variant"
30 size={20}
31 />
32 <Text style={{ fontSize: 14, paddingLeft: 10, color: '#64676D' }}>
33 {item.num_pages}
34 </Text>
35 <MaterialCommunityIcons
36 color="#64676D"
37 name="star"
38 size={20}
39 style={{ paddingLeft: 16 }}
40 />
41 <Text style={{ fontSize: 14, paddingLeft: 10, color: '#64676D' }}>
42 {item.rating}
43 </Text>
44 </View>
45 {/* Buttons */}
46 <View style={{ marginTop: 14 }}>
47 <TouchableOpacity
48 onPress={() => console.log('Bookmarked!')}
49 activeOpacity={0.7}
50 style={{
51 flexDirection: 'row',
52 padding: 2,
53 backgroundColor: '#2D3038',
54 borderRadius: 20,
55 alignItems: 'center',
56 justifyContent: 'center',
57 height: 40,
58 width: 40
59 }}
60 >
61 <MaterialCommunityIcons
62 color="#64676D"
63 size={24}
64 name="bookmark-outline"
65 />
66 </TouchableOpacity>
67 </View>
68 </View>
69 </View>
70 </View>
71 );
72};

Here is the output you are going to get after this step:

ss2

Add action creators and update the reducer

🔗

In the redux/action.js file, let's add two more action creators that are going to update the state when the bookmarks are added or removed by the user. Each action creator is going to be based on the action type we defined earlier. Also, each action creator is going to accept the book item that is added to the bookmark list.

1export const addBookmark = book => dispatch => {
2 dispatch({
3 type: ADD_TO_BOOKMARK_LIST,
4 payload: book
5 });
6};
7
8export const removeBookmark = book => dispatch => {
9 dispatch({
10 type: REMOVE_FROM_BOOKMARK_LIST,
11 payload: book
12 });
13};

The next step is to update the state of the redux store. Open redux/reducers.js and modify the following code snippet to perform the actions we just added.

1import {
2 GET_BOOKS,
3 ADD_TO_BOOKMARK_LIST,
4 REMOVE_FROM_BOOKMARK_LIST
5} from './actions';
6
7const initialState = {
8 books: [],
9 bookmarks: []
10};
11
12function booksReducer(state = initialState, action) {
13 switch (action.type) {
14 case GET_BOOKS:
15 return { ...state, books: action.payload };
16 case ADD_TO_BOOKMARK_LIST:
17 return { ...state, bookmarks: [...state.bookmarks, action.payload] };
18 case REMOVE_FROM_BOOKMARK_LIST:
19 return {
20 ...state,
21 bookmarks: state.bookmarks.filter(book => book.id !== action.payload.id)
22 };
23 default:
24 return state;
25 }
26}
27
28export default booksReducer;

Configure and integrate redux persist

🔗

Import the following statements inside redux/store.js file to create a persisted reducer.

1import AsyncStorage from '@react-native-async-storage/async-storage';
2import { persistStore, persistReducer } from 'redux-persist';

Then, add a persistConfig object with the following properties:

1const persistConfig = {
2 key: 'root',
3 storage: AsyncStorage,
4 whitelist: ['bookmarks']
5};

In the above snippet, the key and storage are required to create the config for a persisted reducer. The storage has the value of the storage engine which is used to save and persist the data. In React Native, it is essential to pass the value of the storage explicitly. In the current demo app, let's use AsyncStorage.

The whitelist takes an array of strings. It is used to define which object key to use from the initial state to save the data. If no whitelist is provided, then redux persists for both books and bookmarks. Providing bookmarks as the value of the whitelist is going to only save the data that is in the bookmarks array (which is empty at the moment but will be populated later when a bookmark is added or removed).

Then, update rootReducer with the persisted reducer with two arguments: persistConfig and booksReducer.

Also, export the persistor. It is an object that is returned by persistStore which wraps the original store.

1const rootReducer = combineReducers({
2 booksReducer: persistReducer(persistConfig, booksReducer)
3});
4
5export const store = createStore(rootReducer, applyMiddleware(thunk));
6export const persistor = persistStore(store);

In React Native apps, you have to wrap the root component with PersistGate. This component delays the rendering of the app's UI until the persisted state is retrieved and saved to redux.

Import the PersistGate from the redux-persist library and import persistor from the redux/store file in the App.js file:

1// Add
2import { PersistGate } from 'redux-persist/integration/react';
3
4// Modify to add persistor
5import { store, persistor } from './redux/store';
6
7// Then, modify the JSX returned from App component
8// Wrap the root component with PersistGate
9return (
10 <Provider store={store}>
11 <PersistGate loading={null} persistor={persistor}>
12 <RootNavigator />
13 </PersistGate>
14 </Provider>
15);

That's it to configure and integrate the redux-persist library to the React Native and Redux application.

Create functionality to add or remove a bookmark

🔗

All book items are shown in the BooksList.js file that is fetched from the API. It is from the tab screen that a user can add or remove a bookmark to a book item.

Let's start by importing other action creators as well:

1// Modify
2import { getBooks, addBookmark, removeBookmark } from '../redux/actions';

The booksReducer is used to access the state. Modify it to access the bookmarks array:

1const { books, bookmarks } = useSelector(state => state.booksReducer);

Now, dispatch two actions using the useDispatch hook and create their handler functions. These handler functions are going to be triggered when the touchable component is pressed by the user. Each handler function is going to accept one argument and that is the current book item from FlatList.

1const addToBookmarkList = book => dispatch(addBookmark(book));
2const removeFromBookmarkList = book => dispatch(removeBookmark(book));
3
4const handleAddBookmark = book => {
5 addToBookmarkList(book);
6};
7
8const handleRemoveBookmark = book => {
9 removeFromBookmarkList(book);
10};

Let's add another handler function called ifExists that is going to dynamically change the UI of the app based on the action triggered. This function is going to use filter on the bookmarks array to make the changes on the UI based on whether a book item already exists in the array (that is stored on the AsyncStorage) or not.

1const ifExists = book => {
2 if (bookmarks.filter(item => item.id === book.id).length > 0) {
3 return true;
4 }
5
6 return false;
7};

Modify the TouchableOpacity component to dynamically change the UI of the app when an action is triggered to add or remove an item from the bookmarks list.

1<TouchableOpacity
2 onPress={() =>
3 ifExists(item) ? handleRemoveBookmark(item) : handleAddBookmark(item)
4 }
5 activeOpacity={0.7}
6 style={{
7 // rest remains same
8 backgroundColor: ifExists(item) ? '#F96D41' : '#2D3038'
9 //
10 }}
11>
12 <MaterialCommunityIcons
13 color={ifExists(item) ? 'white' : '#64676D'}
14 size={24}
15 name={ifExists(item) ? 'bookmark-outline' : 'bookmark'}
16 />
17</TouchableOpacity>

Display bookmarks

🔗

Any book item that is bookmarked is going to be shown in the BookmarksList.js tab. Apart from displaying the list of bookmarked items, it is also going to have the functionality of removing book item from the list.

Start by importing the following statements. This time, only import removeBookmark action creator.

1import React from 'react';
2import {
3 SafeAreaView,
4 Text,
5 View,
6 FlatList,
7 TouchableOpacity,
8 Image
9} from 'react-native';
10import { useSelector, useDispatch } from 'react-redux';
11import { MaterialCommunityIcons } from '@expo/vector-icons';
12
13import { removeBookmark } from '../redux/actions';

Using the useSelector hook allows us to access the bookmarks state. Then, using the useDispatch hook defines the action creator and handler function to remove a book from the bookmarks list.

1export default function BookmarksList() {
2 const { bookmarks } = useSelector(state => state.booksReducer);
3 const dispatch = useDispatch();
4
5 const removeFromBookmarkList = book => dispatch(removeBookmark(book));
6
7 const handleRemoveBookmark = book => {
8 removeFromBookmarkList(book);
9 };
10
11 //...
12}

Lastly, the UI of this tab screen is going to be similar to that of the BooksList.js tab. Using the FlatList component, let's show the list of all the items that are bookmarked.

If there are no items that are bookmarked, let's display a simple message to convey that. This is done by checking the length of the bookmarks array from the state.

Here is the complete JSX snippet returned by the BookmarksList tab component:

1export default function BookmarksList() {
2 // ...
3 const renderItem = ({ item }) => {
4 return (
5 <View style={{ marginVertical: 12 }}>
6 <View style={{ flexDirection: 'row', flex: 1 }}>
7 {/* Book Cover */}
8 <Image
9 source={{ uri: item.image_url }}
10 resizeMode="cover"
11 style={{ width: 100, height: 150, borderRadius: 10 }}
12 />
13 {/* Book Metadata */}
14 <View style={{ flex: 1, marginLeft: 12 }}>
15 {/* Book Title */}
16 <View>
17 <Text style={{ fontSize: 22, paddingRight: 16, color: 'white' }}>
18 {item.title}
19 </Text>
20 </View>
21 {/* Meta info */}
22 <View
23 style={{
24 flexDirection: 'row',
25 marginTop: 10,
26 alignItems: 'center'
27 }}
28 >
29 <MaterialCommunityIcons
30 color="#64676D"
31 name="book-open-page-variant"
32 size={20}
33 />
34 <Text style={{ fontSize: 14, paddingLeft: 10, color: '#64676D' }}>
35 {item.num_pages}
36 </Text>
37 <MaterialCommunityIcons
38 color="#64676D"
39 name="star"
40 size={20}
41 style={{ paddingLeft: 16 }}
42 />
43 <Text style={{ fontSize: 14, paddingLeft: 10, color: '#64676D' }}>
44 {item.rating}
45 </Text>
46 </View>
47 {/* Buttons */}
48 <View style={{ marginTop: 14 }}>
49 <TouchableOpacity
50 onPress={() => handleRemoveBookmark(item)}
51 activeOpacity={0.7}
52 style={{
53 flexDirection: 'row',
54 padding: 2,
55 backgroundColor: '#2D3038',
56 borderRadius: 20,
57 alignItems: 'center',
58 justifyContent: 'center',
59 height: 40,
60 width: 40
61 }}
62 >
63 <MaterialCommunityIcons
64 color="#64676D"
65 size={24}
66 name="bookmark-remove"
67 />
68 </TouchableOpacity>
69 </View>
70 </View>
71 </View>
72 </View>
73 );
74 };
75
76 return (
77 <SafeAreaView style={{ flex: 1, backgroundColor: '#1E1B26' }}>
78 <View style={{ flex: 1, paddingHorizontal: 16 }}>
79 <Text style={{ color: 'white', fontSize: 22 }}>Bookmarks</Text>
80 <View style={{ flex: 1, marginTop: 8 }}>
81 {bookmarks.length === 0 ? (
82 <Text style={{ color: '#64676D', fontSize: 18 }}>
83 Add a book to bookmark list.
84 </Text>
85 ) : (
86 <FlatList
87 data={bookmarks}
88 keyExtractor={item => item.id.toString()}
89 renderItem={renderItem}
90 showsVerticalScrollIndicator={false}
91 />
92 )}
93 </View>
94 </View>
95 </SafeAreaView>
96 );
97}

Running the app

🔗

Go to the simulator or the real device where you are running the Expo client, and you can test the functionality by adding or removing the bookmark to an item. Also, notice the dynamic UI changes of the bookmark button in the first tab.

ss3

Make sure to close the Expo client and then start it to see if the state from the Redux store persists or not.

ss4

And that's it! I hope you have found this tutorial helpful.

Further Reading

🔗

Originally published at Jscrambler.com


More Posts

Browse all posts

Aman Mittal author

I'm a software developer and a technical writer. On this blog, I write about my learnings in software development and technical writing.

Currently, working maintaining docs at 𝝠 Expo. Read more about me on the About page.


Copyright ©  2019-2024 Aman Mittal · All Rights Reserved.