DEV Community

Cover image for Getting Started with Redux and Redux Toolkit
Hajar | هاجر
Hajar | هاجر

Posted on

Getting Started with Redux and Redux Toolkit

What is Redux?

As the official documentation states: Redux is a pattern and library for managing and updating global application state. It serves as a centralized store for state that needs to be used across your entire application, with rules ensuring that the state can only be updated in a predictable fashion (without causing side effects or unexpected behaviors).

So Redux works as your application’s global state manager, ensuring that every change in the state goes through a specific and organized process.

Think of the state as a JavaScript object that needs to be shared across all of your app’s components.

If you have a user object like this:

const user = {
  username: "Jane Doe",
  email: "email@example.com",
};
Enter fullscreen mode Exit fullscreen mode

Sharing this object with every component can quickly get messy if you manually pass it down through your app’s component tree. This is where Redux comes in handy.

How Redux Works

Redux has its own set of terms and rules that each component should follow to access or update the data. The most basic ones are actions and dispatch.

An action in Redux describes what a component wants to do with the state. For example:

  • Does it want to update the user’s username?
  • Change their email?
  • Clear the user’s data when they log out and that data isn't needed anymore?

These tasks can be transformed into action types that Redux can understand and process.

Defining Actions

Actions are simple JavaScript objects that contain a type (a string that identifies the action) and optionally a payload (data to update the state).

Here are some example actions:

const emailUpdated = {
  type: "user/emailUpdated",
  payload: "email@main.co",
};

const usernameUpdated = {
  type: "user/usernameUpdated",
  payload: "Hajar",
};

const clearUserData = {
  type: "user/clearUserData",
};
Enter fullscreen mode Exit fullscreen mode

With these defined, components can use them to specify what kind of changes they want to make to the state.

Now the components know which actions they need to perform and what data/payload to include with them, so they need to dispatch them to the Redux store.

The Dispatch Function: Sending Actions to Redux

The dispatch function is how actions are sent to the Redux store. It’s a regular JavaScript function, the only differences are:

  • Redux knows exactly what the dispatch function is and what to do when it's called within a component.
  • The function accepts action objects, which must include a type (a string that identifies the action).

When you call dispatch with an action, Redux processes it and then triggers the right reducer that handles it.

What Are Reducers?

A reducer is a Redux function that takes the current state and an action, determines how to update the state based on the action type, and then returns the updated one.

For example, if a reducer receives an action like user/usernameUpdated, it modifies the username in the state with the value from the action’s payload. Once the reducer updates the state, Redux notifies the relevant components to re-render.

Et voilà! The component now has the updated user data, without having to worry about the steps Redux took to make it happen.

An example of how a reducer could look:

const ACTION_TYPES = {
  UPDATE_USERNAME: "user/usernameUpdated",
  UPDATE_EMAIL: "user/emailUpdated",
  CLEAR_STATE: "user/userDataCleared",
};

const initialState = {
  username: "",
  email: "",
};

const userReducer = (state = initialState, action) => {
  switch (action.type) {
    case ACTION_TYPES.UPDATE_USERNAME:
      return {
       // spread the state to return a new state object 
        ...state,
        username: action.payload,
      };
    case ACTION_TYPES.UPDATE_EMAIL:
      return {
        ...state,
        email: action.payload,
      };
    case ACTION_TYPES.CLEAR_STATE:
      return initialState;
    default:
      return state;
  }
};

export default userReducer;
Enter fullscreen mode Exit fullscreen mode

And this is how the component would dispatch the actions:

const updateUsername = (name) => {
  dispatch({
    type: "user/usernameUpdated",
    payload: name,
  });
};

const updateEmail = (email) => {
  dispatch({
    type: "user/emailUpdated",
    payload: email,
  });
};

const clearUserData = () => {
  dispatch({
    type: "user/userDataCleared",
  });
};
Enter fullscreen mode Exit fullscreen mode

That's cool and all but have you noticed how much code we've written for such a simple reducer example?

  • We had to define all the actions and make sure they were properly used in the components.
  • We had to create a switch case block for each action.
  • We had to remember to spread the previous state before updating its email or username. (this is to copy the old state to a new object first and return the new one -- reducers need to be pure functions).

So you can imagine how much more we'd need to write to introduce reducers and actions in a real-world application. This is where Redux ToolKit comes in very handy.

Using Redux Toolkit with Next.js

To use Redux Toolkit in an Next.js project, we need to install Next.js:

