DEV Community

Cover image for Redux Demystified
Vedant Lahane
Vedant Lahane

Posted on • Updated on

Redux Demystified

What is Redux?

Redux is a predictable container for JavaScript apps.

Redux is for JavaScript applications

Redux is not tied to React. Can be used with Angular, Vue, or even vanilla JS.

Redux is a state container

Redux stores the state of your application.
Different states in a React application
The state of an application is the state shared by all the individual components of that application.

Redux will store and manage the application state.

Redux is predictable

Redux is a state container and in any JavaScript application, the state of the application can change.

Redux is predictable

In Redux, a pattern is enforced to ensure that all the state transitions are explicit and can be tracked.

Why Redux?

Redux will help you manage the global state of your application in a predictable way.

The patterns and tools provided by Redux make it easier to understand when, where, why & how the state in your application is being updated.

Redux guides you towards writing code that is predictable and testable.

What is Redux Toolkit?

Redux Toolkit is the official, opinionated, batteries-included toolset for efficient Redux development.

It is also intended to be the standard way to write Redux logic in your application.

Why Redux Toolkit?

Redux is great, but it has a few shortcomings:

  • Configuring Redux in an app seems complicated.
  • In addition to Redux, a lot of other packages have to be installed to get Redux to do something useful.
  • Redux requires too much boilerplate code.

Redux Toolkit serves as an abstraction over Redux. It hides the difficult parts ensuring you have a good developer experience.

React-Redux

React-Redux package

Summary of what we’ve learned so far

  • React is a library to build user interfaces.
  • Redux is a library for managing the state in a predictable way in JS apps.
  • Redux Toolkit is a library for efficient redux development.
  • React-Redux is a library that provides bindings to use React and Redux Toolkit together in an application.

Caveats

  • Never learn React and Redux in parallel.
  • “When to use Redux in your application?” Redux helps you deal with shared state management but like any tool, it has certain trade-offs. Pros
  • You have large amounts of application state that are needed in many places in the app.
  • The app state is updated frequently over time.
  • The logic to updating that state may be complex
  • The app has a medium or large-sized codebase and might be worked on by many people. Cons
  • There are more concepts to learn and more code to write.
  • It also adds some indirections to your code and asks you to follow certain restrictions.
  • It’s a trade-off between long-term and short-term productivity.

Prerequisites

React Fundamentals
React Hooks

Getting Started with Redux

  1. Install node.js if you haven’t already. Here’s the link https://nodejs.org/en/
  2. Create a folder learn-redux or any other name on your desktop.
  3. Open the folder in your code editor, preferably Visual Studio Code.
  4. Inside the folder, in your terminal, enter the command npm init --yes This will initialize a package.json file with the default settings. For reference, PS E:\GitHub\learn-redux> npm init --yes
  5. Add redux as a dependency for your project. Enter the command npm-install-redux in your terminal. For reference, PS E:\GitHub\learn-redux> npm install redux
  6. Create an index.js inside your folder.

That’s it! We are all set to get our hands dirty in Redux Toolkit 🚀

Three Core Concepts

  1. A store that holds the state of your application.
  2. An action that describes what happened in the application.
  3. A reducer is what ties the store and actions together. It handles the action and decides how to update the state.

Let’s consider an example of a Cake Store.

  • A store is similar to a cake store in the sense that the cake store has a number of cakes in its store inventory. On the other hand, a redux store has its states in its store.
  • An action is when a customer places an order for a cake. In that case, an order has been placed and the count of the cakes has to be reduced by one.
  • A reducer in our case is a shopkeeper. He receives the order from the customer, which is an action and removes the cake from the shelf which is a store.

Three Principles

  1. First Principle - The global state of your application is stored as an object inside a single store. In simpler terms, maintain our application state in a single object which would be managed by the Redux store.
  2. Second Principle - The only way to change the state is to dispatch an action, an object that describes what happened. Thus, to update the state of your app, you need to let Redux know about that with an action. One should not directly update the state object.
  3. Third Principle - To specify how the state tree is updated based on actions, you write pure reducers. The reducer takes the previous state and an action and returns a new state.

