Article thumbnail

Common mistakes in using React Context API

state management
context api


Maximize React's Context API with our guide. Identify and fix common mistakes for better performance. Elevate skills with best practices, optimizing code for enhanced React applications.

Quick guide to React Context API

Before delving into the nuances of good and bad practices with the React Context API, it's imperative to grasp how it functions and its underlying purpose. Picture a scenario where you have five deeply nested components requiring access to state at the top, along with several functions to update that state. It will look as follows:


If you're advocating for the use of the components composition pattern, your code might take a form similar to the following:


This is where the React Context API proves its value. It steps in to alleviate the constraints posed by maintaining the precise structure of the components tree or enforcing prop-drilling.

By offering a flexible mechanism for sharing data throughout the component tree, the Context API simplifies the process, providing a more dynamic and scalable solution to address complex needs. This flexibility is especially beneficial in scenarios where the structure of the component tree might change frequently - the real applications.


Indeed, the React Context API simplifies the code by eliminating the need for passing unnecessary props through components. Wrapping the components tree with a Provider and utilizing the dedicated hook useContext streamlines the process of accessing the required information at any level. This abstraction enhances code readability and maintenance, providing a cleaner and more efficient solution compared to prop-drilling or rigid components composition patterns.

It's worth noting that the workload when using the Context API is significantly reduced from a developer's standpoint. You simply wrap the necessary components once, and wherever you need access, you can effortlessly utilize the dedicated Context.

Moreover, if there are components in between that are not using the Context API value, and the value changes, their re-render will be skipped. You can observe this behavior in the CodeSandbox example I provided at the end of the article.


Re-render will be skipped!

The last crucial aspect of the Context API is its inherent mechanism of shadowing. This means that if you employ two or more wrappers, it will consistently adopt the values provided by the latest one.


Using Context directly

What does direct usage of context entail? Let's revisit it for enhanced clarity:


Directly using context in this manner can be considered suboptimal for a few reasons:

  1. "Limited reusability": components using context directly may be less reusable, as they are closely tied to a particular context,
  2. "Testing challenges": mocking or providing alternative contexts for testing purposes becomes less straightforward,
  3. "Readability and maintainability": code readability and maintainability can be compromised as the logic for accessing and utilizing context is dispersed throughout the component, potentially making it harder to follow and update,
  4. "Potential for shadowing issues": in scenarios with nested context providers, direct usage might lead to unintentional shadowing of context values, causing unexpected behavior
  5. "Dependency on a specific context structure": this approach makes the component tightly coupled to a specific context structure. If the context structure changes, it can lead to widespread updates in the components using it.

To address these issues, a solution involves implementing a straightforward abstraction using a custom hook for each context. This approach removes the ability to directly access the Context and compels developers to consume APIs exclusively through a single source of truth.


This simple refinement not only addresses a multitude of potential issues during refactoring and structural changes but also promotes code consistency. By mandating a centralized pattern through a custom hook, maintenance becomes more straightforward, minimizing the risk of unintended consequences across the application.

Over-reliance on developers

Without implying any negativity, it's crucial to acknowledge that tasks prone to human error should be automated or validated wherever possible. We're all human, and various individuals, including backend developers or juniors who might not be well-versed in React, may interact with the code.

In scenarios where someone forgets to add the necessary wrapper to the components tree, it could result in an ambiguous exception, typically something like "Cannot read properties of undefined", creating unnecessary confusion for developers.


This issue arises when an object is passed as the provider's value, but the corresponding components tree isn't wrapped. In such cases, implementing custom exception handling becomes crucial to alert developers directly about this oversight, providing clear and actionable warnings. Therefore, in our abstracted custom hook, we will incorporate this provision:


Struggling with the initial state hell

The createContext function necessitates an initial state for functionality, as per the interface contract. However, it also mandates that this state "matches" the type specified in the generic type parameter. The practice of adding numerous empty functions and initial states is not only inefficient but also time-consuming.


Why is this problematic? Forgetting to use the Context Provider can result in false positives or negatives with this state. Moreover, it demands additional effort each time you introduce a new function, creating unnecessary overhead.


Now, if someone forgets to include a provider or if the Context value becomes falsy, we will alert the developer. This approach provides a clear distinction between the state and the API for managing our context

Binding context with concrete components

