Disclaimer
This post is intended for developers with a basic understanding of using Redux and its concepts such as stores, actions and reducers. If you are unfamiliar with these concepts, I highly recommend starting with the Redux Essentials documentation or a concise Redux for Beginners Tutorial video, if you’re looking for a sample project walkthrough.
Simple Recap on Redux
Redux is an open-source JS library designed to help developers manage the state of variables in their applications. Redux does this by creating a centralized store, and creates processes such as dispatch and reducers. This ensures that the various components from any part of a complex application, can read and update the data from the same store, in a uniform and consistent manner. Hence, this allows easy communication between components in an application.
In this blog post, I will not be going in-depth into the boilerplate setup of Redux and other miscellaneous steps in the projects (writing different React components or CSS). I will instead start by showing a simple application that uses Redux, and explain how Redux-Persist will tackle the pain point of the application.
Pokémon with Redux
Setting up the Project
I will be showing a simple Next.js application, initialized with typescript. Additionally, we will be using 2 Redux libraries:
- react-redux
The library needed to have the core redux functionalities - Store, dispatching, actions and so on. Most of us should already be familiar with this.
- @reduxjs/toolkit
A toolkit makes it easier to write good Redux applications and speeds up development, by baking in our recommended best practices, providing good default behaviours, catching mistakes, and allowing you to write simpler code.
# Create a new Next.js application, with typescript
npx create-next-app@latest --ts
# Install redux related tools
npm install @reduxjs/toolkit react-redux
Simple Features of Application
Fast forward a little and we have ourselves a basic application that uses Redux. This is a simple Pokémon application, that you can use to create a team for your next Pokémon journey! If you’re unfamiliar with Pokémon, it is a role-playing game based around building a small team of monsters (Pokémon), to battle other wild monsters and players (Trainers) to become the very best! This application uses the RESTful Pokémon API that can be found here. Even if you’re unfamiliar with Pokémon, the features of this application will still be easy to understand, if you’re already familiar with Redux. For now, let’s just take a look at 2 Redux-related files, and the 3 main components of our application that uses the Redux functionalities. In the videos I included, I made use of Google Chrome’s Inspect tool and Redux DevTools, a chrome extension that allows us to debug application state changes.
1. pokemonSlice.ts
This file declares the initial state value of our store. In our case, the state that we will be keeping track of, is the Pokémon in our team, in the form of an array. Additionally, we also create 3 reducers - addPokemon
, removePokemon
and removeAllPokemon
- that will mutate our state by adding or removing single/all Pokémon from the state.
export const pokemonSlice = createSlice({
name: "pokemon",
initialState,
reducers: {
addPokemon: (state, action: PayloadAction<PokemonBasicInfo>) => {
state.value = [...state.value, action.payload];
},
removePokemon: (state, action: PayloadAction<PokemonBasicInfo>) => {
state.value = state.value.filter(
(poke) => poke.name !== action.payload.name
);
},
removeAllPokemon: (state) => {
state.value = [];
},
},
});
export const selectValue = (state: RootState) => state.pokemonTeam.value;
export const { addPokemon, removePokemon, removeAllPokemon } =
pokemonSlice.actions;
export default pokemonSlice.reducer;
2. store.ts
This is the file which holds the whole state tree of our application. configureStore
, similar to createStore
, initializes our store with the reducers and initial state that were declared in pokemonSlice.ts
.
import { configureStore } from "@reduxjs/toolkit";
import pokemonTeamReducer from "./slices/pokemonSlice";
export const store = configureStore({
reducer: {
pokemonTeam: pokemonTeamReducer,
},
});
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;
3.Pokedex.tsx
This component displays a grid of Pokémon, where users can add Pokémon to their team. Users also have the ability to remove a Pokémon from their team if needed. These are both accomplished using Redux’s useSelector
and useDispatch
, combined with the reducers that we declared previously
const Pokedex = () => {
const [pokedex, loading] = useGetPokemon();
const myTeam = useSelector(selectValue);
const myTeamNames = myTeam.map((poke) => poke.name);
const dispatch = useDispatch();
function handleClick(poke: PokemonBasicInfo) {
if (myTeamNames.length == 6 && !myTeamNames.includes(poke.name)) {
alert(
"You can only have 6 Pokémon in your team! Remove one Pokémon to add another."
);
return;
}
if (!myTeamNames.includes(poke.name)) dispatch(addPokemon(poke));
else dispatch(removePokemon(poke));
}
return (
<div className={styles.pokedexWrapper}>
<h1 className={styles.pokedexTitle}>Kanto Pokédex</h1>
{loading ? (
<h1>Loading...</h1>
) : (
<>
<div className={styles.pokedex}>
{pokedex.map((poke: PokemonBasicInfo) => (
<div
key={poke.name}
className={
myTeamNames.includes(poke.name)
? styles.pokemonChosen
: styles.pokemon
}
onClick={() => handleClick(poke)}
>
<img src={poke.image} alt={poke.name} />
</div>
))}
</div>
</>
)}
</div>
);
};
export default Pokedex;
4. Team.tsx
This component showcases the Pokémon that we have already added into our team. It does this by reading the state from the store using Redux’s useSelector
.
const Team = () => {
const myTeam = useSelector(selectValue);
const dispatch = useDispatch();
return (
<div className={styles.teamContainer}>
<h1 className={styles.header}>My Team</h1>
{myTeam.length ? (
<div className={styles.teamWrapper}>
<div className={styles.teamListing}>
{myTeam.map((poke: PokemonBasicInfo) => (
<div key={poke.name} className={styles.pokemon}>
<img src={poke.image} alt={poke.name} />
<h1 className={styles.pokemonName}>{poke.name}</h1>
</div>
))}
</div>
<button
className={styles.clearButton}
onClick={() => {
dispatch(removeAllPokemon());
}}
>
Reset
</button>
</div>
) : (
<h1 className={styles.emptyMessage}>
Click on a Pokémon to add it to your team!
</h1>
)}
</div>
);
};
export default Team;
5. _app.tsx
The entry point for our app is then wrapped with a Provider
which provisions the store
we previously created. This allows all its children components to access the store and make modifications to the state.
function MyApp({ Component, pageProps }: AppProps) {
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}
For an in-depth look at the project, please visit my GitHub.
Limitations of Redux Alone
But here’s a problem.
Did you catch that? If you said that the problem was choosing Oddish to be part of my team, technically you’re not wrong….. BUT a more drastic issue would be that my team selection gets wiped whenever I refresh the page. What if I forgot what my team selection was before? How would I make sure that the application remembers (persists) the choices (state) that I have made?
Redux-Persist
Redux-Persist is an add-on library to Redux that will allow us to save our Redux store in the local or session storage. Before I dive deeper into how and why it works, let’s take a look at adding it into our application. This is a simple process as it only requires us to modify our store.ts
and _app.tsx
. But before that, let’s install the package.
# Install redux-persist
npm i redux-persist
Changes /Additions Made
Now, let’s delve deeper into the changes and additions that were made to our existing files.
1. store.ts
Function / Object | Description |
---|---|
combineReducers() | Merges all the reducers into a single reducer object. Think of it as throwing all our reducers into a giant box, so that we can easily reference all our reducers by pointing to that giant box. |
persistConfig | This is a configuration object that is used to customize the persistence of our reducers in our store. The key configuration to include is the key and storage. For our case, we our key is simply root and the storage we are using is the session storage which is imported from redux-persist/lib/storage/session . Alternatively, to use local storage, we can instead import storage from redux-persist/lib/storage but of course, the choice is yours. This will determine where our store and state is being stored. You can look at it as the core of redux-persist as it allows the application to remember and persist the data using the local/session storage as its memory. |
persistReducer() | Takes in the persistConfig object we created earlier, together with the all-in-one reducer to create our persistedReducer. What we are doing is simply applying the persistence configuration on all our reducers |
configureStore() | If you’re familiar with Redux’s createStore, this is a simply an abstraction/wrapper around that function which helps to add good default to our store setup. This function takes in a configuration object which in our case just includes the persistedReducer we created. This store is exported and is the same one provisioned via the Provider wrapper in _app.tsx. |
persistStore() | Finally, we pass our previously created store object into this function, to finalize the creation of a store that is able to persist state, by saving it in the local storage. This persistor is exported to be used in _app.tsx |
const persistConfig = {
key: "root",
storage,
};
const reducers = combineReducers({ pokemonTeam: pokemonTeamReducer });
const persistedReducer = persistReducer(persistConfig, reducers);
export const store = configureStore({
reducer: persistedReducer,
});
export let persistor = persistStore(store);
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;
2. _app.tsx
Apart from the store that we already provisioned, to ensure that the persistence configuration is set for all the components in our application, we add another wrapper PersistGate
. This wrapper takes in the persistor
that we created in store.ts
and can also take in a loading component. e.g loading={<Loading />}
. PersistGate
will help to delay the rendering of the app until the persisted state has been retrieved from the local/session storage and saved to redux.
function MyApp({ Component, pageProps }: AppProps) {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Component {...pageProps} />
</PersistGate>
</Provider>
);
}
Final Product
Add here’s the new and improved Pokémon team builder, now enhanced with redux-persist! As you can see, our application remembers our team, despite refreshing the page! When we look at the Redux DevTools deeper, we can see that Redux performs a persist and rehydrate, allowing our data to be ‘remembered’ on refresh. Additionally, we can also see that our information is being stored in the session storage, in the key persist:root
.
Once again, for an in-depth look at the project, please visit my GitHub.
Conclusion
Redux-persist is a very helpful addon to Redux that allows our application to persist and remember our state, by saving the data in the browser’s persistent storage. This ensures that the data will still be available for our components to modify and utilise, despite a browser refresh. We are able to further specify which storage to use - local or session - or customize the persistence of our store by including/excluding desired reducers. With that said, what’s stopping you from simply creating a helper function that adds the state to the storage whenever we do a useDispatch()
, and check that storage for existing any data whenever we perform a useSelector()
to retrieve the state? Instead, maybe we could have used localStorage.setItem("team", [])
and localStorage.get('team')
? Let me know your thoughts on which you prefer or if you have any other alternatives for persisting Redux state!
Lastly, if you’re looking for a more in depth understanding of redux-persist and its other features and configurability, here are other helpful articles/video you can visit!
- Redux: Persist Your State by Zack (2017)
- The Definitive Guide to Redux Persist by Mark Newton (2017)
- Redux-persist: The Good Parts by Feargal Walsh (2018)
- How to persist and rehydrate redux store efficiently by The Minimalist Coder (2021)
Top comments (0)