Reducer - (previousState, action) ⇒ newState

Let’s get back to our Cake Shop.

  • Let’s assume we are tracking the number of cakes on the shelf. So our object would look something like this.
// A redux store as per the First Principle

{
    numberOfCakes: 10
}
Enter fullscreen mode Exit fullscreen mode
  • A common action would be scanning the QR code to place an order for a cake. This action would look like the one below.
// A redux action as per the Second Principle

{
  type: 'CAKE_ORDERED'
}
Enter fullscreen mode Exit fullscreen mode
  • A reducer could be a shopkeeper in our case. The shopkeeper performs the action of placing an order and then reduces the cake count. Just like this reducer below.
const reducer = (state = inititalState, action) => {
  switch (action.type) {
        case CAKE_ORDERED:
            return {
                numberOfCakes: state.numberOfCakes - 1
            }
    }
}
Enter fullscreen mode Exit fullscreen mode

Three Principles Overview

Three Principles Overview

Diving deeper into the Three’s

Actions

  • The only way your application can interact with the store.
  • Carry some information from your app to the redux store.
  • Plain Javascript objects.
  • Have a type property that describes something that happened in the application.
  • The type property is typically defined as string constants.
  • An action creator is a function that returns an object.

Reducers

  • Reducers specify how the app’s state changes in response to the actions sent to the store.
  • Reducer is a function that accepts state and action as arguments and returns the next state of the application. (previousState, action) ⇒ newState

Store

  • One store for the entire application.
  • Responsibilities of a Redux store:
    • holds the application state
    • allows access to the state via getState()
    • allows the state to be updated via dispatch(action)
    • registers listeners via subscribe(listener)
    • handles unregistering of the listeners via the function returned by subscribe(listener)

Bind Action Creators

The first argument is an object where we define different action creators.
The second argument is what we want to bind those actions to.

const bindActionCreators = redux.bindActionCreators()

const actionCreatorOne = (paramOne = 1) => {
    return {
        type: "ACTION_ONE",
        payload: paramOne
    }
}

const actions = bindActionCreators({ actionCreatorOne(), actionCreatorTwo() }, store.dispatch)

actions.actionCreatorOne()
actions.actionCreatorTwo()
Enter fullscreen mode Exit fullscreen mode

Although bind action creators are not necessary, redux does bring it along with all of its other packages.

Combine Reducers

const combineReducers = redux.combineReducers

const rootReducer = combineReducers({
    keyOne: // reducerOne,
    keyTwo: // reducerTwo
})

const store = createStore(rootReducer)
Enter fullscreen mode Exit fullscreen mode

combineReducers take an object as an argument. The object has keys as any name and the values as a reducer function.
When we dispatch an action, both the reducers receive that action. The difference is that one of them acts on the action whereas the other just ignore it.
Now by doing what we have just done, each of the reducers is managing its own part of the application global state.
The state parameter is different for every reducer and corresponds to the part of the state it manages.
When your app grows, you can split the reducers into different files and keep them completely independent, managing different features. For example, authReducer, a userReducer, profileReducer, etc.

Immer

In a Redux environment, we learned to never mutate the object state.
Here’s how we achieved the same.

const cakeReducer = (state = initialCakeState, action) => {
  switch (action.type) {
    case CAKE_ORDERED:
      return {
        ...state, // spread operator to make a copy of all the properties
        numberOfCakes: state.numberOfCakes - 1, // only update the desired property
      };
    case CAKE_RESTOCKED:
      return {
        ...state,
        numberOfCakes: state.numberOfCakes + action.payload,
      };
    default:
      return state;
  }
};
Enter fullscreen mode Exit fullscreen mode