Context should accommodate any kind of ReactNode - be it components, divs, null, and more. If you directly use a component inside Context, you tightly couple it with that particular component, limiting the presentation to a specific manner.


To address this, we should enable the passing of any ReactNode as children, essentially embracing a composition pattern with the content projection technique.


Thanks to this enhancement, we can now apply our Context logic by simply wrapping any component with its dedicated Provider.


Absence of initial state override

What if you find yourself in a situation where you need to override the default state because another component is loading data? It can be tricky without a built-in mechanism in the Context Provider.


Relying on useEffect can introduce challenges, especially when used solely for setting the initial state. That's why our Provider should offer a dedicated mechanism for this purpose, ideally achieved through properties rather than resorting to useEffect.


Moreover, the inclusion of an essential property, initialState, becomes particularly crucial when aiming to synchronize the server state and client state, especially in scenarios involving frameworks like Next or Gatsby.

To explore the integration of Zustand with frameworks like Next or any other server-side rendering (SSR) and static site generation (SSG) frameworks, feel free to delve into the article: 🌟 How to integrate state management in Zustand with NextJS.

Lack of memoization

You never know at which level a developer will add the Context Provider, so if you are creating a value from state and adding some functions to change this state, you'll end up doing it on every render.


To avoid unnecessary recalculations, remember to wrap your value with the useMemo hook. It will have a positive impact on runtime performance.


Coupling of logic with propagation

As we know, Context is intended solely for sharing data across nested components trees. To adhere to the single responsibility principle, it's a bad idea to incorporate complex logic within the Context provider. Such an approach would result in a substantial amount of code being loaded whenever any component utilizing your context is rendered. To address this, consider improving it with lazy loading, as illustrated below:


With the introduction of lazy loading, the code responsible for loading, located within a larger library, is now loaded only when the load action is triggered. However, this approach couples the propagation mechanism with application logic. Consequently, if you ever need to change the mechanism of business state transition, you would have to modify the propagation mechanism, breaking the open/closed principle from SOLID. A more effective solution exists that addresses these issues, ensuring the Context provider remains responsible only for facilitating state changes, keeping the bundle size minimal.


As demonstrated, we crafted a custom hook for a significant portion of the logic responsible for loading articles. Here, we employed dynamic import to load a substantial code chunk dynamically. Furthermore, we've strategically placed the state change logic outside the Context Provider. This design choice affords flexibility – if you decide to adopt an alternative state propagation mechanism in the future, you can replace only the propagation logic, leaving the business logic untouched and initial bundle size minimal.

Excessive reliance on the Context API

The React Context API is designed for state propagation. Using it as a state manager, typically an architecture and data flow pattern, may be a mistake. Your initial choice might be to start with Context, but eventually, you may find yourself dealing with a complex Providers structure and coupling between Providers to consume logic from different application areas.


Having something like this in your codebase indicates that you may be mishandling Context. It establishes a strong coupling between the Providers structure, making it nearly impossible to refactor or migrate to real state manager easily in the future.

So, use Context API as a data propagation mechanism for presentation components to enhance developer experience or for managing simple global states like theme changes. However, it's not suitable for application-specific logic and data. Attempting to embed business needs in Context typically results in a convoluted and messy structure that requires refactoring later.

You might consider flattening the providers using a function to improve visual readability. However, this approach introduces additional runtime performance overhead, and underneath, it still creates the same structure, ultimately masking the problem visually rather than addressing it.


To enhance this, consider opting for a dedicated state management tool rather than relying solely on Context API. The latter is not designed for handling complex application states effectively. Utilizing a robust state management tool can offer better scalability and maintainability for your application.

If you're interested in exploring these tools further, check out the following article: 🥇 Comparing Redux with Zustand for state management in React.

The final code

Conclusions and summary

Woah! That was a substantial amount of text and examples. I hope everything is clear now. If you have any questions, feel free to ask in the comments section. After this article, you should have a good understanding of how and when to use Context API appropriately based on different cases.

Keep in mind that while you can attempt to use Context API for application state management, it comes with risks. Eventually, you might find the need for extensive refactoring due to performance limitations, lacking built-in optimizations like selectors found in Zustand or Redux.

I create content regularly!

I hope you found my post interesting. If so, maybe you'll take a peek at my LinkedIn, where I publish posts daily.


Add your honest opinion about this article and help us improve the content.

created: 22-11-2023
updated: 22-11-2023