May the State Be with You: A journey to simplify state management in React using Redux Toolkit.

May the State Be with You: A journey to simplify state management in React using Redux Toolkit.

A simple guide on how to implement Redux-Toolkit in a React App.

Introduction:

👋Hello there! 🧑‍💻fellow developers. In this beginner-friendly article, I will explain how the Redux library functions and demonstrate how one can implement the magnificent Redux Toolkit effortlessly to manage complex application states. Feel free to 🏃‍♂️skip to relevant parts.

Prerequisites:

  • 🌐Web Development

  • ⚛️React library

But first, why use Redux?

🕑A long time ago in a galaxy far, far away... developers were building really 🚀cool, 🧿eye-catching web apps using ReactJS. As their apps got more extensive and more complex, keeping track of all the app's states started to get tricky, especially remembering what state each component of the app needed.

For small apps, it was no big deal for devs to set up State Hooks at the top level and pass the state jooks down to child components. But for apps with tons of components, it became a real 🌀headache to keep tabs on all the states, share it between components, and keep track of which component used or changed each piece of state. Moreover, in the case of deeply 🪆nested components, developers are often required to pass states through intermediate levels, leading to unnecessary complexity and inefficiency in the application🆘.

Enter Master Dan Abramov and Master Andrew Clark with their ⚙️invention, Redux. Redux centralises all states in one location and distributes them only to the components that require them. This approach enhances the efficiency of the state management system by minimising redundancy and improving code maintainability⚖️.

How Redux works?

Redux is a state management framework that keeps your app's state in one place. It has a straightforward "one-way flow" for updating the info without the hassle of tangled callbacks everywhere.

One-Way Data Flow:

It consists of three parts:

  1. State: The single source of truth for the App. All data is stored in one place

  2. View: The UI is rendered based on the data in the State. The UI re-renders if the state changes.

  3. Action: The data is the central State changes in response to an event like a button click or timeout.

In the context of Redux, the pattern is implemented in the following way:

Imagine a 🌊mighty river called Dispatch that flows steadily into the vast 🗃Redux Store. We gently place an action object into this swift river Dispatch, and it is then swiftly carried by the currents to the wise Reducer function.

The action object holds two precious things:

  1. 🧳A payload, containing valuable data

  2. 🚏An action type, indicating the nature of the payload

The all-knowing Reducer function 🔬gazes deeply into the action object and based on the Action type, it makes deliberate 📝changes to the Central State. To make the necessary changes, the payload is used or not used, depending on the situation.

Once the State evolves, another river carries the new State to the eager components that relied upon it. The components then 📱 📲 re-render joyfully, displaying the updated state to the user.

The Central State is thus changed in one place and through a single pipeline, removing the peril of uncontrolled and untracked State updates. The One-Way Data Flow in Redux brings peace and order to the application☮️.

How to Implement Redux Toolkit?

Well, we can simply use the Redux library directly, but we have to manually set up a lot of things. Redux Toolkit is the cool and recommended way to do things in React. It's a separate library built on top of Redux for React. It also simplifies a lot of things, thus further 📉reducing boilerplate code.

Let's take a simple React app that has 3 nested components. The grandchild component will have some buttons and input fields, and based on those events, things will be changed in the parent and grandparent components.

Follow the below steps.👇

Step 1: Setup a basic react app, using

npx create-react-app redux-toolkit-demo

Step 2: Install the redux dependencies @reduxjs/toolkit is the main library, and react-redux is an extra library that provides a few things to set up 🚀the whole thing.

npm install @reduxjs/toolkit react-redux

Step 3: Now come to react app’s App.js file and add the following. This will act as our Grandparent component. Notice it is referring to a parent component.

import Parent from "./parent/Parent";

function App() {
  return (
    <div className="App" style={{
      border: '1px solid black',
      height:"50vh",
      margin: '50px',
    }}>
      <h1>Grandparent Component</h1>
    <Parent/>
    </div>
  );
}

export default App;

Step 4: Create to new file parent/Parent.jsx and add the following. As the name suggested it will be our parent component.

import React from 'react'
import Child from './child/Child'

function Parent() {
  return (
    <div style={{
        border: '1px solid black',
        padding: '10px',
        backgroundColor: 'lightblue',
        height: '300px',
    }}>
        <h1>Parent</h1>
        <Child/>
    </div>
  )
}

export default Parent;