In practical applications, the state is more complex with nested levels, and in such situations, updating the state could be troublesome.

Immer simplifies handling immutable data structures.

To install immer enter the npm install immer command in your terminal.

const personalData = {
    name: "Vedant",
    address: {
        street: "123 Main St",
        city: 'Boston',
        state: 'MA',
    }
}

{
    ...personalData,
    address: {
        ...personalData.address,
        street: "789 Main St"
    }
}

produce(personalData, (draft) => {
    draft.address.street = "789 Main St"
})
Enter fullscreen mode Exit fullscreen mode

Middleware

It is the suggested way to extend Redux with custom functionality.

Provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

Middleware is usually used for logging, crashing, reporting, performing asynchronous tasks, etc.

Let’s check out the logger middleware. To use logger , enter the command npm i redux-logger in the terminal.

redux-logger

Log all information related to redux in your application.

const applyMiddleware = redux.applyMiddleware

const reduxLogger = require("redux-logger")
const logger = reduxLogger.createLogger()

const store = createStore(rootReducer, applyMiddleware(logger))
Enter fullscreen mode Exit fullscreen mode

Async Actions

Try recollecting the cake shop scenario again. So, the following were the events occurring in a cake shop.

As soon as an action was dispatched, the state was immediately updated.

Thus, if you dispatch the CAKE_ORDERED action, the numberOfCakes was right away decremented by 1.

Same with the ICECREAM_ORDRERED action as well.
All the above actions were synchronous actions.

Asynchronous actions comprise asynchronous API calls to fetch data from an endpoint and use that data in your application.

What next?

Let’s have our application fetch a list of users from an API endpoint and store the list in a redux store. We already know that there exists the state, the actions, and the reducers as the three main concepts in any redux app.

A typical state in our app would look like,

// State
state = {
    loading: true,
    data: [],
    error: '',
}

// loading - Display a loading spinner in your component
// data - List of users
// error - Display error to the user
Enter fullscreen mode Exit fullscreen mode

Here are some common actions,

// Actions
FETCH_USERS_REQUESTED - // Fetch the list of users
FETCH_USERS_SUCCEEDED - // Fetched successfully
FETCH_USERS_FAILED - // Error when fetching the data
Enter fullscreen mode Exit fullscreen mode

These are the reducers,

// Reducers
case: FETCH_USERS_REQUESTED
            loading: true

case: FETCH_USERS_SUCCEEDED
            loading: false
            users: data // (from API)

case: FETCH_USERS_FAILED
            loading: false
            error: error // (from API)
Enter fullscreen mode Exit fullscreen mode

Redux Thunk Middleware

Let’s learn how to define an asynchronous action creator using axios & redux-thunk .

axios - requests to an API endpoint

redux-thunk - a middleware to define async action creators

Thunk middleware brings to the table the ability for an action creator to return a function instead of an action object.

Also, the function need not be pure. It means that the function can consist of API calls.
It has dispatch method as its arguments and thus can dispatch actions as well.

const redux = require("redux")
const thunkMiddleware = require("redux-thunk").default
const axios = require("axios")
const createStore = redux.createStore
const applyMiddleware = redux.applyMiddleware

const initialState = {
    loading: false,
    users: [],
    error: "",
}

const FETCH_USERS_REQUESTED = "FETCH_USERS_REQUESTED"
const FETCH_USERS_SUCCEEDED = "FETCH_USERS_SUCCEEDED"
const FETCH_USERS_FAILED = "FETCH_USERS_FAILED"

const fetchUsersRequest = () => {
    return {
        type: FETCH_USERS_REQUESTED,
    }
}

const fetchUsersSuccess = users => {
    return {
        type: FETCH_USERS_SUCCEEDED,
        payload: users,
    }
}

const fetchUsersFailure = error => {
    return {
        type: FETCH_USERS_FAILED,
        payload: error,
    }
}

