How to use redux-persist in React Native with Asyncstorage
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 into 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 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 directorycd redux-persist-asyncstorage-exampleyarn add @react-navigation/native @react-navigation/bottom-tabs axios@0.21.0redux@4.0.5 redux-persist@6.0.0 redux-thunk@2.3.0 react-redux@7.2.2# install dependencies with Expo specific package versionexpo install react-native-gesture-handler react-native-reanimatedreact-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';34export default function BooksListApp() {5 return (6 <View style={styles.container}>7 <Text>BooksList</Text>8 </View>9 );10}1112const 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';34export default function BookmarksList() {5 return (6 <View style={styles.container}>7 <Text>BookmarksList</Text>8 </View>9 );10}1112const 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';56// Import mock screens7import BooksList from '../screens/BooksList';8import BookmarksList from '../screens/BookmarksList';910const 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};1011const screenOptions = (route, color) => {12 let iconName;1314 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 }2425 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.Navigator5 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};1718export 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';23import RootNavigator from './navigation/RootNavigator';45export 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:
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 types2export 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';23import { 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.data9 });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 errors16 console.log(error);17 }18};
Add a reducer
🔗Whenever an action has 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';23const initialState = {4 books: [],5 bookmarks: []6};78function 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}1617export 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';34import booksReducer from './reducers';56const rootReducer = combineReducers({ booksReducer });78export 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';34import { store } from './redux/store';5import RootNavigator from './navigation/RootNavigator';67export 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.
1import React, { useEffect } from 'react';2import {3 Text,4 View,5 FlatList,6 TouchableOpacity,7 Image,8 SafeAreaView9} from 'react-native';10import { useSelector, useDispatch } from 'react-redux';11import { MaterialCommunityIcons } from '@expo/vector-icons';1213import { getBooks } from '../redux/actions';
To access 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);34 //...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();23const fetchBooks = () => dispatch(getBooks());45useEffect(() => {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 <FlatList7 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 <Image7 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 <View21 style={{22 flexDirection: 'row',23 marginTop: 10,24 alignItems: 'center'25 }}26 >27 <MaterialCommunityIcons28 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 <MaterialCommunityIcons36 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 <TouchableOpacity48 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: 4059 }}60 >61 <MaterialCommunityIcons62 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:
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: book5 });6};78export const removeBookmark = book => dispatch => {9 dispatch({10 type: REMOVE_FROM_BOOKMARK_LIST,11 payload: book12 });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_LIST5} from './actions';67const initialState = {8 books: [],9 bookmarks: []10};1112function 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}2728export 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 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});45export 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// Add2import { PersistGate } from 'redux-persist/integration/react';34// Modify to add persistor5import { store, persistor } from './redux/store';67// Then, modify the JSX returned from App component8// Wrap the root component with PersistGate9return (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// Modify2import { 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));34const handleAddBookmark = book => {5 addToBookmarkList(book);6};78const 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 }56 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<TouchableOpacity2 onPress={() =>3 ifExists(item) ? handleRemoveBookmark(item) : handleAddBookmark(item)4 }5 activeOpacity={0.7}6 style={{7 // rest remains same8 backgroundColor: ifExists(item) ? '#F96D41' : '#2D3038'9 //10 }}11>12 <MaterialCommunityIcons13 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 Image9} from 'react-native';10import { useSelector, useDispatch } from 'react-redux';11import { MaterialCommunityIcons } from '@expo/vector-icons';1213import { 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();45 const removeFromBookmarkList = book => dispatch(removeBookmark(book));67 const handleRemoveBookmark = book => {8 removeFromBookmarkList(book);9 };1011 //...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 <Image9 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 <View23 style={{24 flexDirection: 'row',25 marginTop: 10,26 alignItems: 'center'27 }}28 >29 <MaterialCommunityIcons30 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 <MaterialCommunityIcons38 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 <TouchableOpacity50 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: 4061 }}62 >63 <MaterialCommunityIcons64 color="#64676D"65 size={24}66 name="bookmark-remove"67 />68 </TouchableOpacity>69 </View>70 </View>71 </View>72 </View>73 );74 };7576 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 <FlatList87 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.
Make sure to close the Expo client and then start it to see if the state from the Redux store persists or not.
And that's it! I hope you have found this tutorial helpful.
Further Reading
🔗Originally published at Jscrambler.com
More Posts
Browse all posts