In this article, we will look at how to get rid of React.createContext() in your apps and find a motivation for doing so.
If you landed at this article you are probably familiar with React and may have already had experience with React context, but if not, feel free to read it anyway and share it with people who might be interested.
1. Why avoid React.createContext()?
Context degrades readability, entangles and restricts the app.
Just take a look at this basic example:
<ThemeContext.Provider value={theme}>
<AuthContext.Provider value={currentUser}>
<ModalContext.Provider value={modal}>
<VolumeContext.Provider value={volume}>
<RouterProvider router={router} />
</VolumeContext.Provider>
</ModalContext.Provider>
</AuthContext.Provider>
</ThemeContext.Provider>
Doesn't look too comprehensible and reliable, does it?
Here are some potential issues when using contexts:
- The more contexts used, the less readable and controllable your app becomes;
- Sometimes nested contexts require a correct order to work, which makes your app difficult to maintain;
- Some contexts should be re-used during the setup of a test environment to keep working properly;
- You have to make sure the component is a child down the tree of a necessary context provider.
Fun fact: the well-known "promise-hell" looks alike 🤷♂️
loadClients()
.then((client) => {
return populate(client)
.then((clientPopulated) => {
return commit(clientPopulated);
});
});
2. How to replace React.createContext()?
Create hooks instead.
Let's replace ThemeContext from the example above with a custom useTheme hook:
import { useAppEvents } from 'use-app-events';
const useTheme = () => {
const [theme, setTheme] = useState('dark');
/** Set up communication between the instances of the hook. */
const { notifyEventListeners, listenForEvents } = useAppEvents();
listenForEvents('theme-update', (themeNext: string) => {
setTheme(themeNext);
});
const updateTheme = (themeNext: string) => {
setTheme(themeNext);
notifyEventListeners('theme-update', themeNext);
};
return {
theme,
updateTheme,
};
};
We used an npm package called
use-app-events
to let all instances of the useTheme hook communicate to synchronize their theme state. It ensures that the theme value will be the same when useTheme is called multiple times around the app.Moreover, thanks to the
use-app-events
package, the theme will be synchronized even between browser tabs.
At this point, ThemeContext is no longer needed as the useTheme hook can be used anywhere in the app to get the current theme and update it:
const App = () => {
const { theme, updateTheme } = useTheme();
updateTheme('light');
// Output: <div>Current theme: light</div>
return <div>Current theme: {theme}</div>;
}
What are the pros of the approach:
- No need to set up a hook somewhere up the tree before children can use it (including test environments);
- The render structure is cleaner, meaning no more confusing arrow-shaped structure built out of your contexts;
- State is synchronized between tabs.
Conclusion
React.createContext() was a powerful tool some time ago for sure, but hooks provide a more controlled and reliable way to manage the global state of your app if properly implemented in conjunction with the use-app-events
package.
Top comments (15)
In some scenarios, react context is more useful than the hook like above. So depending on the situation you can choose between react hook or react context. And finally, if you want to have a theme config for all part of your app, I suggest you use react-context to do this job ;))
Hey, in my opinion, if you want to generally avoid React.Context, you shouldn't create even a single one, because in that case, you would think "if I already have one, why wouldn't I create another one" and this would lead you into this eventually:
So, you need to set some rules in your app, like standards, for example, "Don't use React.Context in this app at all" if you want to avoid cases like on the screen above
but how does react hooks control whole app like context does
Contexts are usually supposed to be used to pass some data deep in the tree without "prop-drilling", so these hooks allow you to do the same.
The hooks create and share the same state synchronized using the
use-app-events
package, that's how they allow you to manage a global state via hooks anywhere in the app no matter how deep you are in the treeI don't know, but this seems kind of weird - "state" and "events" are two different things, the idea that you can just replace one with the other seems ... odd.
I mean, I also use events, but I don't use them to replace global state.
If
React.Context
falls short, then just use a more robust state management solution like Redux!(RTK, Redux ToolKit, is pretty awesome - it removes 90% of the notorious Redux "boilerplate")
In my opinion, it is probably much faster and easier to create a simple hook to manage something like volume, theme, user, and any other state in your app :)
Also, having a state distributed between hooks looks much more "modular", I would say (it is simple and fast, but not perfect anyway)
I am mainly a backend dev but I agree with the readability point.
Recently I had to read a decently large react source code, with a lot of context providers, that was not fun 🥲!
example:
I'll rather use zustand!
Yeah, zustand is a thing, but it might be "overkill" sometimes if you need a simple state management for the entire app
On the other hand, the
use-app-events
package is very small (16 kb vs 326kb zustand) and is not supposed to handle the organization of your app state, but rather gives you tools to help organize communication between the (un)related components, hooks, or tabs.To mention, this article describes just one of the cases in how the
use-app-events
package can be used -> how to replace contexts with hooksThere are a few problems with this approach though. You're creating an event listener in a hook that is never cleaned up, which means that when this hook is used in a component that is mounted/unmounted several times, it will create multiple listeners that do the same thing. And then there's the 2nd problem, you're updating state in a side-effect which has the potential to create many bugs in React apps.
If you need to manage state in your app in this way, its better to use a proper state management library like jotai.
Hey, valid points, but fortunately, listeners get removed every time the useAppEvents hook is unmounted, meaning that if the useTheme hook is unmounted, the useAppEvents hook inside of it removes listeners it created for this particular instance
These side-effects are used only to synchronize the instances of the same hook in this case, thus triggering re-render when they are synchronized, so they work pretty React-ish, I would say
I haven't experienced any issues with this approach yet, but the combination of
notifyEventListeners
andlistenForEvents
is flexible enough to let you play aroundWhichever AI generated that banner image was probably trained on the gayest corners of the fediverse
Yeah, let's just don't use react context, but a third party library will do the job! You know what? Let's just don't use react at all.
Whoever still can't understand react context for couple of years now, should reconsider if he is using the right tool.
Yeah, you are totally right, it's so hard for a senior developer to understand the React context, sorry 😄
By the way, the
use-app-events
package was not created to replace React context, but it can help you do thatP.S. you aren't forced to use all the tools that React provides to develop any web app
Fully agreed, React Context is a hassle and unintuitive to use, imho.
I would recommend Recoil (my personal favourite) or Jotai instead, though. They are very similar to each other, very simple to use, and they work just fine.
Some comments have been hidden by the post's author - find out more