const reducer = (state = initialState, action) => {
    switch(action.type) {
        case FETCH_USERS_REQUESTED:
            return {
                ...state,
                loading: true,
            }
        case FETCH_USERS_SUCCEEDED
            return {
                ...state,
                loading: false,
                users: action.payload,
                error: "",
            }
        case FETCH_USERS_FAILED
            return {
                ...state,
                loading: false,
                users: [],
                error: action.payload,
            }
        default:
            return state
    }
}

const fetchUsers = () => {
    return async function(dispatch) {
        dispatch(fetchUsersRequest())
        try {
            const { data: users } = await axios.get("https://jsonplaceholder.typicode.com/users")
            dispatch(fetchUsersSuccess(users))
        } catch (error) {
            dispatch(fetchUsersFailure(error.message))
        }
    }
}

const store = createStore(reducer, applyMiddleware(thunkMiddleware))
store.subscribe(() => console.log(store.getState()))
store.dispatch(fetchUsers())
Enter fullscreen mode Exit fullscreen mode

Now you might ask, “All this is good. So why Redux Toolkit?”
Below is the answer to your question.

Redux concerns

Redux requires too much boilerplate code.

  • action
  • action object
  • action Creator
  • switch statement in a reducer

A lot of other packages have to be installed to work with Redux.

  • redux-thunk
  • immer
  • redux devtools

Therefore, Redux Toolkit!

Redux Toolkit

Redux toolkit is the official, opinionated, batteries-included toolset for efficient Redux development.

  • abstract over the setup process
  • handle the most common use cases
  • include some useful utilities

Getting started with Redux Toolkit

  1. Create a folder redux-toolkit-demo or any other name on your desktop.
  2. Open the folder in your code editor, preferably Visual Studio Code.
  3. Inside the folder, in your terminal, enter the command npm init --yes This will initialize a package.json file with the default settings. For reference, PS E:\GitHub\learn-redux> npm init --yes
  4. Add redux as a dependency for your project. Enter the command npm i @reduxjs/toolkit in your terminal. For reference, PS E:\GitHub\learn-redux> npm i @reduxjs/toolkit
  5. Create an index.js inside your folder.

Opinionated folder structure for Redux Toolkit

  1. Create an index.js inside your redux-toolkit-demo folder.
  2. Create a folder app inside redux-toolkit-demo.
  3. Create a file store.js inside the app folder. This file will contain code related to our redux store.
  4. Create another folder named features on the same level as the app folder. This folder will contain all features of our application.

And you’re done!

Slice

Group together the reducer logic and actions for a single feature in a single file. And, that file name must contain Slice in its suffix.

The entire application state is split into slices and managed individually.

const createSlice = require("@reduxjs/toolkit").createSlice // ES Module import

const initialState = { 
// initial state object
}

const someSliceName = createSlice({
    name: // any name,
    initialState: // the initial state,
    reducers: {
        // reducer actions
        actionName: (state, action) => {
            state.propertyName = // any value              // Direct state mutation possible
        }
    } 
})

module.exports = someSliceName.reducer                // default export
module.exports.someActionName = someSliceName.actions    // named export
Enter fullscreen mode Exit fullscreen mode
  • createSlice under the hood uses the immer library. Thus, Redux Toolkit handles the state update on our behalf.
  • createSlice will automatically generate action creators with the same name as the reducer function (here, actionName) we have written.
  • createSlice also returns the main reducer function which we can provide to our redux store.
  • createSlice abstracts all of the boilerplate code of writing the action type constants, action object, action creators, and switch cases and also handles immutable updates.

Configuring Store

  • configureStore takes an object as an argument.
  • The object has a key reducer and this reducer is where we specify all the reducers.
const configureStore = require("@reduxjs/toolkit").configureStore; // similar to createStore in redux

const store = configureStore({
  reducer: {
    reducerOneName: // reducerOne,
  },
});
Enter fullscreen mode Exit fullscreen mode

