DEV Community

Cover image for Don't use React.createContext(), create hooks.
Maksym Marchuk
Maksym Marchuk

Posted on • Updated on

Don't use React.createContext(), create hooks.

In this article, we will look at how to get rid of React.createContext() in your apps and find a motivation for doing so.

You are probably familiar with React and may have already had experience with React context if you landed at this article, but if not, feel free to read it anyway and share 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>
Enter fullscreen mode Exit fullscreen mode

Doesn't look too comprehensible and reliable, does it?

Here are some potential issues when using contexts:

  1. The more contexts used, the less readable and controllable your app becomes;
  2. Sometimes nested contexts require a correct order to work, which makes your app difficult to maintain;
  3. Some contexts should be re-used during the setup of a test environment to keep working properly;
  4. 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,
  };
};
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

What are the pros of the approach:

  1. No need to set up a hook somewhere up the tree before children can use it (including test environments);
  2. The render structure is cleaner, meaning no more confusing arrow-shaped structure built out of your contexts;
  3. 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)

Collapse
 
lc_phm_f11952cc8e810faf profile image
Lạc Phạm

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 ;))

Collapse
 
maqs profile image
Maksym Marchuk

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:

Image description

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

Collapse
 
aditya_tejus_12090b05f26f profile image
Aditya Tejus

but how does react hooks control whole app like context does

Collapse
 
maqs profile image
Maksym Marchuk

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 tree

Collapse
 
leob profile image
leob

I 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")

Collapse
 
maqs profile image
Maksym Marchuk

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)

Collapse
 
sfundomhlungu profile image
Sk

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:

 <StrictMode>
    <StyledEngineProvider injectFirst>
      <CssVarsProvider theme={theme} defaultMode="dark">
        <CssBaseline />
      <Compose
            components={[
              A LOT OF PROVIDERS HERE...
            ]}
          >
            <App />
          </Compose>

   </CssVarsProvider>
    </StyledEngineProvider>
  </StrictMode>,


Enter fullscreen mode Exit fullscreen mode

I'll rather use zustand!

Collapse
 
maqs profile image
Maksym Marchuk

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 hooks

Collapse
 
brense profile image
Rense Bakker

There 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.

Collapse
 
maqs profile image
Maksym Marchuk

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 and listenForEvents is flexible enough to let you play around

Collapse
 
darkwiiplayer profile image
Info Comment hidden by post author - thread only accessible via permalink
𒎏Wii 🏳️‍⚧️

Whichever AI generated that banner image was probably trained on the gayest corners of the fediverse

Collapse
 
sinanyilmaz profile image
Sinan Yilmaz

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.

Collapse
 
maqs profile image
Maksym Marchuk

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 that

P.S. you aren't forced to use all the tools that React provides to develop any web app

Collapse
 
elsyng profile image
Ellis

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.

Collapse
 
omor07 profile image
Omor07

Some comments have been hidden by the post's author - find out more