On the way to finding a better alternative to Context Provider in React

When it comes to sharing the state between different components in React you may face a difficult decision. What to choose: prop drilling, Context Provider, third-party library like Redux? The latest may be a better option. But if you are not in the mood to mess up with all of these reducers, selectors, actions, payloads? If you are not ready to architect the mechanic of flawless transition of the entire application’s immutable state? To find more sensible “why nots” check out an article written by the author of Redux: You Might Not Need Redux.

As for prop drilling, it is not a universal solution. You always have to keep in mind where to send the setter and what path it will go. As an unwanted side effect of prop drilling, it can provoke frequent re-rendering of the whole branches of the component tree that affects performance and user experience. Besides being the possibly easiest solution to come up with, it often demands much more to pay attention to make it right. And the cost of even a relatively small modification of the existing code with prop drilling implemented could appear big. The only reliable fix for such problems might turn out to be massive refactoring.

Context Provider

Let’s look at Context Providers closely as a more universal and predictable solution. I am going to use shortened code snippets here, so for reviewing the full versions feel free to walk to the referencing sandboxes. To illustrate how Context Provider works I chose a model where some component (Box) supposes reacting on command from another component (Button) which is in sibling branch, so no direct prop drilling is assumed:

What we have is:

It is a pretty simple example implemented straightforwardly. It just works. But in real applications, the state is rarely limited to the only value. Multiple components are subscribed to multiple states. What happens if every component subscribes to the whole bunch of all states?

The result:

It is not great that the first component triggers the re-render on the change of the border color of the second component. It has no border at all and is not obligated to react to the update of this particular part of the app’s state. For optimizing the behavior of both components we have to divide the common Context Provider into two independent ones:

Looks much better:

It seems to be the end. Well done! Not really. Let’s just look closely at the buttons. How often does re-rendering happen to them? (Check this sandbox.)

At this point, I must say that generally updating a single button in React application after every push is not a problem. But we chose it just as an illustration that could be applied to bigger and more complex web applications where components having control over the state may be implemented differently. From time to time it can be critical to secure their re-rendering behavior. It is one more time where we must examine our decision over the implementation and it may occur the cause of the potential weaker performance of the app. The only way to ensure that generally is to divide Context Providers once again:

The fully optimized version of our example took us four Context Providers to introduce two values of the global state. Each provider required several repetitive lines of code, not to mention the clumsy structure of the component tree that is not helping to maintain and debug the app:

Can we do better?

I wrote the state management library use-interstate for React that solves the problems we discussed above. It provides access to the shared state in a pretty straightforward way. It is intuitive and ensures better readability. No redundancy, no unwanted extra components in the inspector panel of the browser. It repeats the original React useState but for the state accessible through all components of the app. The function useInterstate stands for it. Compare

const [state] = useState();

and

const stateValue = useInterstate('name');

The string 'name' is a name of a single state value. To change that value we have the function setInterstate

setInterstate('name', newValue);

or

setInterstate('name', (prevValue) => {
// ... deduce newValue from prevValue
return newValue;
});

Let’s see how our example would look like rewritten with use-interstate. First of all, we initialize interstate:

This is how we define the initial values for the state. initInterstate returns methods useInterstate and setInterstate that will be used by the components who want to have access to the global state. As a side note, if the app's codebase is organized by grouping in multiple files this piece of code must be extracted in a separate file with the export of the methods to provide them to all components:

export const { useInterstate, setInterstate } = initInterstate(/* init values */);

Later on, each component needs to import the necessary methods:

import { useInterstate, setInterstate } from './State';

Now our Box components look like:

Notice how to get multiple values of the state: const { color1, color2 } = useInterstate(["color1", "color2"])

Button components implemented with use-interstate:

The main App component:

Look at how our code is compact now. How it is easy to implement. And it is fully optimized:

Conclusion

The React ecosystem is large, full of different tools for all occasions, and it is still growing. And one field where developers often feel unsatisfied is state management. There are powerful and yet sophisticated libraries with a big learning curve like Redux. There are native but still hard-to-use solutions like Context Providers. It reveals a big gap for tools that would be easy to understand, optimized right out of the box, and not demand excessive code constructions to implement in React applications. This is “why use-interstate?” More than that, it is not just an alternative for Context Providers. It can do a lot more. It strongly supports TypeScript, providing some innovative features. It even can explain most errors right in the IDE spiking human language:

If you are willing to become an early adopter of use-interstate check out the GitHub repository. It includes all necessary documentation to start. The most enjoyable part of the library is you don’t need to be an expert to make it helpful in a practical way. The example above opens the door to using use-interstate in your next project, explaining how to make state sharing much easier. Have a good coding!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Andrei Kovalev

Andrei Kovalev

Front-end Developer | TypeScript advocate | Open-source contributor