Middleware

const { getDefaultMiddleware } = require("@reduxjs/toolkit");
const reduxLogger = require("redux-logger");

const store = configureStore({
  reducer: {
    reducerOneName: // reducerOne,
    reducerTwoName: // reducerTwo,
  },
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
});
Enter fullscreen mode Exit fullscreen mode

Example of the logger middleware terminal

Initial State { cake: { numberOfCakes: 10 }, icecream: { numberOfIcecreams: 20 } }
 action cake/ordered @ 23:31:25.354
   prev state { cake: { numberOfCakes: 10 }, icecream: { numberOfIcecreams: 20 } }
   action     { type: 'cake/ordered', payload: undefined }
   next state { cake: { numberOfCakes: 9 }, icecream: { numberOfIcecreams: 20 } }
 action cake/ordered @ 23:31:25.357
   prev state { cake: { numberOfCakes: 9 }, icecream: { numberOfIcecreams: 20 } }
   action     { type: 'cake/ordered', payload: undefined }
   next state { cake: { numberOfCakes: 8 }, icecream: { numberOfIcecreams: 20 } }
 action cake/ordered @ 23:31:25.359
   prev state { cake: { numberOfCakes: 8 }, icecream: { numberOfIcecreams: 20 } }
   action     { type: 'cake/restocked', payload: 2 }
   next state { cake: { numberOfCakes: 10 }, icecream: { numberOfIcecreams: 20 } }
Enter fullscreen mode Exit fullscreen mode

The type property has a slice name as the first part and the key of each reducer function as the second part, separated by a “/”.
Thus, cake is a slice name and there are reducer functions ordered & restocked .

Async Actions

  • Asynchronous actions in RTK are performed using createAsyncThunk method.
  • createAsyncThunk method has two arguments.
  • The first argument is the action name.
  • The second argument is a callback function that creates the payload.
  • createAsyncThunk automatically dispatches lifecycle actions based on the returned promise. A promise has pending, fulfilled or rejected. Thus, createAsyncThunk returns a pending, fulfilled or rejected action types.
  • We can listen to these actions types by a reducer function and perform the necessary state transitions.
  • The reducers though are not generated by the slice and have to be added as extra reducers.
const createSlice = require("@reduxjs/toolkit").createSlice;
const createAsyncThunk = require("@reduxjs/toolkit").createAsyncThunk;
const axios = require("axios");

const initialState = {
  loading: false,
  users: [],
  error: "",
};

//Generates pending, fulfilled and rejected action types.
const fetchUsers = createAsyncThunk("user/fetchUsers", () => {
  return axios
    .get("https://jsonplaceholder.typicode.com/users")
    .then((response) => response.data.map((user) => user.id));
});

// example - a simple user slice
const userSlice = createSlice({
  name: "user",
  initialState,
  extraReducers: (builder) => {
    builder.addCase(fetchUsers.pending, (state) => {
      state.loading = true;
    });
    builder.addCase(fetchUsers.fulfilled, (state, action) => {
      state.loading = false;
      state.users = action.payload;
      state.error = "";
    });
    builder.addCase(fetchUsers.rejected, (state, action) => {
      state.loading = false;
      state.users = [];
      state.error = action.error.message;
    });
  },
});

module.exports = userSlice.reducer;
module.exports.fetchUsers = fetchUsers;
Enter fullscreen mode Exit fullscreen mode

React Redux setup

  1. Create a react project Now, we could also use create-react-app but let’s try this new frontend tooling library vite.
  2. Inside the root folder, in your terminal, enter the command npm create vite@latest project-name This will initialize a react app named project-name .
  3. Make the terminal point to the react project directory by entering the command cd project-name in the terminal.
  4. Inside the folder, in your terminal, enter the command npm install This will install all the required packages in package.json file in your app.
  5. Copy & Paste the app and features folders from your redux-toolkit-demo folder into the src sub-folder of the newly created react app.
  6. Install the required dependencies - axios , createSlice , createAsyncThunk
  7. Start the server by entering the command npm run dev

