image

Introduction

State management in React has always been a contested topic, with different opinions on how best to get it done. Redux, an open-source, do-it-all library that provides a central store and actions to modify the store, has been a good choice in state management.   However, Redux is quite complex, requires extra lines of code, and has a steep learning curve. A viable alternative to Redux is a combination of Context API and useReducer.

State management in React

When React first came out, developers used prop drilling to pass data between different components. This is the process of passing the data as a prop to each level of the nested component until you reach your target.   There was no global state, and this hack produced a lot of extra code. Also, adding properties that a component will never use affects its architectural design and performance.   To solve the problem of global state management, Redux was introduced. Redux is an open-source library that provides your app with a store where all components can access a property, no matter where they are nested in the component tree.

What is Redux?

From its official website, Redux is “A Predictable State Container for JS Apps.” It helps you write codes you can trust to behave in a consistent pattern. Also, it centralizes your application’s state, allowing you to perform powerful tasks like undo/redo, persist state, and many more.   It also comes with Redux DevTools, which makes tracing through your application easier. This way, you can know how, where, when, and why your application’s state may have changed.   However, one disadvantage is the amount of extra code you need to write to set up a global state. For a library replacing the code-heavy prop drilling pattern, it also requires developers to write a lot.   For Redux to function, here are the three major parts you need: actions, reducers, and store.

Actions

Actions are objects used to convey data to the Redux store. They have two properties: a payload that contains the information that should be changed and a type property that describes what the action does.
Actions are objects used to convey data to the Redux store. They have two properties: a payload that contains the information that should be changed and a type property that describes what the action does.

// myAction.js
const reduxAction = payload => {
    return {
        type: 'action description',
        payload
    }
};
export default reduxAction;
Conventionally, the type is usually in all caps, with the words separated by underscores. For example, UPDATE_CART, DELETE_ITEM, etc.

Reducers

Reducers are pure functions (functions that return the same result if the same arguments are passed) that implement the action’s behavior. They take the current state and action as variables, perform an action on the state, and return the new state.
// myReducer.js
const reducer = (state, action) => {
    const { type, payload } = action;
    switch (type) {
        case "action type":
            return {
                ["action description"]: payload
            };
        default:
            return state;
    }
};
export default reducer;

Store

Your application’s state is housed in the store. This is where all components can access it, no matter how deeply nested they are. There can only be one store while setting up Redux.
// myStore.js
import { createStore } from 'redux'
const store = createStore(componentName);
 
Even though Redux solved our state management woes, we still had to jump through many hoops and write some time-consuming codes to set it up. Now, while one could argue this is necessary for a robust app, you do not need to use Redux every time you need to share state among some components in your app.   A good alternative is to use React Context API + useReducer.

What is the React Context API?

Introduced in React v16.3, the API enables you to share data with a tree of React components. According to the official React documentation, ”provides a way to pass data through the component tree without having to pass props down manually at every level”. This is React’s solution to the problem we noted earlier—sharing state in multiple components, irrespective of their nesting and connection. To get started with a context, you’ll need to import the function from React and then call the method with a parameter as its default value. The method returns an object from which you can deconstruct Provider and Consumer components.
import React, {createContext} from 'react';
const newContext = createContext({ theme: 'dark' });
The Provider ensures the state is available to all child components, no matter how deeply nested they are. It also receives a value prop, where you can update the current value.

<Provider value={theme: 'light'}>
    { children }
</Provider >

On the other hand, the Consumer consumes the data that the Provider passes down without a need for prop drilling. The useContext hook is a more concise way of handling this, though.
<Consumer>
    {value => <span>{value}</span>}
</Consumer>
Without the addition of React hooks, especially useReducer, the Context API doesn’t look like much good on its own. However, when you pair it with useReducer, you have a solution that can rival , without the need for extra code.  

The useReducer hook

Introduced in the v16.8 React update, useReducer provides a template to specify how you want your data to change objectively. The hook receives two arguments: a reducer function and an initial state. It then returns a new state and a function conveniently named dispatch with which you can dispatch a new action.  
const myReducer = (state, action) => {
    const { type } = action;
    switch (action) {
        case 'action description':
            const newState = // Perform an action based on this action description
    return newState;
        default:
            throw new Error() // Throw a new error if the switch case is not picked up
    }
}
const [state, dispatch] = useReducer(myReducer, []);

In the lines of code above, we have defined our reducer function, which we used as an argument in useReducer to instantiate a new state. We also get a dispatch function from the return value of useReducer.

// This allows us to do this
return (
    <button onClick={() =>
      dispatch({ type: 'action type'})}>
    {state}
    </button>
  )

To put it all together . . .

Now, let us explore how to combine useReducer and Context API to create some elegant code.

import React, { createContext, useReducer } from 'react';
const initialState = {};
const store = createContext(initialState);
const { Provider } = store;
const StateProvider = ({ children }) => {
    const [state, dispatch] = useReducer((state, action) => {
        switch (action.type) {
            case 'action description':
                const newState = // perform an action based on the action description
        return newState;
            default:
                throw new Error();
        };
    }, initialState);
    return <Provider value={{ state, dispatch }}>{children}</Provider>;
};
export { store, StateProvider }

To use our state, we’ll need to wrap it around the component whose children would need access to the store. If we want a global state, we will do this:

// store.js
// root index.js file
import { createRoot } from "react-dom/client";
import App from './App';
import { StateProvider } from './store.js';
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
    <StateProvider>
        <App />
    </StateProvider>
);

Now, you can access your store from any component in the component tree. All you need to do is import the useContext hook from React and your store from store.js.

// deeplyNestedComponent.js
import React, { useContext } from 'react';
import { store } from './store.js';

const deeplyNestedComponent = () => {
  const globalState = useContext(store);
  console.log(globalState); // this will return { theme: light }
};

Similarities and differences between Redux and useReducer + Context API

Similarities

  1. Both use the “Flux architecture” (a design pattern that uses actions and reducers).
  2. They both solve the problem of “prop drilling.”
  3. Both create a separation of concerns between your state management logic and your UI.

Differences

  1. Redux passes down an instance of the current Redux store, while Context passes down only the current state value. This means every change to state value results in all components changing in Context, while Redux only triggers re-render for components which use the portion of the state.
  2. Redux is a standalone library you need to install to use with React, unlike Context, which comes with React Core.
  3. Redux supports using middleware, which brings opportunities to perform complex operations before modifying your state. To do this with Context, you can use useEffect.
  4. Redux store can be visualized with the Redux DevTools extension. This comes in handy with troubleshooting. You could replicate this in Context with the React Developer Tools and a logger in Reducer; however, this is more tedious.

Use cases to aid your decision-making

When you do not need a large, combined reducer, using Redux may be redundant and become overkill.

Use Context API + useReducer when:

  • The app is mid-sized.
  • You’re working on an isolated part of a large application whose state has become complex but does not need to be accessed by other components.
  • Your state does not change often.
  • The codebase of your application is relatively small.

Use Redux when:

  • You have many application states required in many places in the app.
  • The app requires the input of multiple developers on a medium-to-large codebase.
  • The state will be updated frequently.
  • The logic to update the state is complex.
  • You want to visualize the changes to your state as it is triggered.

Conclusion

In modern web development, choosing the right tool is important. You can use Redux for your global state and use isolated Context for some larger components within the app.

The next time you are about to reach for the Redux sledgehammer, ask yourself: do I really need Redux, or can Context API get this done?