How To Simplify Redux Logic With Redux Toolkit
Building a Pokémon App with ReactJS & Modern Redux
Redux is a popular state management library for JavaScript applications. It provides a store that all components can access to get the current state. However, updating the state immutably, creating action types and case reducers, and handling the asynchronous functions all require a lot of code. This is why the Redux team created the Redux Toolkit, which makes writing Redux logic clear and efficient.
In this tutorial, we will create a Pokémon website using React and Redux Toolkit. My focus is to demonstrate how Redux Toolkit makes managing the state in a React app easier. So, I assume that you already have a basic understanding of React and Redux.
You can view the live version of the final website here.
Pokeverse Webpage
Project Overview
Making asynchronous API calls with
createAsyncThunk
Defining actions and reducers using
createSlice
Setting up a store with
configureStore
Combining Redux state with React components
Dispatching actions to modify the state
I’ve set up a basic layout for this project. If you’d like to follow along, you can get the source code in this basic layout branch.
Basic layout for the website
Inside src/components/
folder, there’s only one component called PokeCard
that displays data from an object. App
component maps items from an array to PokeCard
. Later, we’ll load data into this pokemons
array.
// src/App.js
import { Col, Container, Row } from 'react-bootstrap';
import PokeCard from './components/PokeCard';
function App() {
const pokemons = [
{
id: 1,
name: 'clefairy',
sprites: {
front_default:
'https://PokeAPI/....png',
},
height: 13,
weight: 44,
exp: 5,
},
];
return (
<Container className="p-2">
<h1>Pokeverse</h1>
<Row md={4}>
{pokemons.map((poke) => (
<Col className="p-1" key={poke.id}>
<PokeCard pokemon={poke} />
</Col>
))}
</Row>
</Container>
);
}
export default App;
Preparing HTTP requests
For handling HTTP requests, we’ll define two functions: one for getting a list of all Pokémon and the other for getting the details of each Pokémon.
// src/sevices/PokeService.js
import axios from 'axios';
const getPokeList = () => axios.get('https://pokeapi.co/api/v2/pokemon');
const getPokeDetail = (url) => axios.get(url);
const PokeService = { getPokeList, getPokeDetail };
export default PokeService;
While it is possible to combine these functions or write all of these codes in an action creator, I recommend this approach since it makes the code more readable and maintainable for more complex applications.
Making a thunk for API calls
In this section, we’ll make an action creator for fetching all Pokémon data. First, we’ll get a list of all Pokémon, and then get detailed data for each item. Since we’re making asynchronous API calls, we’ll use createAsyncThunk
from @reduxjs/toolkit
.
createAsyncThunk
needs two parameters:
A string that will be used as a prefix for generated action types
A payload-creator function that should return a
Promise
// src/redux/pokemons/pokemons.js
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import PokeService from '../../services/PokeService';
export const getPokemons = createAsyncThunk('pokemons/get', async () => {
const pokeListRes = await PokeService.getPokeList();
const pokeList = pokeListRes.data.results;
let pokemons = await Promise.all(
pokeList.map(async (data) => {
const res = await PokeService.getPokeDetail(data.url);
const { id, name, sprites, weight, height, base_experience: exp} = res.data;
return {id, name, sprites, weight, height, exp, favorite: false};
}),
);
return pokemons;
});
The above function will generate 3 action creators and 3 action types.
getPokemons.pending
:pokemons/get/pending
getPokemons.fullfilled
:pokemons/get/fullfilled
getPokemons.rejected
:pokemons/get/rejected
I will not go into all of the details for making API calls and extracting response data. Essentially, this function will return an array of Pokémon objects as action.payload
. I’ve added a new property called favorite
in each object which will be used to implement adding/removing items to a favorite list.
Using createSlice
function
We’re going to use the createSlice
function which helps us simplify the Redux reducer and actions. This function has 3 important features:
It allows us to mutate the state safely, so we don’t need to write a lot of code for all the object spreads for immutable updates.
It automatically creates action types and action creators from each case reducer function.
It automatically returns the current state in the default case.
The arguments for createSlice
are:
name
: a string that will be used as a prefix for generated action typesinitialState
: an initial state of the reducerreducers
: an object in which keys are strings and values are case reducer functionsextraReducers
: this function listens for action types defined outsidecreateSlice
(like the ones generated from*createAsyncThunk*
)
Adding a simple case reducer
We’ll add a simple case reducer for toggling favorite
property of the items.
// src/redux/pokemons/pokemons.js
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
...
const initialState = {status: "idle", data: [], error: ""};
const pokeSlice = createSlice({
name: "pokemons",
initialState,
reducers: {
toggleFavorite(state, action) {
const pokemon = state.data.find((p) => p.id === action.payload);
pokemon.favorite = !pokemon.favorite;
},
},
});
toggleFavorite
only needs id
of an item as a single parameter, which can be accessed as action.payload
. We can find the target Pokémon using id
and toggle its favorite property.
This is all the syntax required to write a basic case reducer, as createSlice
will automatically generate required actions and action creators.
Adding cases in extraReducers
For the action types fromgetPokemons
, we’ll add cases in extraReducers
function.
// src/redux/pokemons/pokemons.js
...
const pokeSlice = createSlice({
name: 'pokemons',
...
extraReducers(builder) {
builder
.addCase(getPokemons.pending, (state, action) => {
state.status = 'loading';
})
.addCase(getPokemons.fulfilled, (state, action) => {
state.status = 'success';
state.data = action.payload;
})
.addCase(getPokemons.rejected, (state, action) => {
state.status = 'error';
state.error = action.error.message;
});
},
});
builder.addCase()
function works similarly to a switch/case
statement. I’ve added cases for all action creators just to demonstrate the usage. You can skip the pending
and rejected
cases, and include only a case for fullfilled
status.
After that, we can export actions and reducer from the slice using the Ducks pattern.
// src/redux/pokemons/pokemons.js
...
const pokeSlice = createSlice({
...
});
export const { toggleFavorite } = pokeSlice.actions;
export default pokeSlice.reducer;
Setting up Redux Store
Redux Toolkit has a configureStore
API that simplifies the store setup process.
// src/redux/configureStore.js
import { configureStore } from '@reduxjs/toolkit';
import pokemonReducer from './pokemons/pokemons';
export default configureStore({
reducer: {
pokemons: pokemonReducer,
},
});
If we have other reducers, we can add them inside reducer
and configureStore
will automatically combine them into a root reducer. And it also adds the thunk
middleware for us.
The code for Redux is complete and now the next step is to integrate the Redux store with React and start utilizing it in the application.
Adding Store to React Components
First, we need to add the store we created into App
component using Provider
from react-redux
library.
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import 'bootstrap/dist/css/bootstrap.min.css';
import store from './redux/configureStore';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
);
Dispatching action to get data
Inside App
component, we’ll import all the required functions.
// src/App.js
import { Col, Container, Row } from 'react-bootstrap';
import PokeCard from './components/PokeCard';
// Imports for redux
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getPokemons } from './redux/pokemons/pokemons';
The global state object has a property called data
which will be populated with the array of Pokémon after the API calls. The code for that is inside the *extraReducers*
function written as *state.data = action.payload*
. We’ll remove the placeholder array and select that data
with the useSelector
hook.
Currently, the state is in idle
status and data
is empty as we defined for initialState
. In order to get the data, we need to dispatch getPokemons
action with useDispatch
hook.
// src/App.js
function App() {
const pokemons = useSelector((state)=>state.pokemons.data);
const status = useSelector((state)=>state.pokemons.status);
const dispatch = useDispatch();
useEffect(() => {
if (status === 'idle') dispatch(getPokemons());
}, [status, dispatch]);
return (
...
);
}
Inside useEffect
, we’re making the dispatch call if status
is idle
. This condition helps us avoid unnecessary calls. Since getPokemons
is an action creator, we need to call that inside dispatch
function. After adding this code, you should be able to see the list of Pokémon getting displayed in the browser.
Displaying items after API call
Implementing Favorite Button
We will dispatch the toggleFavorite
action inside onClick
function. First, let’s add a simple counter for the favorite list.
// src/App.js
...
function App() {
...
const totalFavorite = pokemons.filter((p) => p.favorite).length;
return (
<Container className="p-2">
<h1>Pokeverse</h1>
<h3>Favorites : ❤ {totalFavorite}</h3>
...
And inside src/components/PokeCard.js
which is responsible for displaying a card for each item, we’ll make the dispatch
call inside handleClick
.
// src/components/PokeCard.js
import React from 'react';
import { Badge, Button, Card, ListGroup } from 'react-bootstrap';
import { useDispatch } from 'react-redux';
import { toggleFavorite } from '../redux/pokemons/pokemons';
const PokeCard = ({ pokemon }) => {
const { id, name, sprites, exp, height, weight, favorite } = pokemon;
const dispatch = useDispatch();
const handleClick = () => {
dispatch(toggleFavorite(id));
};
return (
...
);
}
Since toggleFavorite
needs id
of the item as action.payload
, we need to pass it inside the function call. And lastly, add handleClick
as onClick
function for buttons, and this feature should be working properly.
Conclusion
By using Redux Toolkit, we were able to make Redux logic clearer and more efficient compared to traditional Redux. I really appreciate you taking the time to read through this tutorial and I hope it was helpful in some way.
If there’s anything that was unclear or incorrect in my explanation, please feel free to reach out. Your feedback and suggestions are always appreciated and I would love to hear how I can make this tutorial even better.
Reference
This project is based on Modern Redux with Redux Toolkit, the tutorial created by the Redux team. If you want to gain a deeper understanding of the topic, I recommend reading the official tutorial linked below.