DEV Community

Cover image for Using Redux Toolkit for State Management in React
CheriseFoster
CheriseFoster

Posted on

Using Redux Toolkit for State Management in React

If you're building a large and complex application, it's no question that trying to manage state can get tricky and redundant. Insert: Redux. Redux acts as a state container for JavaScript applications so that state can be managed in separate files and accessed in your React components. However, Redux can be complicated, which is why Redux Toolkit was introduced to make managing state easier and simpler, reducing boilerplate code and hiding the difficult parts of Redux to ensure a good developer experience. If you've never used Redux before, there is a learning curve, but this blog is here to break down the basics.

But first, why would I use Redux?
A few reasons to use Redux would include: if you have large amounts of state in your application that are needed in various places, if your state is updated frequently over time, or if your application has a medium to large-sized codebase and might be worked on by many people.


Core Concepts

There are 3 core concepts in Redux:

-Store: This is what holds the state of your application. It allows access to state via getState(),and allows state to be updated via dispatch(action).
-Action: Actions describe what happened. They are the only way your app can interact with the store. Actions must have a "type" property that describes something that happened in the app.
-Reducers: Reducers tie the store and actions together by specifying how the app's state changes in response to actions sent to the store. In simpler terms, it handles the action and describes how to update state.


Redux Principles

  1. The global state of your application is stored as an object inside a single store. You can maintain your application state in a single object to be managed by the Redux store.
  2. To update the state of your application, you need to let Redux know about that with an action. The state object is not allowed to be updated directly, meaning it is immutable. But don't worry, Redux handles this for you by making a copy of the current state.
  3. To specify how the state tree is updated based on actions, you write pure reducers (i.e. functions). Reducers take the previous state coupled with an action and return the next state. reducer = (previousState, action) => newState

Let's Get Coding

To get started, make sure you have redux toolkit installed:
npm install @reduxjs/toolkit react-redux

I created an application with a sign up/log in feature. In my src folder lies my components folder with all of my React components, and another folder I named redux. Each file under the redux folder contains state for various parts of my application. The first file to create in your redux folder should be the store.js file.

//store.js

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

export default configureStore({
    reducer: {

    }
})
Enter fullscreen mode Exit fullscreen mode

The configureStore import creates the Redux store from Toolkit and is a way to pass in all of our reducers. When we make a Redux "slice", which is your reducer logic and actions for a single feature in your app (or your state for a particular feature), we will pass that into the store reducer.

To keep things simple and organized, I like to put the word "slice" into each of my slice file names. I created my authentication file under my redux folder and named it authSlice.js.

In authSlice.js:

Import createSlice from the Redux Toolkit library, this allows us to write our logic easier and with less boilerplate:

//authSlice.js

import { createSlice } from '@reduxjs/toolkit'

const authSlice = createSlice({
  name: 'auth',
  initialState: {
    user: null,
    isAuthenticated: false,
    isLoading: false,
    error: null,
  },
  reducers: {

  }
});

Enter fullscreen mode Exit fullscreen mode

const authSlice = createSlice({...}) creates a single slice of the Redux state.
In the next line, "name: 'auth'", we give a name to the slice which is referenced back in the store.js file as "auth: authReducer." The name can be whatever you want but keep it simple and easy.

We need to define an initial state for the slice, similar to defining your initial state when using state in a React component. If I were to just use state inside my component instead of Redux, it might look like this:

