Web apps maintain state to enable more advanced user interactions. When we enter an email into a website, we expect that website to remember the email and not have to enter it over and over each time it displays. One way to remember information across renders in React is with state, specifically useContext hooks. I looked at useContexts and started using them in a project but ultimately went with component props instead. Here is what I learned in the process.
What are React contexts?
React context allows a state object to be read and written to form any component that imports the useContext hook and access to a provider in the component hierarchy. The state can be any collection of variables and functions. The provider is a React component that sets the initial state and allows all descendants to access the state. This simplicity makes React contexts flexible. It also allows for multiple different contexts within a web app depending on which components the app renders. You can modify the variables in a React context with functions that are a part of the state itself. Benefits of using React’s context include ease of importing and the ability to add the useContext hook to any component without modifying the props.
Where did we find problems?
We learned about useContext and started implementing it on our project to store information across all steps in a user workflow. We added the information the workflow needed and quickly saw the context state expanding with functions and variables. This on its own wasn’t a major issue. The state type was all in one file, which stayed readable.
The problem was that it became too tempting to modify variables that the current workflow step shouldn’t have had any power over. When a component imports useContext, it gains read and write access to the full state. The workflow developed hard-to-diagnose bugs that involved the state object being manipulated in unexpected ways. Within four weeks of development, the state was enabling interesting behaviors. We could click a button and changes would propagate through multiple steps and components. However, the code became difficult to maintain and understand, even on a small team where everyone had full knowledge of the codebase.
What did we do instead?
We decided to shift to a state object that is propped down from component to component. The benefit was that we could check at each layer to ensure the types only exposed the attributes and the functions that the component was interested in. This helped us keep the responsibilities of each component narrow and improved the readability of the code.
We found React Contexts enabled easy data sharing. However, this tool requires discipline to effectively control what is on the context and what can edit it. Did my team miss an opportunity by not making use of React contexts? Were the issues we bumped into a problem of our understanding or problems inherent in the tool? Let me know in the comments, and always experiment to find the right patterns for each project.
Thanks for the story. The global contezxt to rule them all sounds like an anti-pattern. The flow I found valuable is to keep multiple contexts, each focused on single domain. This way the components will get only what they need.
Cheers
In my opinion, your argument against contexts can be easily nullified by not exposing the setter functions of your states via context.
That would be the easiests solution if only your parent component should be allowed to alter state.
If you want different components to be able to set the state, but not all of them, you could also split your context into a state and setter context. The components that only alter state would only have to access the latter then, and would not re-render due to the states changing, as long as your setter references are stable. And this way, you can easily find children that change the state by looking for accessors of the setter context.
This might not work for one or the other reason in certain projects, but I feel you reintroduced the thing that contexts were made to avoid: prop-drilling.
From the limited understanding i have of your project, Im not entirely certain why you chose to use the context api in this instance. It sounds like you built some kind of multi page user form, but no one is advocating that localized components/work flows should use the context api. That wouldnt be sustainable in a codebase, for exactly the reason you are describing. Context is not for replacing state/props, its also not a replacement of state management tools like redux, but rather its for deep component trees, or unrelated components (no common parent) to have easy access to the same, hopefully limited, values.
The dedicated state management tools are there for a reason, don’t try to reinvent the wheel, the context api is no good for that purpose, nothing new about that.
This is not at all how Context is intended to be used, and reading the docs on it would told you this before you even got started. Not really sure how a blog post is warranted from “We tried a tool without bothering to understand how it works, and we scrapped it when we didn’t understand how it works.”
Immense swag here ngl
So you basically replaced useContext with prop drilling…
I think context is good, but not in a native form, as it causes unnecessary re-renders, we need to wrap it in a redux-way so we can make use of memoization, as useContext will work inside the component, it doesn’t care about useMemo
I actually found a way to modify useReducer and createContext to only give me the props that I wanted read for a specific component.
I noticed that once you useContext on a component with a global state it will rerender every single time you change one item in the state. This is due to when you create a context it has a key “Provider” which has a key “Consumer”, you need to delete both the consumer and provider and place your own code for both which handles registering its own listeners and when to update instead of React doing the job itself. The useReducer you will need to change around when the reducer updates the state. You can do this by only forcing an update when the callback deems necessary
Ugh, why not use valtio?… (Or jotai, or zustand)