DEV Community

Mohammad Faisal
Mohammad Faisal

Posted on • Edited on • Originally published at Medium

My Frustrations With the Context API in React

To read more articles like this, visit my blog

State management in React has come a long way. Redux is the longtime industry standard, but after the introduction of the Context API, many argue that the days of Redux are over.

I have had the privilege to work with both and use them in large projects.

I can understand the advantages that the Context API brings to the table, but I am going with Redux for my next project.

In a way, this is a love letter to Redux. Let’s begin!

Middleware Support

Middleware is an integral part of my workflow. I use them to show alerts based on the response of remote API calls without ever getting the result back to the component. Some other use cases can be:

  • Logging

  • Crash Reporting

  • Talking to an asynchronous API

  • Routing

Here is how to do it with Redux:

const middleware = [
    ...getDefaultMiddleware(),
    errorMiddleware,
    loggerMiddleware,
];

export const rootStore = configureStore({
    reducer: rootReducer,
    middleware,
});
Enter fullscreen mode Exit fullscreen mode

These are features that can hardly be ignored in most large-scale projects.

Implementing them with the Context API is possible, but Redux has made it much more manageable. Unfortunately, I find it unnecessarily complex to implement this with Context.

Provider Hell

Whether you’re using the Context API or Redux, it’s advised to keep the size of each reducer/context small for better understanding.

That's not an issue with Redux, as we can use combineReducer() to combine multiple reducers into a single store and wrap our whole application with that.

But in the Context API, you must wrap your component with the contexts that it actually needs. It creates a provider hell when your application grows large in size.

export const UserComponent = () => {
  return (
    <Context1 value={value}>
      <Context2 value={value2}>
        <Context3 value={value3}>
          <Context4 value={value4}>

            {YOUR_COMPONENT_CODE}

          </Context4>
        </Context3>
      </Context2>
    </Context1>
  );
}
Enter fullscreen mode Exit fullscreen mode

Yes, I am aware that there are some neat techniques to avoid this issue, like creating an HOC with the required context and wrapping the component with that HOC.

But imagine how many different considerations you have to keep in mind while coding. The situation gets worse when someone else comes and tries to dig through your code.

So why should we make our lives harder?

Familiar Data Flow

Redux has been used by so many projects that there is a high chance that most junior developers have encountered it at some point in their careers.

A typical Redux data flow looks like this: Component -> Actions -> (Middlewares) -> Reducers -> Component.

The workflow and structure of the code are predictable. I think it’s a huge advantage when working on a large project where developers are constantly coming and going.

When using Context, you don’t know where your contexts are. Each developer has their own way of organizing things. This can lead to some unnecessary overhead for others, which is costly.

Performance

You saw this coming. Context has known issues with performance because whenever any part of the context is updated, the whole sub-tree re-renders.

Let’s say we wrap two components with a context. This context holds two values: userDetails and orderList.

export const App = () => {

  value = {
    userDetails: {},
    orderList: {}
  }

  return <Context value={value}>
    <UserComponent /> 
    <OrderComponent />
  </Context>
}
Enter fullscreen mode Exit fullscreen mode

The UserComponent depends on the value of userDetails, whereas OrderComponent depends on orderList.

Now if userDetails updates, both components re-render (although it has nothing to do with OrderComponent).

Do you see how an extensive application can misuse Context and create huge performance issues?

Yes, there are solutions like splitting the context or memorizing the component before updating. But with the help of selectors, we can avoid this problem altogether. Why reinvent the wheel?

Clear Separation

Redux creates a clear separation of concerns. You can remove almost 100% of your data manipulation from your component.

Most modern applications depend on some kind of remote data anyway. So by using selectors, you can pre-process data before entering into the component.

Source: [Hacker Noon](https://hackernoon.com/https-medium-com-heypb-react-redux-workflow-in-4-steps-beginner-friendly-guide-4aea9d56f5bd)

It’s a huge win when developing a large application before everybody knows where data comes from and how it’s being modified.

With Context, you don’t have the privilege to do that — or at least it’s tricky to achieve.

Debugging

Regarding debugging, Redux provides an excellent toolkit that makes the debugging experience a breeze.

With the introduction of the redux-toolkit, it has become easier. You turn it on, and the rest is handled by Redux.

With Context, it’s much more challenging to visualize the changes happening. However, you can mitigate the problem by keeping your context small and predictable.

Plugin Support

Redux has rich plugin support that can facilitate many side functionalities that are time-consuming to implement and hard to maintain.

You can think of almost anything and be pleasantly surprised that someone else has already implemented the solution. Some of the most popular ones are:

  • redux-persist

  • redux-thunk

  • redux-form

  • reselect

These plugins can improve the quality of your application. You have to learn some extra things, but they can reduce the hassle in the long run.

Size

In terms of size, you can argue that Redux needs additional resources to function, increasing the bundle size.

I agree with that, but is it that important when you have the benefits of a rich developer experience and a respectable community?

Final Thoughts

Don’t get me wrong: If you can use Context correctly, most of these problems are solvable. Some might argue that it’s a safer option because it’s built-in.

I agree, but libraries and packages are there for a reason. For smaller projects, the Context API is OK. But for larger ones, I think going with Redux is the safer route.

Let me know what you think in the comments. Have a great day!

Resources

Get in touch with me via LinkedInor my Personal Website.

Top comments (4)

Collapse
 
markerikson profile image
Mark Erikson

One quick note: we specifically discourage the use of getDefaultMiddleware as a standalone function, and that is already being removed in RTK 2.0.

The right syntax is:

configureStore({
  reducer,
  middleware: (getDefaultMiddleware) => {
    // use the `getDefaultMiddleware` callback here
  }
})
Enter fullscreen mode Exit fullscreen mode
Collapse
 
noblica profile image
Dušan Perković

Oh man, I'm not a fan of either of these solutions. . Generally if it's something coming from a backend, I think React/TanStack Query is a really good solution.

Other than that, I've been using either Jotai for something simple, or Zustand for more complicated state management. Their APIs are simpler and easier to work with then something like Redux IMO.

Collapse
 
barrymichaeldoyle profile image
Barry Michael Doyle

Yeah I much prefer Tanstack's React Query for managing data coming from the backend. Redux was great it's time, but it has inspired all these wonderful new technologies along the way.

Also a big zustand fan :)

Collapse
 
barrymichaeldoyle profile image
Barry Michael Doyle • Edited

Ever since I started using zustand I've never really looked back.

It's like having React Context without the need to wrap everything in providers, it just works! And the best part is you can use middleware like persist to persist parts of your zustand state.

One part that you might love about it is the fact that it hooks up to redux devtools.