How To Simplify Redux Logic With Redux Toolkit

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.

Screenshot of final version

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.

Screenshot of starting layout

Basic layout for the website

Inside src/components/ folder, there’s only one component called PokeCardthat 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 = () =&gt; axios.get('https://pokeapi.co/api/v2/pokemon');

const getPokeDetail = (url) =&gt; 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 .

createAsyncThunkneeds two parameters:

  1. A string that will be used as a prefix for generated action types

  2. 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.

  1. getPokemons.pending : pokemons/get/pending

  2. getPokemons.fullfilled : pokemons/get/fullfilled

  3. 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:

  1. 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.

  2. It automatically creates action types and action creators from each case reducer function.

  3. It automatically returns the current state in the default case.

The arguments for createSlice are:

  1. name : a string that will be used as a prefix for generated action types

  2. initialState : an initial state of the reducer

  3. reducers : an object in which keys are strings and values are case reducer functions

  4. extraReducers : this function listens for action types defined outside createSlice (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 idand 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.

Webpage showing items from the API

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.