Ebenezer Don Full-stack software engineer with a passion for building meaningful products that ease the lives of users.

Use Hooks + Context, not React + Redux

6 min read 1681

Use Hooks And Context, Not React And Redux

Redux introduces a lot of complexity to our codebase with the excessive amount of code it requires. At best, this makes it an imperfect solution for state management in React applications. And yet, far too many React developers default to Redux for state management without considering other alternatives.

In this article, I will introduce the React Context API for state management and show you what makes React Hooks plus the Context API a better solution than Redux.

video by Leigh Halliday

Why we need a state management tool

In typical React, the way to handle data between disconnected components is through prop drilling. Since there is no global state that components can access if, for instance, you want to pass data from a top-level component to a fifth-level component, you’ll have to pass the data as a prop on each level of the tree until you get to your desired component.

This results in writing a ton of extra code, and giving components properties that they will never use also affects their architectural design. In order to solve this problem, we needed a way to provide a global state that all components, no matter how deeply nested they are, could access.

By solving this, Redux, an open-source JavaScript library for managing application state, became the go-to solution for React developers.

How Redux works

The Redux documentation describes it as a predictable state container for JavaScript applications that helps us to write applications that behave consistently, run in different environments, and are easy to test.

One disadvantage of prop drilling is the need for writing a considerable amount of extra code in order to access data from a top-level component. With Redux, this disadvantage becomes more severe since all the extra code it requires for setting up a global state introduces even more complexity. Redux requires three main building parts to function: actions, reducers, and store.

Actions

These are objects that are used to send data to the Redux store. They typically have two properties: a type property for describing what the action does and a payload property that contains the information that should be changed in the app state.

// action.js
const reduxAction = payload => {
  return {
    type: 'action description',
    payload
  }
};

export default reduxAction;

The type is usually in all caps with its words separated by underscores. For example, SIGNUP_USER or DELETE_USER_DATA.

Reducers

These are pure functions that implement the action behavior. They take the current application state, perform an action, and then return a new state:

const reducer = (state, action) => {
  const { type, payload } = action;
  switch(type){
    case "action type":
      return {
        ["action description"]: payload
      };
    default:
      return state;
  }
};

export default reducer;

Store

The store is where the application’s state is housed. There is only one store in any Redux application:

import { createStore } from 'redux'

const store = createStore(componentName);

Since our application can only have one Redux store, in order to create a single root reducer for all our components, we’ll need the combineReducers method from Redux.

With the long process and considerable amount of code required to set up Redux, imagine what our codebase will look like when we have multiple components to work with. Even though Redux solves our state management problem, it is really time-consuming to use, has a difficult learning curve, and introduces a whole new layer of complexity to our application.

Fortunately, the React Context API solves this problem. When combined with React Hooks, we have a state management solution that is less time-consuming to set up, has an easier learning curve, and requires minimal code.

The React Context API

The new Context API came with React 16.3. Here’s how Context is explained in the React documentation:

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

The React Context API is React’s way of managing state in multiple components that are not directly connected.

To create a context, we’ll use the createContext method from React, which accepts a parameter for its default value:

import React from 'react';

const newContext = React.createContext({ color: 'black' });

The createContext method returns an object with a Provider and a Consumer component:

const { Provider, Consumer } = newContext;

The Provider component is what makes the state available to all child components, no matter how deeply nested they are within the component hierarchy. The Provider component receives a value prop. This is where we’ll pass our current value:

<Provider value={color: 'blue'}>
  {children}
</Provider>

The Consumer, as its name implies, consumes the data from the Provider without any need for prop drilling:

<Consumer>
  {value => <span>{value}</span>}}
</Consumer>

Without Hooks, the Context API might not seem like much when compared to Redux, but combined with the useReducer Hook, we have a solution that finally solves the state management problem.

What are Hooks in React?

Hooks are a type of function that enable the execution of custom code in a base code. In React, Hooks are special functions that allow us to “hook into” its core features.

React Hooks provide an alternative to writing class-based components by allowing us to easily handle state management from functional components.

The useContext Hook

If you noticed, when explaining the React Context API, we needed to wrap our content in a Consumer component and then pass a function as a child just so we could access (or consume) our state. This introduces unnecessary component nesting and increases the complexity of our code.