npx create-next-app@latest
Enter fullscreen mode Exit fullscreen mode

Then, install @reduxjs/toolkit and react-redux:

npm install @reduxjs/toolkit react-redux
Enter fullscreen mode Exit fullscreen mode

Configuring the Redux Store
Next, create a file app/redux/store.ts to configure the Redux store. we'll use the configureStore API from Redux Toolkit.

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

export default configureStore({
  // The reducer will be added here later
  reducer: {}
});
Enter fullscreen mode Exit fullscreen mode

Wrapping the Application with Redux Provider

To make your Redux store accessible throughout your application, wrap your root component with the Provider component from react-redux and pass the store to it.

"use client";

import { Provider } from "react-redux";
import store from "@/app/redux/store";
import MainContent from "@/app/components/MainContent";

export default function Home() {
  return (
    <Provider store={store}>
      <MainContent />
    </Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Setting Up a Slice with Redux Toolkit

In Redux Toolkit, a slice manages the state and reducers. To create a slice, use the createSlice function from @reduxjs/toolkit.

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

const initialState = {
  username: "",
  email: "",
};

export const userSlice = createSlice({
  name: "user",         
  initialState,       
  reducers: {           
    usernameUpdated: (state, action) => {
      state.username = action.payload;
    },
    emailUpdated: (state, action) => {
      state.email = action.payload;
    },
    userDataCleared: (state) => {
      state.username = "";
      state.email = "";
    },
  },
});

export default userSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

The main arguments that createSlice accepts are:

  • name: A unique name for the slice.
  • initialState: The initial state of the slice.
  • reducers: the reducer functions to handle updating the state.

Note: Have you noticed how we're directly modifying the state here? In createSlice, we can use that syntax to simplifying the logic of updating the state - specially for the deeply-nested values. However, under the hood this is what it happens:

Redux Toolkit allows us to write "mutating" logic in reducers. It doesn't actually mutate the state because it uses the Immer library, which detects changes to a "draft state" and produces a brand new immutable state based off those changes.

But wait, how do we check for action types and map them to the reducers like we did in the previous code? Here’s the cool part: you don't need to! Once you define the reducers, Redux Toolkit automatically generates the action creators for each one, and we can easily export them from the slice file like this:

// app/redux/userSlice.ts
import { createSlice } from "@reduxjs/toolkit";


const initialState = {
  username: "",
  email: "",
};


export const userSlice = createSlice({
  name: "user",
  initialState,
  reducers: {
    usernameUpdated: (state, action) => {
      state.username = action.payload;
    },
    emailUpdated: (state, action) => {
      state.email = action.payload;
    },
    userDataCleared: (state) => {
      state = initialState;
    },
  },
});


// the actions are exported to be used by any component now
export const { usernameUpdated, emailUpdated, userDataCleared } =
  userSlice.actions;


export default userSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

Then we can update the store with the userSlice.reducer:

import { configureStore } from "@reduxjs/toolkit";
import userReducer from "./userSlice";

export default configureStore({
  reducer: {
    user: userReducer,
    // we can inroduce more reducers from different slices here
    // posts: postsReducer
  },
});
Enter fullscreen mode Exit fullscreen mode

Using Selectors and Dispatching Actions

Now that the store and slice are set up, we can use selectors to get the data and dispatch actions to update it.

The selectors are to access the state data and make them available to the application's components. This is how to create and export them in the userSlice file:

export const selectUsername = (state) => state.user.username;
export const selectEmail = (state) => state.user.email;
Enter fullscreen mode Exit fullscreen mode

Now we can use the selector data and dispatch actions:

import { useSelector, useDispatch } from "react-redux";
import { selectUsername, selectEmail } from "@/redux/userSlice";
import { usernameUpdated, emailUpdated, userDataCleared } from "@/redux/userSlice";

const UserComponent = () => {
  const username = useSelector(selectUsername);
  const email = useSelector(selectEmail);
  const dispatch = useDispatch();

  const handleUpdateUsername = (name: string) => dispatch(usernameUpdated(name));
  const handleUpdateEmail = (email: string) => dispatch(emailUpdated(email));
  const handleClearUserData = () => dispatch(userDataCleared());

  return (
    <div>
      <h2>Username: {username}</h2>
      <p>Email: {email}</p>
      <button onClick={() => handleUpdateUsername("new_username")}>Update Username</button>
      <button onClick={() => handleUpdateEmail("new_email@example.com")}>Update Email</button>
      <button onClick={handleClearUserData}>Clear User Data</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Top comments (0)