Back

Do You Really Need Redux? - Pros and Cons of this State Management Library

 Do You Really Need Redux? - Pros and Cons of this State Management Library

Redux is a pattern and library for managing and updating application state, using events called “actions”. In other words, the Redux pattern provides state management for JavaScript apps. You can use the Redux library with any frontend framework, such as React, Angular, or even Vue. In the end, you can implement the Redux pattern in any vanilla JS application.

This article covers what Redux is, why you need it, how it works, the benefits, and when not to use Redux. To make Redux clearer, we’ll update you with code examples to make the concept easier to grasp. Let’s get started!

What is Redux?

As mentioned in the introduction, Redux is a pattern that facilitates state management. It allows you to maintain a predictable state container for your JavaScript apps. This is important for consumer-facing applications where the interface changes based on user input.

On top of that, Redux prevents race conditions where two components simultaneously try to update the state. It accomplishes this task by defining actions that get dispatched to reducers.

Each action contains a type (also seen as an identifier) and a payload. Next, a reducer accepts the action and changes the state based on the received action type and payload.

Reducers are pure functions, which means they are predictable. A pure function returns the same output for the same input. You can use reducers to generate a new application state.

Lastly, to notify our interface that the application state has changed, we can subscribe to data changes. Whenever the application state changes, we update the UI.

It’s a simple, yet elegant solution to facilitate predictable state management for small and large applications. Luckily, most popular frameworks offer support for Redux. Here’s a quick overview:

  • React -> react-redux: You can add Redux to your React application by installing the react-redux dependency.

  • Angular -> @ngrx/store or @angular-redux/store: Both options work well to implement Redux into your Angular application. According to npmtrends.com, @angular-redux/store is the most popular library in terms of weekly downloads.

  • Vue -> vuejs-redux: This dependency offers a light-weight implementation of Redux for Vue with only 45 lines of code and no dependencies. It provides the same API as the react-redux dependency.

Next, why should you use Redux?

Why use Redux?

Instead of directly exploring examples, let’s learn more about the problem Redux solves.

As with many applications, they start small. Imagine a pyramid structure of seven components where each component as two child components. Each component manages its state. However, situations occur where we have to share a state with a child component or a child component wants to modify the parent component’s state.

Do you see the problem? While our application grows to a higher number of components, maintaining data consistency becomes a hairy challenge. It’s not an easy task to manage each component’s state while sharing it with many other components. You’ll likely experience data inconsistency bugs, a fearsome nightmare for frontend developers.

without Redux vs. with Redux Image source: Codecentric blog

As shown in the image, Redux takes away the responsibility from individual components to manage a state. Instead, we create a single store that handles our state management. On top of that, all communication regarding reading, updating, or creating data happens via the store. It prevents data inconsistency bugs from appearing. Moreover, components can listen to state changes to update the UI and avoid these data inconsistency bugs.

Lastly, you can install the Redux DevTools that give you insights into your application’s current state to simplify debugging or testing your application. It’s a great incentive to get started with Redux.

Next, let’s explore Redux with code examples.

Redux with code examples - How does it work?

Let’s recap the Redux cycle before we take a look at our code example. This is how the Redux cycle looks like:

  1. Users interact with the interface and triggers an action
  2. Action with/without payload is sent to a reducer using the dispatcher
  3. Reducer checks if it handles the action and produces a new state based on the action and its payload
  4. State changes are notified via subscription methods
  5. UI renders again based on state changes received via the subscription method

Now, let’s explore how this works using code. We’ll create a simple webpage that allows you to increase or decrease a counter in the state using plus and minus buttons. We’ll use a single index.html document that contains a script tag with all the necessary code.

You can find the completed code via CodeSandbox.io.

Step 1: Exploring index.html

First, let’s create an index.html document with the following HTML setup. This will render the current counter value and buttons to increase or decrease the counter.

<!DOCTYPE html>
<html>
  <head>
    <title>Redux basic example</title>
    <script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
  </head>
  <body>
    <div>
      <p>
        Counter: <span id="count">0</span>
        <button id="increment">+</button>
        <button id="decrement">-</button>
      </p>
    </div>
    <script>
    </script>
  </body>
</html>

Next, let’s take a look at how we can define actions.

Step 2: Define Redux actions

Let’s define actions we want to dispatch to the reducer when the user clicks the increase or decrease button.

We can listen for the click event and dispatch a new action to the Redux store, which contains the reducer.

Make sure to dispatch an object that contains the type property. This property contains the name of the action. As a best practice, use the format <reducer-name>/<action>. This makes it easier to identify actions as multiple components can send the same action. In our example, we will name the reducer counter. Therefore, we get the following actions:

  • counter/increment
  • counter/decrement
<script>
      document
        .getElementById("increment")
        .addEventListener("click", function () {
          store.dispatch({ type: "counter/increment" });
        });

      document
        .getElementById("decrement")
        .addEventListener("click", function () {
          store.dispatch({ type: "counter/decrement" });
        });
</script>

Next, let’s define the reducer.