Step 5: In the same parent folder create a new file child/Child.jsx and add the following code. It will be our last nested component. It has 2 input fields, these inputs will be used to make some changes in the 👨‍🦳Grandparent and 🧔‍♂️parent components. More on that later.

import React from 'react'

function Child() {
  return (
    <div
      style={{
        backgroundColor: "lemonchiffon",
        height: "100px",
      }}
    >
      <h1>Child</h1>

        <label>
          Grandparent text
          <input />
        </label>
        <label>
          parent color
          <input />
        </label>
        <button>Go</button>
    </div>
  );
}

export default Child;

The app should look like this👇

UI of the App before applying Redux Toolkit.

Now we will implement the Redux Toolkit.

Step 6: Let’s first create reducer functions. In the redux toolkit by convention, reducer functions are kept in 🗂slice files. So, we will create two slice files each one containing one reducer function to change the state for the parent and Grandparent components.

But first, store/store.js

It combines the different reducers and creates the Redux store

import { configureStore } from "@reduxjs/toolkit";
import parent from "./features/parentSlice";
import grandParent from "./features/grandparentSlice";

const centralStore = configureStore({     //Central store
    reducer: {                            //Reducer
        parent,                           //Parent reducer         
        grandParent                       //GrandParent reducer
    }
});

export default centralStore;

store/features/parentSlice.js

Here a Parent slice is created. The state here will form part of the larger central state. An initial state is created; when the 🖼UI first renders, this initial state will be used. Inside the reducers object, we find "setParent". It is an 🚏action type; we can have more than one action type. It takes two parameters: state, referring to its scope state, and 🧳payload, i.e. passed by us when we call dispatch, more on that later. The setParent is changing the colour. And in the end, we are 🚛exporting the reducer for the store and the action for our input component.

The selector will be used by the Parent component to query the state.

import { createSlice } from "@reduxjs/toolkit";

const parentInitialState = {          //Initial state of parent
  parent: "parent",
  color: "lightblue",
};

const parent = createSlice({          //Slice of parent
    name: "parent",                   //Name of slice
    initialState: parentInitialState, //Initial state of parent
    //Reducer function to set the state of parent
    reducers: {    
        setParent: (state, action) => {
            state.color = action.payload;
        }
    }
});

export const { setParent } = parent.actions; //Exporting the action      
export default parent.reducer;               //Exporting the reducer
//Exporting the selector
export const parentColor = state => state.parent.color;

store/features/grandparentSlice.js

import {createSlice} from '@reduxjs/toolkit';
const grandParentInitialState = {   //Initial state of grandParent
    grandParent: 'grandParent',
    text: 'grandParent Text '
}

const grandParent = createSlice({   //Slice of grandParent
    name: 'grandParent',            //Name of slice
    initialState: grandParentInitialState,
    //Initial state of grandParent
    reducers: {
        setGrandparent: (state, action) => {  
    //Reducer function to set the state of grandParent
            state.text = action.payload;
        }
    }
});
//Exporting the action
export const {setGrandparent} = grandParent.actions;
export default grandParent.reducer; //Exporting the reducer
//Exporting the selector
export const grandParentText = state => state.grandParent.text;

After the Redux store is set up, we have to now wrap our application with redux, so that the components can use the redux functionalities.

Step 7: 📖Open your index.js file

Import the following

// It will expose to redux to whole app
import { Provider } from 'react-redux';
//your Redux store
import centralStore from "./store/store"

Then 📨wrap your App with the provider.

<Provider store={centralStore}>
    <App />
</Provider>

Step 8: Now we will 🛠modify our UI components to use Redux

App.js

Here useSelector is used to 📬fetch the state, now we have passed grandParentText to it so that it returns only the text from the State, not the whole state.

import Parent from "./parent/Parent";
import { useSelector } from "react-redux";
import { grandParentText } from "./store/features/grandparentSlice";
function App() {
  const text = useSelector(grandParentText);
  return (
    <div
      className="App"
      style={{
        border: "1px solid black",
        height: "50vh",
        margin: "50px",
      }}
    >
      <h1>Grandparent Component</h1>
      <h2>{text}</h2>
      <Parent />
    </div>
  );
}

export default App;

Parent.jsx

Here useSelector is used to fetch 🚦colour

import React from 'react'
import Child from './child/Child'
import { useSelector } from 'react-redux';
import { parentColor } from "../store/features/parentSlice";
function Parent() {
  const color = useSelector(parentColor);
  return (
    <div
      style={{
        border: "1px solid black",
        padding: "10px",
        backgroundColor: color,
        height: "300px",
      }}
    >
      <h1>Parent</h1>
      <Child />
    </div>
  );
}