Provider

  • Install the react-redux package in your folder. Enter the following command npm i react-redux
  • Restart the server by entering the command npm run dev .
  • We need to make available the store to the react app component tree. This is where the react-redux library comes into the picture.
  • react-redux library exports a component called provider .
  • Firstly import the provider component from react-redux library Like this,
// main.jsx

import { Provider } from "react-redux
import store from "./app/store"

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Provider>
      <App />
    </Provider>
  </React.StrictMode>
)
Enter fullscreen mode Exit fullscreen mode
  • It is very important to note that the Provider component should be present at the top of all the components. Thus the props store is provided to every component in the app.
  • This is because the Provider component uses React Context under the hood.

useSelector

  • The useSelector hook is used to get hold of any state that is maintained in the redux store.
  • It is sort of a wrapper around store.getState()
// CakeView.jsx

import React from "react"
import { useSelector } from "react-redux"

export const CakeView = () => {
  const numberOfCakes = useSelector((state) => state.cake.numberOfCakes)
  return (
    <div>
        <h2>Number of Cakes - {numberOfCakes}</h2>
        <button>Order cake</button>
        <button>Restock cakes</button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

useDispatch

  • The useDispatch hook is used to dispatch an action in React-Redux.
  • The hook returns a reference to the dispatch function from the redux store.
// IcecreamView.jsx

import React from "react"
import { useState } from "react"
import { useSelector, useDispatch } from "react-redux"
import { ordered, restocked } from "./icecreamSlice"

export const IcecreamView = () => {
  const [value, setValue] = useState(1)
  const numberOfIcecreams = useSelector((state) => state.icecream.numberOfIcecreams)
  const dispatch = useDispatch()

  return (
    <div>
        <h2>Number of icecream - {numberOfIcecreams} </h2>
        <button onClick={() => dispatch(ordered())}>Order cake</button>
        <input type="number" value={value} onChange={(e) => setValue(parseInt(e.target.value))}/>
        <button onClick={() => dispatch(restocked(value))}>Restock icecream</button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
// UserView.jsx

import React, {useEffect} from "react"
import { useSelector, useDispatch } from "react-redux"
import { fetchUsers } from "./userSlice"

export const UserView = () => {
  const user = useSelector((state) => state.user)
  const dispatch = useDispatch()
  useEffect(() => {
    dispatch(fetchUsers())
  }, [])

  return (
    <div>
        <h2>List of Users</h2>
        {user.loading && <div>Loading...</div>}
        {!user.loading && user.error ? <div>Error: {user.error}</div> : null}
        {!user.loading && user.users.length ? (
          <ul>
            {user.users.map(user => (
              <li key={user.id}>{user.name}</li>
            ))}
          </ul>
        ) : null}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

That's all!

There's a simple analogy I believe in & I'd like to share it with y'all.

  • The curiosity that lead you to search for something is the first but I'd say the most important part. Remember, you are that time old when you started learning something say XYZ.
  • Consuming the learning material (a blog, a video, or some documentation, etc.) effectively is the next important step.
  • The application part of learning something is the one that the majority fails at.

I can't emphasize more on how important it is to apply the learnings. So, after learning Redux, I made a social-media app wherein I used Redux Toolkit.
Live: https://jurassic-world.netlify.app
GitHub Repo: https://github.com/MarkVed17/jurassic-world

Dropping my repository link whilst I started with Redux.
https://github.com/MarkVed17/learn-redux

Now, if you are someone who has already stepped in the React ecosystem for a while now, you might've come across the React Context API versus Redux for state management. There's a lot of ground to cover this one. So, let's keep that topic of debate for some other day.

Until then, Keep learning! Keep growing! 😎

Let's connect on LinkedIn & Twitter.

Resources

Assets Credits

Top comments (0)