Step 3: Define a reducer

As we can dispatch multiple actions to the reducer, we’ll use a switch statement to handle the different actions.

First, we define the initial state for the application by setting the count equal to zero. Next, we define a counterReducer function that accepts the current state and the dispatched action.

Two scenarios are possible here:

  1. Reducer receives an counter/increment action to increase the counter
  2. Reducer receives an counter/decrement action to decrease the counter

Note that we use the reducer function as an argument for the createStore function to define a new Redux store for our application.

<script>
      // Define an initial state for the app
      const initialState = {
        count: 0
      };

      // Create a "reducer" function that determines what the new state
      // should be when something happens in the app
      function counterReducer(state = initialState, action) {
        switch (action.type) {
          case "counter/increment":
            return { ...state, count: state.count + 1 };
          case "counter/decrement":
            return { ...state, count: state.count - 1 };
          default:
            // If the reducer doesn't care about this action type,
            // return the existing state unchanged
            return state;
        }
      }
</script>

Tip: Don’t forget to add a default clause to your switch statement that returns the current state. You may have multiple reducers for your application. When you dispatch an action, the action is sent to all reducers and not only the intended one. Therefore, you want all reducers to return the current state to avoid errors and only the intended reducer to return the updated state.

Step 4: Subscribing to state changes

In this step, we want to subscribe to state changes to update the UI when the state has changed.

We define a render method that retrieves the current state and renders the count property. Further, we pass this render function as an argument to the store.subscribe method to update the UI automatically when the state changes. This method exposed by our newly created store will call the render function when the state has changed.

<script>
      // Our "user interface" is some text in a single HTML element
      const countEl = document.getElementById("count");

      // Whenever the store state changes, update the UI by
      // reading the latest store state and showing new data
      function render() {
        const state = store.getState();
        countEl.innerHTML = state.count.toString();
      }

      // Update the UI with the initial data
      render();

      // And subscribe to redraw whenever the data changes in the future
      store.subscribe(render);
</script>

Make sure to take a look at the completed code. You can use the CodeSandbox to play with the code yourself or fork the example.

<!DOCTYPE html>
<html>
  <head>
    <title>Redux basic example</title>
    <script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
  </head>
  <body>
    <div>
      <p>
        Counter: <span id="count">0</span>
        <button id="increment">+</button>
        <button id="decrement">-</button>
      </p>
    </div>
    <script>
      const initialState = {
        count: 0
      };

      function counterReducer(state = initialState, action) {
        switch (action.type) {
          case "counter/increment":
            return { ...state, count: state.count + 1 };
          case "counter/decrement":
            return { ...state, count: state.count - 1 };
          default:
            return state;
        }
      }

      const store = Redux.createStore(counterReducer);

      const countEl = document.getElementById("count");

      function render() {
        const state = store.getState();
        countEl.innerHTML = state.count.toString();
      }

      render();

      store.subscribe(render);

      document
        .getElementById("increment")
        .addEventListener("click", function () {
          store.dispatch({ type: "counter/increment" });
        });

      document
        .getElementById("decrement")
        .addEventListener("click", function () {
          store.dispatch({ type: "counter/decrement" });
        });
    </script>
  </body>
</html>

That’s it!

What are the benefits of using Redux?

There are many benefits to using Redux. The most prominent benefit is improved state management for your application. Yet, there are many other benefits.

  1. Easy debugging and testing. You can use the Redux DevTools or log the state to understand better what’s happening in your application.

  2. Reducer functions can be tested quickly. As reducer functions are pure functions, they produce the same output for the same input. Therefore, testing pure functions become a simple task.

  3. Hook monitoring tools. You can hook monitoring tools to your application’s state to monitor the state in real-time. This improves visibility for your application and allows you to track different metrics.

  4. Predictable outcome. Every action produces a predictable outcome. Your state store acts as a single source of truth. Therefore, you can avoid data inconsistency bugs and don’t have to worry about data synchronization issues between components.

When not to choose Redux?

For beginners, it’s an obvious choice to opt for Redux. Yet, you don’t always need Redux to manage the state of your application.

Applications that consist of mostly simple UI changes most often don’t require a complicated pattern like Redux. Sometimes, old-fashioned state sharing between different components works as well and improves the maintainability of your code.

Also, you can avoid using Redux if your data comes from a single data source per view. In other words, if you don’t require data from multiple sources, there’s no need to introduce Redux. Why? You won’t run into data inconsistency problems when accessing a single data source per view.

Therefore, make sure to check if you need Redux before introducing its complexity. Although it’s a reasonably efficient pattern that promotes pure functions, it might be an overhead for simple applications that involve only a couple of UI changes. On top of that, don’t forget that Redux is an in-memory state store. In other words, if your application crashes, you lose your entire application state. This means that you have to use a caching solution to create a backup of your application state, which again creates extra overhead.

If you want to learn more about Redux, check out the Redux FAQ section, which contains tons of interesting questions you might have about Redux. Also, check out this amazing analogy for Redux by Hitesh Choudhary.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.