The useContext Hook makes things a lot nicer and more straightforward. In order to access our state with it, all we need to do is call it with our created context as its argument:

const newContext = React.createContext({ color: 'black' });

const value = useContext(newContext);

console.log(value); // this will return { color: 'black' }

Now, instead of wrapping our content in a Consumer component, we can simply access our state through the value variable.

The useReducer Hook

The useReducer Hook came with React 16.7.0. Just like the reduce() method in JavaScript, the useReducer Hook receives two values as its argument — in this case, the current state and an action — and then returns a new state:

const [state, dispatch] = useReducer((state, action) => {
  const { type } = action;
  switch(action) {
    case 'action description':
      const newState = // do something with the action
      return newState;
    default:
      throw new Error()
  }
}, []);

In the above block, we’ve defined our state and a corresponding method, dispatch, handling it. When we call the dispatch method, the useReducer() Hook will perform an action based on the type that our method receives in its action argument:

...
return (
  <button onClick={() =>
    dispatch({ type: 'action type'})}>
  </button>
)

The useReducer Hook plus the Context API

Setting up our store

Now that we know how the Context API and the useReducer Hook work individually, let’s see what happens when we combine them in order to get the ideal global state management solution for our application. We’ll create our global state in a store.js file:

// store.js
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 = // do something with the action
        return newState;
      default:
        throw new Error();
    };
  }, initialState);

  return <Provider value={{ state, dispatch }}>{children}</Provider>;
};

export { store, StateProvider }

In our store.js file, we used the createContext() method from React that we explained earlier to create a new context. Remember that the createContext() method returns an object with a Provider and Consumer component. This time, we’ll be using only the Provider component and then the useContext Hook when we need to access our state.

Notice how we used the useReducer Hook in our StateProvider. When we need to manipulate our state, we’ll call the dispatch method and pass in an object with the desired type as its argument.

In our StateProvider, we returned our Provider component with a value prop of state and dispatch from the useReducer Hook.

Accessing our state globally

In order to access our state globally, we’ll need to wrap our root <App/> component in our StoreProvider before rendering it in our ReactDOM.render() function:

// root index.js file
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { StateProvider } from './store.js';

const app = (
  <StateProvider>
    <App />
  </StateProvider>
);

ReactDOM.render(app, document.getElementById('root'));

Now, our store context can be accessed from any component in the component tree. To do this, we’ll import the useContext Hook from react and the store from our ./store.js file:

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

const ExampleComponent = () => {
  const globalState = useContext(store);
  console.log(globalState); // this will return { color: red }
};

Adding and removing data from our state

We’ve seen how we can access our global state. In order to add and remove data from our state, we’ll need the dispatch method from our store context. We only need to call the dispatch method and pass in an object with type (the action description as defined in our StateProvider component) as its parameter:

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

const ExampleComponent = () => {
  const globalState = useContext(store);
  const { dispatch } = globalState;

  dispatch({ type: 'action description' })
};

Conclusion

To a good extent, Redux works for state management in React applications and has a few advantages, but its verbosity makes it really difficult to pick up, and the ton of extra code needed to get it working in our application introduces a lot of unnecessary complexity.

On the other hand, with the useContext API and React Hooks, there is no need to install external libraries or add a bunch of files and folders in order to get our app working. This makes it a much simpler, more straightforward way to handle global state management in React applications.

Plug: , a DVR for web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Ebenezer Don Full-stack software engineer with a passion for building meaningful products that ease the lives of users.