export default Parent;

Child.jsx

Here we are importing setParent and setGrandparent reducers and then passing them to the 📇dispatch function with input values.

import React, {useState} from 'react'
import { useDispatch } from 'react-redux';
import { setParent } from '../../store/features/parentSlice';
import { setGrandparent } from '../../store/features/grandparentSlice';
function Child() {
    const [values, setValues] = useState({
        grandparentText: '',
        parentColor: '',
    });
    const dispatch = useDispatch();
    const handleGo = () => {
        dispatch(setParent(values.parentColor));
        dispatch(setGrandparent(values.grandparentText));
    }
  return (
    <div
      style={{
        backgroundColor: "lemonchiffon",
        height: "100px",
      }}
    >
      <h1>Child</h1>

      <label>
        Grandparent text
        <input
          value={values.grandparentText}
          onChange={(e) =>
            setValues({ ...values, grandparentText: e.target.value })
          }
        />
      </label>
      <label>
        parent color
        <input
          value={values.parentColor}
          onChange={(e) =>
            setValues({ ...values, parentColor: e.target.value })
          }
        />
      </label>
      <button onClick={handleGo}>Go</button>
    </div>
  );
}

export default Child;

With that, you have successfully integrated Redux Toolkit into your application. Nicely done! Here is the 🛰GitHub repo for the app in case you want to check it out. Let me know if you have any 🙋‍♂️questions or need any 👨‍🏫help with the steps.

🛍Bonus: Async operation

What if you have to fetch external resources or make an 📡HTTP request before changing the state? You might have heard the terms middleware and thunk. Don’t worry Redux-toolkit got you covered too.

For implementing thunk middleware, some changes have to be made to the slice file, rest remains the same.

Let’s say on an 🧨event trigger, the app will fetch a list of movies from an API and store it in the state.

Step 1: Create a new slice file, moviesSlice.js

Here we are creating a separate async action called fetchMovies. This will fetch our movies list and return the list. We are not creating reducers; instead, we will use “extraReducers”.

When we 🛎fetch, it can be in 3 states: either it starts or is pending, fails or is fulfilled. So, we have three functions to handle each state. You can see we use dot operation with fetchMovies action followed by ⏳pending, ✅fulfilled or ⛔️rejected; they correspond to the former that we discussed. Each function then changes the states accordingly. You can use the status or error keys to 📊display a loading spinner or an error message.

While fetching if some error happens it will automatically trigger rejected extraReducer part.

import { createAsyncThunk } from "@reduxjs/toolkit";
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
    list:[],
    status: "idle",
    error: null
};

export const fetchMovies = createAsyncThunk(
    "movies/fetchMovies",
    async (payload) => {
        const response = await fetch("https://api.themoviedb.org/3/movie/popular?api_key=6c0a6a2a0d2d6b3a6a3e0c0e6d9d9e0a&language=en-US&page=1");
        const data = await response.json();
        return data.results;
    }
);

const moviesSlice = createSlice({
    name: "movies",
    initialState,
    reducers: {},
    extraReducers: (builder) => {
        // Add reducers for additional action types here, and handle     loading state as needed
        builder.addCase(fetchMovies.pending, (state, action) => {
            state.status = "loading";
        });
        // Add reducers for additional action types here, and handle fulfilled state as needed
        builder.addCase(fetchMovies.fulfilled, (state, action) => {
          // Add user to the state array
          state.list.push(action.payload);
        });
        // Add reducers for additional action types here, and handle rejected state as needed
        builder.addCase(fetchMovies.rejected, (state, action) => {
            state.status = "failed";
            state.error = action.error.message;
        });
  },
});

export default moviesSlice.reducer;
export const selectAllMovies = state => state.movies.list;

Step 2: And lastly don’t forget to add your new slice to the Redux store

import { configureStore } from "@reduxjs/toolkit";
import parent from "./features/parentSlice";
import grandParent from "./features/grandparentSlice";
import moviesSlice from "./features/moviesSlice";
const centralStore = configureStore({ //Central store
    reducer: {                                //Reducer
        parent,                              //Parent reducer         
        grandParent,                         //GrandParent reducer
        moviesSlice
    }
});

export default centralStore;

The End

Did you find this article valuable?

Support Debarshi Raj Basumatary by becoming a sponsor. Any amount is appreciated!