React Redux Save Data in Local Storage with Persist Tutorial
In this comprehensive tutorial, we will learn how to persist redux store’s states in React js app using react-redux, redux toolkit, and redux persist packages.
If you have worked with the browser’s local storage api before, then you won’t face understanding redux persist. Redux persist is a notable library that allows you to save redux storage into your browser’s local storage, session storage, or async storage.
In this post, we will create api reducer and product reducer using redux and redux toolkit apis and save the redux store in the browser’s local storage.
To add the data in local storage through redux persist, we will be adding both the reducers in the combineReducers function and pass to the redux persist. To persist the redux store, we have to define the persistConfig, where we determine the key as an id, storage for various types of storage, and some other configurations.
How to Save React Redux State in Local Storage using Redux Persist
- Step 1: Download New React App
- Step 2: Install Required Packages
- Step 3: Create Redux Toolkit Slices
- Step 4: Connect Redux Persist to Redux Store
- Step 4: Consume Redux Persist State in Components
- Step 5: Define Navigational Routes
- Step 6: Start React Server
Download New React App
Head over to the terminal app, start typing the given command on the console, then hit enter to install the new react app.
npx create-react-app react-redux-persist-example
This tutorial is not just limited to working with redux in rect but also highlights the essential paradigms for newbie developers.
Here is the project folder structure for working with react and redux properly.
Install Required Packages
We need to install bootstrap, react-redux, @reduxjs/toolkit, react-router-dom, and redux-persist packages. These modules are essential for setting up redux store and storing react state centrally as well as globally in our react app.
npm install bootstrap react-redux @reduxjs/toolkit react-router-dom redux-persist --legacy-peer-deps
Create Redux Toolkit Slices
In this step, you have to make the features/ directory in the app/ folder, make sure to create apiSlices.js and useCartSlice.js files into this folder.
We will first fetch data using the api with createApi, and fetchBaseQuery modules, pass the api name into the baseUrl and create the endpoint that renders the data from the server.
Add code in the app/features/apiSlice.js file.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const apiSlice = createApi({
reducerPath: 'apiProductSlice',
baseQuery: fetchBaseQuery({
baseUrl: 'https://fakestoreapi.com',
}),
tagTypes: ['Product'],
endpoints: (builder) => ({
getProducts: builder.query({
query: () => '/products',
providesTags: ['Product'],
}),
}),
})
export const { useGetProductsQuery } = apiSlice
Similarly, create the another file name it app/features/useCartSlice.js add the given code to the file.
import { createSlice } from '@reduxjs/toolkit'
const useCartSlice = createSlice({
name: 'cart',
initialState: {
cartItems: [],
totalCount: 0,
tax: 0,
subAmount: 0,
totalAmount: 0,
},
reducers: {
addCartProduct: {
reducer: (state, action) => {
let cartIndex = state.cartItems.findIndex(
(item) => item.id === action.payload.id,
)
if (cartIndex >= 0) {
state.cartItems[cartIndex].quantity += 1
} else {
let tempProduct = { ...action.payload, quantity: 1 }
state.cartItems.push(tempProduct)
}
},
},
getCartProducts: (state, action) => {
return {
...state,
}
},
getCartCount: (state, action) => {
let cartCount = state.cartItems.reduce((total, item) => {
return item.quantity + total
}, 0)
state.totalCount = cartCount
},
getSubTotal: (state, action) => {
state.subAmount = state.cartItems.reduce((acc, item) => {
return acc + item.price * item.quantity
}, 0)
},
removeCartItem: (state, action) => {
let index = state.cartItems.findIndex(
(item) => item.id === action.payload,
)
if (index !== -1) {
state.cartItems.splice(index, 1)
}
},
increment: (state, action) => {
let index = state.cartItems.findIndex(
(item) => item.id === action.payload,
)
state.cartItems[index].quantity += 1
},
decrement: (state, action) => {
let index = state.cartItems.findIndex(
(item) => item.id === action.payload,
)
if (state.cartItems[index].quantity <= 0) {
state.cartItems[index].quantity = 0
} else {
state.cartItems[index].quantity -= 1
}
},
calculateTax: (state, action) => {
// GST value: 18% => action.payload
let totalTax = (18 / 100) * state.subAmount
state.tax = totalTax
},
getTotalAmount: (state, action) => {
state.totalAmount = state.tax + state.subAmount
},
},
})
export const {
addCartProduct,
getCartProducts,
removeCartItem,
getCartCount,
getSubTotal,
increment,
decrement,
calculateTax,
getTotalAmount,
} = useCartSlice.actions
export default useCartSlice.reducer
Connect Redux Persist to Redux Store
In this step, we will create the react redux store and set up the redux persist inside the react store using the reducers we have created in earlier step.
Inside the app/ folder create the store.js file, then inside the file add the give code.
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { combineReducers } from '@reduxjs/toolkit'
import { apiSlice } from '../features/apiSlice'
import useCartReducer from '../features/useCartSlice'
import storage from 'redux-persist/lib/storage'
import {
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from 'redux-persist'
const persistConfig = {
key: 'root',
storage: storage,
blacklist: ['apiProductSlice'],
}
export const rootReducers = combineReducers({
cart: useCartReducer,
[apiSlice.reducerPath]: apiSlice.reducer,
})
const persistedReducer = persistReducer(persistConfig, rootReducers)
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat(apiSlice.middleware),
})
setupListeners(store.dispatch)
export default store
To set up the redux store, import the configureStore, and setupListeners.
Import the apiSlice and useCartReducer from their respective files. Next, import the combineReducers from the redux toolkit library. The combine reducer groups any number of reducers in a single object and returns a reducer object.
We are defining apiSlice and reducer in a redux store and configuring redux persist in the redux store file.
As you can see, we determined the api reducer to blocklist property. This means we don’t want to persist with this specific reducer in local storage.
It also offers some other properties; you can check out the complete documentation of redux persist here.
Next, we are going to set up the provider for making the redux state globally and centrally available, also setting up the redux persist state into react-redux App.
Update the given code into the index.js file.
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { BrowserRouter } from 'react-router-dom'
import { persistStore } from 'redux-persist'
import { PersistGate } from 'redux-persist/integration/react'
import { Provider } from 'react-redux'
import store from './app/store'
let persistor = persistStore(store)
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<BrowserRouter>
<Provider store={store}>
<PersistGate persistor={persistor}>
<App />
</PersistGate>
</Provider>
</BrowserRouter>,
)
We need to navigate between two pages in our react demo app therefore import the BrowserRouter.
Consume Redux Persist State in Components
This step will show you how to consume Redux Persist state in various react components.
Hence, you have to build the src/components/cart/Products.js file then add the suggested code for showing the all the products retrieved from the redux api slice.
import React, { useEffect } from 'react'
import { useGetProductsQuery } from '../../features/apiSlice'
import {
addCartProduct,
getCartCount,
getSubTotal,
calculateTax,
getTotalAmount,
} from '../../features/useCartSlice'
import { useDispatch } from 'react-redux'
function Products() {
const dispatch = useDispatch()
let productObj = {
id: '',
title: '',
price: '',
image: '',
}
const addToCart = (item) => {
productObj = {
id: item.id,
title: item.title,
price: item.price,
image: item.image,
}
dispatch(addCartProduct(productObj))
dispatch(getCartCount())
dispatch(getSubTotal())
dispatch(calculateTax())
dispatch(getTotalAmount())
}
const {
data: products,
isLoading: isProductLoading,
isSuccess: isProductSuccess,
isError: isProductError,
error: prouctError,
} = useGetProductsQuery({ refetchOnMountOrArgChange: true })
useEffect(() => {}, [dispatch])
let getData
if (isProductLoading) {
getData = (
<div className="d-flex justify-content-center w-100">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
)
} else if (isProductSuccess) {
getData = products.map((item) => {
return (
<div className="col" key={item.id}>
<div className="card h-100 product-card">
<div className="img-grid mb-3">
<img src={item.image} className="card-img-top" alt={item.title} />
</div>
<div className="card-body">
<h5 className="card-title">${item.price}</h5>
<p className="card-text">
{item.description.substring(0, 50)}...
</p>
<button className="btn btn-outline-danger me-2">Buy now</button>
<button
onClick={() => {
addToCart(item)
}}
className="btn btn-outline-primary"
>
Add to cart
</button>
</div>
</div>
</div>
)
})
} else if (isProductError) {
getData = (
<div className="alert alert-danger w-100 text-center" role="alert">
{prouctError.status} {JSON.stringify(prouctError)}
</div>
)
}
return (
<div>
<div className="row row-cols-1 row-cols-md-3 row-cols-sm-2 g-4">
{getData}
</div>
</div>
)
}
export default Products
Now, we will create the src/components/cart/header.js file then add the entire code inside the file. In this file, we will set up the site header where we will add some value that we need to show even after the page reloaded or refreshed.
import React from 'react'
import { Link } from 'react-router-dom'
import { useSelector } from 'react-redux'
function Header() {
const { totalCount } = useSelector((state) => state.cart)
return (
<header>
<div className="d-flex flex-column flex-md-row align-items-center pb-3 mb-3 border-bottom">
<Link
to="/"
className="d-flex align-items-center text-dark text-decoration-none"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="40"
height="32"
className="me-2"
viewBox="0 0 118 94"
role="img"
>
<title>Bootstrap</title>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M24.509 0c-6.733 0-11.715 5.893-11.492 12.284.214 6.14-.064 14.092-2.066 20.577C8.943 39.365 5.547 43.485 0 44.014v5.972c5.547.529 8.943 4.649 10.951 11.153 2.002 6.485 2.28 14.437 2.066 20.577C12.794 88.106 17.776 94 24.51 94H93.5c6.733 0 11.714-5.893 11.491-12.284-.214-6.14.064-14.092 2.066-20.577 2.009-6.504 5.396-10.624 10.943-11.153v-5.972c-5.547-.529-8.934-4.649-10.943-11.153-2.002-6.484-2.28-14.437-2.066-20.577C105.214 5.894 100.233 0 93.5 0H24.508zM80 57.863C80 66.663 73.436 72 62.543 72H44a2 2 0 01-2-2V24a2 2 0 012-2h18.437c9.083 0 15.044 4.92 15.044 12.474 0 5.302-4.01 10.049-9.119 10.88v.277C75.317 46.394 80 51.21 80 57.863zM60.521 28.34H49.948v14.934h8.905c6.884 0 10.68-2.772 10.68-7.727 0-4.643-3.264-7.207-9.012-7.207zM49.948 49.2v16.458H60.91c7.167 0 10.964-2.876 10.964-8.281 0-5.406-3.903-8.178-11.425-8.178H49.948z"
fill="currentColor"
></path>
</svg>
<span className="fs-4">React cart example</span>
</Link>
<nav className="d-inline-flex mt-2 mt-md-0 ms-md-auto">
<span className="cart py-2 text-dark text-decoration-none position-relative">
<Link to="/cart">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="bi bi-cart4"
viewBox="0 0 16 16"
>
<path d="M0 2.5A.5.5 0 0 1 .5 2H2a.5.5 0 0 1 .485.379L2.89 4H14.5a.5.5 0 0 1 .485.621l-1.5 6A.5.5 0 0 1 13 11H4a.5.5 0 0 1-.485-.379L1.61 3H.5a.5.5 0 0 1-.5-.5zM3.14 5l.5 2H5V5H3.14zM6 5v2h2V5H6zm3 0v2h2V5H9zm3 0v2h1.36l.5-2H12zm1.11 3H12v2h.61l.5-2zM11 8H9v2h2V8zM8 8H6v2h2V8zM5 8H3.89l.5 2H5V8zm0 5a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm-2 1a2 2 0 1 1 4 0 2 2 0 0 1-4 0zm9-1a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm-2 1a2 2 0 1 1 4 0 2 2 0 0 1-4 0z" />
</svg>
<span className="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
{totalCount}
<span className="visually-hidden">unread messages</span>
</span>
</Link>
</span>
</nav>
</div>
</header>
)
}
export default Header
Next, we need to make the src/components/cart/Cart.js file then define the all given code within the file. In this file, we are writing the cart view code using the reducers objects and bootstrap ui.
import React, { useEffect } from 'react'
import { Link } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
import {
getCartProducts,
getSubTotal,
removeCartItem,
getCartCount,
increment,
decrement,
calculateTax,
getTotalAmount,
} from '../../features/useCartSlice'
function Cart() {
const dispatch = useDispatch()
const { cartItems, subAmount, tax, totalAmount } = useSelector(
(state) => state.cart,
)
useEffect(() => {
dispatch(getCartProducts())
dispatch(getSubTotal())
dispatch(getCartCount())
dispatch(calculateTax())
dispatch(getTotalAmount())
}, [dispatch])
let showCart
if (cartItems !== undefined && cartItems.length > 0) {
showCart = (
<>
<table className="table table-hover mb-3 cart-table">
<thead>
<tr>
<th>Product</th>
<th className="text-center">Quantity</th>
<th className="text-center">Price</th>
<th className="text-center">Total</th>
</tr>
</thead>
<tbody>
{cartItems !== undefined &&
cartItems.length > 0 &&
cartItems.map((product, index) => {
return (
<tr key={product.id}>
<td className="col-md-4">
<div className="media">
<a className="thumbnail pull-left" href="/#">
<img
className="media-object"
src={product.image}
alt={product.title}
/>
</a>
</div>
</td>
<td className="col-sm-1 col-md-2 text-center">
<div className="cart-quantity">
<button
className="btn btn-sm btn-outline-primary inline-block"
onClick={() => {
dispatch(increment(product.id))
dispatch(getSubTotal())
dispatch(getCartCount())
dispatch(calculateTax())
dispatch(getTotalAmount())
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="bi bi-plus-lg"
viewBox="0 0 16 16"
>
<path
fillRule="evenodd"
d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2Z"
/>
</svg>
</button>
<span className="text-center inline-block">
{product.quantity}
</span>
<button
className="btn btn-sm btn-outline-primary inline-block"
onClick={() => {
dispatch(decrement(product.id))
dispatch(getSubTotal())
dispatch(getCartCount())
dispatch(calculateTax())
dispatch(getTotalAmount())
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="bi bi-dash"
viewBox="0 0 16 16"
>
<path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z" />
</svg>
</button>
</div>
</td>
<td className="col-sm-1 col-md-1 text-center">
<strong>{product.price}</strong>
</td>
<td className="col-sm-1 col-md-1 text-center">
<strong>
{parseFloat(product.price * product.quantity).toFixed(
2,
)}
</strong>
</td>
<td className="col-sm-1 col-md-1">
<button
type="button"
onClick={() => {
dispatch(removeCartItem(product.id))
dispatch(getSubTotal())
dispatch(getCartCount())
dispatch(calculateTax())
dispatch(getTotalAmount())
}}
className="btn btn-sm btn-outline-danger"
>
Remove
</button>
</td>
</tr>
)
})}
</tbody>
</table>
<div className="col-md-12 cart">
<div className="col-md-6 p-4 offset-md-6 border alert alert-secondary">
<div className="d-flex gst">
<div className="flex-grow-1">GST 18%</div>$
{parseFloat(tax).toFixed(2)}
</div>
<hr />
<div className="d-flex gst">
<div className="flex-grow-1">Subtotal</div>$
{parseFloat(subAmount).toFixed(2)}
</div>
<hr />
<div className="d-flex">
<div className="flex-grow-1">
<strong>Total Amount</strong>
</div>
<div>
<strong>${parseFloat(totalAmount).toFixed(2)}</strong>
</div>
</div>
<div className="d-grid mt-3">
<button type="button" className="btn btn-dark">
Proceed to Checkout
</button>
</div>
</div>
</div>
</>
)
} else {
showCart = (
<div className="w-100 alert alert-warning mt-5">
<h2 className="h4 text-center mb-3">Your cart is empty</h2>
<p className="text-center">
Looks like you have not added anything to your cart. Let's buy some
products.
</p>
<div className="text-center">
<Link to="/" className="btn btn-danger">
Shop now
</Link>
</div>
</div>
)
}
return <>{showCart}</>
}
export default Cart
Define Navigational Routes
Open and add the suggested code within the App.js file, here we are importing few components between which we will be navigating throughout our app using the react router dom methods.
import '../node_modules/bootstrap/dist/css/bootstrap.min.css'
import './App.css'
import { Routes, Route } from 'react-router-dom'
import Header from './components/header/Header'
import Products from './components/products/Products'
import Cart from './components/cart/Cart'
function App() {
return (
<div className="container py-3">
<Header />
<Routes>
<Route path="/" element={<Products />} />
<Route path="/cart" element={<Cart />} />
</Routes>
</div>
)
}
export default App
Start React Server
You can start the react app using the following command:
npm start
Your app will start on the given url.
http://localhost:3000
Conclusion
Redux persist is a powerful library with certain great features; one of the best things we liked about it is that it offers Blacklist and Whitelist features.
Due to these features, you have the authority to persist a specific reducer or not. This library is excellent, but updates don’t come often for it; it also has particular issues on GitHub.
Project code can be found here.