function AuthenticationComponent() {
  const [user, setUser] = useState(null);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
Enter fullscreen mode Exit fullscreen mode

As you can see, there is much less code using Redux! Let's move on to the reducer object:

//authSlice.js

reducers: {
    loginStart: (state) => {
      state.isLoading = true;
    },
    loginSuccess: (state, action) => {
      state.isLoading = false;
      state.isAuthenticated = true;
      state.user = action.payload;
      state.error = null;
    },
    loginFailure: (state, action) => {
      state.isLoading = false;
      state.error = action.payload;
    },
    logout: (state) => {
      state.isAuthenticated = false;
      state.user = null;
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

It's common practice in Redux to use Start (or often "Begin"), Success, and Failure (and sometimes Update) in the reducer object. It's not necessary, but it helps to handle the different states of async request within a reducer.

Actions and Payload

An action is a plain JS object that has a 'type' property which indicates the action that's being performed (logging in) and usually a 'payload' field which contains any data that should be used to update the state.

  • 'loginStart': Takes the current state and marks the start of the login process by setting the state.isLoading to true.

  • 'loginSuccess': Takes the current state and an action. It sets isLoading to false as the login attempt has finished, updates isAuthenticated to true indicating a successful login, sets user to the payload containing user data, and clears any errors by setting error to null.

  • 'loginFailure': Sets isLoading to false since the login attempt has concluded, but an error occurred, and then sets error to the payload, which should contain information about what went wrong.

  • 'logout': Sets isAuthenticated to false and logging the user out, and resets the user to null, clearing any stored user information.

The logout action doesn't require a payload since there's no need for additional data to perform these operations. The type of the action (which, thanks to Redux Toolkit, is automatically generated) is all the reducer needs to update the state accordingly.

Finally, we need to be able to use this code in other files, so we need to export it:

//authSlice.js

export const { 
  loginStart, 
  loginSuccess, 
  loginFailure, 
  logout,
} = authSlice.actions;
export default authSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

The export const {...} line includes all of our functions that return action objects when called. The reducer function authSlice.reducer is exported as the default export of the module. This reducer is what we would include in our store (store.js) configuration to handle state transitions based on the actions dispatched.

Make sure you import the reducer slice in your store.js file after you create it!

//store.js

import { configureStore } from "@reduxjs/toolkit";
import authReducer from './authSlice';

export default configureStore({
    reducer: {
        auth: authReducer
    }
})
Enter fullscreen mode Exit fullscreen mode

You need a way to provide the Redux store to all of the components in your application that need to access the state or actions. To do this, use the "Provider" component from the react-redux library. "Provider" is a higher-order component that takes your Redux store as a prop and allows the rest of you application access to it.

In your index.js file:

//index.js

import App from "./components/App";
import store from "./redux/store";
import { Provider } from "react-redux";

const container = document.getElementById("root");
const root = createRoot(container);
root.render(
    <Router>
      <Provider store={store}>
        <App />
      </Provider>
    </Router>
  );

Enter fullscreen mode Exit fullscreen mode

You can now successfully deploy your Redux state into whatever components of your application you need. In my Authentication component, I deployed the loginStart, loginSuccess, and loginFailure functions. Import the useDispatch hook provided by the react-redux library so you can dispatch actions to the store from within your React components:

I used formik in this component to handle the sign in form:

//AuthenticationForm Component

import { useDispatch, useSelector } from 'react-redux';
import { loginStart, loginSuccess, loginFailure } from "../redux/authSlice";

function AuthenticationForm() {
    const dispatch = useDispatch();

const signInFormik = useFormik({
        {code}...
        },
        validationSchema: signInSchema,
        onSubmit: (values, { ... }) => {
            dispatch(loginStart()); // Dispatch start action
            fetch('/api/login', {
                {code}...,
            }).then((res) => {
                  {code}...
                }
            }).then((user) => {
                dispatch(loginSuccess(user)); // Dispatch success action
                navigate('/home');
            }).catch((error) => {
                dispatch(loginFailure(error)); // Dispatch failure action
                setErrors({ ... });
            });
        }
    });

Enter fullscreen mode Exit fullscreen mode

Conclusion

Redux Toolkit provides a simpler way to use Redux that helps you avoid boilerplate code and enforces best practices. By structuring your application's state management with slices and utilizing the other tools that Redux Toolkit offers, you can write more efficient, easier-to-understand code.

Our example of an auth slice is just the tip of the iceberg. There's much more you can do with Redux Toolkit, including creating more complex slices, handling side effects with thunks or sagas, and optimizing re-renders with memoized selectors.

//full code for authSlice.js

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

const authSlice = createSlice({
  name: 'auth',
  initialState: {
    user: null,
    isAuthenticated: false,
    isLoading: false,
    updateError: null,
    error: null,
  },
  reducers: {
    loginStart: (state) => {
      state.isLoading = true;
    },
    loginSuccess: (state, action) => {
      state.isLoading = false;
      state.isAuthenticated = true;
      state.user = action.payload;
      state.error = null;
    },
    loginFailure: (state, action) => {
      state.isLoading = false;
      state.error = action.payload;
    },
    logout: (state) => {
      state.isAuthenticated = false;
      state.user = null;
    },
  },
});

export const { 
  loginStart, 
  loginSuccess, 
  loginFailure, 
  logout
} = authSlice.actions;
export default authSlice.reducer;
Enter fullscreen mode Exit fullscreen mode


Resources: https://redux.js.org/
img src

Top comments (0)