22 Replies to “Use Hooks + Context, not React + Redux”

  1. Ebenezer, this is a most excellent primer on using these relatively new technologies! For the past few months, I’ve had a steep learning curve on using Hooks and Context in the most effective way possible. Me and my colleagues now have some pretty good patterns working for us but are always trying to improve.

    Follow-up article suggestion: Show the community how to write tests for each of the entities you’ve mentioned, namely Context and Reducers. This is something I’m working through now and there doesn’t seem to be many articles on the subject.

  2. The global / lifted up state with useContext + useState/useReduce combo is possible way, but be aware about the performance problems – the whole app (down from provider lever) is re-rendered on every state change. Have a look into Hookstate – supercharged useState which allows to lift the state up to global space, is as simple as useState itself and has got incredible performance – https://github.com/avkonst/hookstate (disclaimer: I am a maintainer)

  3. Andrew, I’d like your advice on something: My colleagues and I have been having a debate about whether to have one global instance of the Context state (or Hookstate) or whether to have multiple smaller instances of the state where there is no need to share the state between different parts of an app.

    Interested in your thoughts on this!

  4. There is the best practice in software engineering: “local (more isolated) variable is better than global (more accessible)”. Applying it to react state, the most local is per component state. The next level is the state lifted up to the parent component, when multiple peer / same level components need to share access to the same state (typical case is multi-field forms). The next level is the state per root component or global state when multiple components in different branches of DOM hierarchy need access to the same state. So, lift your state up when you need it but do not open more than you need it. It will give you more maintainable software. Hookstate allows me to lift up and down easily when I need it.

    The usual practice in software engineering can be said as something like “use different variables to hold not related data”. It means it is OK to have multiple global state variables or multiple local variables. For example, I keep user account state and app settings configurable by a user in two separate variables, although both are usually global as require sharing across the whole app. Hookstate helps me with this too.

  5. Thanks, Andrew. I’ve always followed the same practice as well. This is why, for each of 3 modal forms I’ve built, each has their own Context. One downside of this is that the child forms (Add Vehicle and Add User) inside the first 2 modal forms are both used in the 3rd modal form as well. This has resulted in some duplicate code in the Contexts, Reducers, and Actions but we have definitely achieved code separation.

  6. I wonder why there is a need to pass the reducer (useReducer results).. why not just pass the functions and variables to providers vulue, so there is NO need to use dispatch function when calling state change. Use useState, and pass the results to the provider, so on the consumer components just call the function that alter the state. If there is some heavy logic before altering the state why not wrap it on function first before passing. My point is whats the benefit of calling

    dispatch(“UPDATE_USER”,true) where as you can directly call

    updateUser(true) function without the ceremony of useReducer hooks

  7. Nice article so far. I got confused by the code snippet in `Setting up our store`. Turned out the code snippet is scrollable. I noticed that by accident. Was pretty confusing..

  8. Is it just me that thinks this is the same amount of boilerplate?

    Without any of the features that make redux worth using.

  9. Great article. Apparently the only difference between the two(when trying to decide which to use), is that Redux can handle frequent state changes while Context API cannot.

  10. Nice @ how blog post completely missed that Redux is a few lines of code, uses context internally, and has dev tools + you don’t need to dumpster dive in codebases (along with time travel, a very simple pattern, middleware, consistent interfaces, selectors, immutability and much lower likelihood to miss render waste, without diving into debugger, etc). TLDR: don’t MVC or work on teams, just wing it and live in debugger/memorize all your code111

  11. I would only agree with you if it’s for a small web application not being planned to scale, otherwise I strongly disagree. You lose A LOT of things by not using Redux, not just “few advantages” as you mentioned. A lot of optimization under the hood is done by Redux through serialization & the connect HOC.

    Please read this:

    https://blog.isquaredsoftware.com/2018/03/redux-not-dead-yet/
    https://www.reddit.com/r/reactjs/comments/bqf5ot/can_anyone_tell_me_why_hooks_have_exploded/eo6fe6e/?context=10000

  12. Agree with anon. If you end up with a store and reducer, why not use redux in the first place? With the new redux hooks API, the usage is way simpler and the boilerplate is light. Add caching to selectors and you get awesome perfs.

  13. @Ebenezer: Same question here + 1 more,

    1. How do I use this inside a class component (as by Tiago)
    2. After updating the state from inside the sub-components, the state should be returned back and GUI refreshed as well?

    -> -> -> Show Logged in as Yes or No based on a boolean in the store.

    I put a button in the Header component to update the loggedIn to true, when I click the button, the reducer gets executed properly but the Text in header (expected Logged In to change from No to Yes) does not change.

    Can you please help?

    Regards,

    SG

  14. Hi Shailendra, Hooks can’t be used directly in a class component, although, the React Context API can be used with class based components. Yomi did a good job explaining the React Context API with class-based examples: https://blog.logrocket.com/how-and-when-to-use-reacts-new-context-api-b584e41b2704/

    For your second question, can you confirm that you’re using the useContext Hook to access your store? Also if you could share a code snippet or screenshot, that would be helpful.

    – Ebenezer

Leave a Reply