How to optimize shared states in React
Tags: react, javascript, hooks, context
Published: 2020-03-10 13:57:40

---
title: How to optimize shared states in React
published: true
description: 1 state : 1 context and other approaches
tags: react, javascript, hooks, context
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/383swg9373d5dbwlfkp4.png
---

Consider the following example:

```react
export default function App() {
  const [count, setCount] = useState(0);
  const [toggle, setToggle] = useState(false);

  return (
    <Context.Provider value={{ count, setCount, toggle, setToggle }}>
      <SubtreeComponent>
        <Decrement />
        <Counter />
        <Increment />
        <Toggle />
      </SubtreeComponent>
    </Context.Provider>
  );
}
```

```react
export const Context = createContext();
```

```react
export function Counter() {
  const { count } = useContext(Context);

  return <span>{count}</span>;
}
```

```react
export function Increment() {
  const { setCount } = useContext(Context);

  return <button onClick={() => setCount(prev => prev + 1)}>Increment</button>;
}
```

```react
export function Decrement() {
  const { setCount } = useContext(Context);

  return <button onClick={() => setCount(prev => prev - 1)}>Decrement</button>;
}
```

```react
export function Toggle() {
  const { toggle, setToggle } = useContext(Context);

  return (
    <label>
      <input
        type="checkbox"
        checked={toggle}
        onChange={() => setToggle(prev => !prev)}
      />
      Toggle
    </label>
  );
}
```

[codesandbox](https://codesandbox.io/s/context-x-0-f2xl3)

![This image shows that we clicked the increment button](https://dev-to-uploads.s3.amazonaws.com/i/czskwdnqhubfy1sgci50.png)

(During the profiling sessions, the `increment` button was clicked)

![This image shows that all the components rendered during the profiling session](https://dev-to-uploads.s3.amazonaws.com/i/y4j8gzu0dxqcqgrahtfe.png)

Intuitively, when we change a context value, we might assume that this change propagates solely to context consumers (components calling `useContext`) that use this particular value. However, a change in a single value of a context propagates to all its consumers scheduling them to update and re-render regardless of whether they use this value or not. This change also causes the entire subtree wrapped in the context provider to re-render.

Although it may not necessarily result in significant performance issues, except when values change too often or when there are expensive re-render calculations that haven’t been memoized (`useMemo`), it is more likely to lead to undesirable behavior, particularly when a consumer component fires effects after every render.

Firstly, we want to prevent the context provider subtree from re-rendering unnecessarily. This can be accomplished by passing the provider subtree as a `children` prop to a wrapper component.

(The context provider subtree is represented by `SubtreeComponent` for the sake of simplicity)

```react
export default function App() {
  return (
    <Provider>
      <SubtreeComponent>
        <Decrement />
        <Counter />
        <Increment />
        <Toggle />
      </SubtreeComponent>
    </Provider>
  );
}
```

```react
export function Provider({ children }) {
  const [count, setCount] = useState(0);
  const [toggle, setToggle] = useState(false);

  return (
    <Context.Provider value={{ count, setCount, toggle, setToggle }}>
      {children}
    </Context.Provider>
  );
}
```

[codesandbox](https://codesandbox.io/s/context-x-1-v4py3)

![This image shows that the subtree didn’t render during the profiling session](https://dev-to-uploads.s3.amazonaws.com/i/wtqr622vofmxlinld3m1.png)

Now, we want to prevent consumers from re-rendering unless necessary, or, more precisely, unless they actually use the changed value. One convenient approach is to create a separate context for each independent value.

```react
export function Provider({ children }) {
  const [count, setCount] = useState(0);
  const [toggle, setToggle] = useState(false);

  return (
    <CountContext.Provider value={{ count, setCount }}>
      <ToggleContext.Provider value={{ toggle, setToggle }}>
        {children}
      </ToggleContext.Provider>
    </CountContext.Provider>
  );
}
```

```react
export const CountContext = createContext();
```

```react
export const ToggleContext = createContext();
```

[codesandbox](https://codesandbox.io/s/context-x-2-ggm9p)

![This image shows that all the consumers rendered during the profiling session](https://dev-to-uploads.s3.amazonaws.com/i/ejjy5gwhxvfydafi1nit.png)

Note that the consumers rendered nonetheless. This happens because both state variable declarations are in the same parent component. So we should split them into two components.

```react
export default function App() {
  return (
    <CountProvider>
      <ToggleProvider>
        <SubtreeComponent>
          <Decrement />
          <Counter />
          <Increment />
          <Toggle />
        </SubtreeComponent>
      </ToggleProvider>
    </CountProvider>
  );
}
```

```react
export function CountProvider({ children }) {
  const [count, setCount] = useState(0);

  return (
    <CountContext.Provider value={{ count, setCount }}>
      {children}
    </CountContext.Provider>
  );
}
```

```react
export function ToggleProvider({ children }) {
  const [toggle, setToggle] = useState(false);

  return (
    <ToggleContext.Provider value={{ toggle, setToggle }}>
      {children}
    </ToggleContext.Provider>
  );
}
```

[codesandbox](https://codesandbox.io/s/context-x-3-tj2se)

![This image shows that only the count consumers rendered during the profiling session](https://dev-to-uploads.s3.amazonaws.com/i/l79pbugbc2j4ju5l1oxc.png)

State variable declarations return a pair of values, the current state and a function that updates that state. These values can be consumed independently, so we should split them into two contexts.

```react
export function CountProvider({ children }) {
  const [count, setCount] = useState(0);

  return (
    <CountContext.Provider value={count}>
      <SetCountContext.Provider value={setCount}>
        {children}
      </SetCountContext.Provider>
    </CountContext.Provider>
  );
}
```

```react
export function ToggleProvider({ children }) {
  const [toggle, setToggle] = useState(false);

  return (
    <ToggleContext.Provider value={toggle}>
      <SetToggleContext.Provider value={setToggle}>
        {children}
      </SetToggleContext.Provider>
    </ToggleContext.Provider>
  );
}
```

[codesandbox](https://codesandbox.io/s/context-x-4-2kgwj)

![This image shows that only one consumer rendered during the profiling session](https://dev-to-uploads.s3.amazonaws.com/i/543j815fh1njsbgn5ya9.png)

So far, so good. But as you may have noticed, this code could rapidly become too long and time-consuming.

[react-context-x](https://github.com/charbelrami/react-context-x) is a tiny (3kB) library that might come in handy. It provides a familiar API that is basically an abstraction to the code shown in these examples.

Consider an object of all the states we want to share from the same level in the component tree.

```react
const states = {
  count: 0,
  toggle: false
};
```

`createContexts` (plural) is a function that receives these states, creates a pair of contexts for each of one, and returns an array with all these pairs.

```react
const states = {
  count: 0,
  toggle: false
};

export const contexts = createContexts(states);
```

Then, we pass this array to a `Providers` component that inserts all the required providers into the component tree.

```react
export default function App() {
  return (
    <Providers contexts={contexts}>
      <SubtreeComponent>
        <Decrement />
        <Counter />
        <Increment />
        <Toggle />
      </SubtreeComponent>
    </Providers>
  );
}
```

To consume these contexts, we use hooks that accept the array as the first argument and, as the second argument, a string that identifies which context we want to access.

```react
export function Counter() {
  const count = useStateContext(contexts, "count");

  return <span>{count}</span>;
}
```

```react
export function Increment() {
  const setCount = useSetStateContext(contexts, "count");

  return <button onClick={() => setCount(prev => prev + 1)}>Increment</button>;
}
```

```react
export function Decrement() {
  const setCount = useSetStateContext(contexts, "count");

  return <button onClick={() => setCount(prev => prev - 1)}>Decrement</button>;
}
```

```react
export function Toggle() {
  const toggle = useStateContext(contexts, "toggle");
  const setToggle = useSetStateContext(contexts, "toggle");

  return (
    <label>
      <input
        type="checkbox"
        checked={toggle}
        onChange={() => setToggle(prev => !prev)}
      />
      Toggle
    </label>
  );
}
```

[codesandbox](https://codesandbox.io/s/context-x-o0dcc)

![This image shows that only one consumer rendered during the profiling session using context-x](https://dev-to-uploads.s3.amazonaws.com/i/383swg9373d5dbwlfkp4.png)

Thanks!

Learn more:

* [How to optimize your context value by Kent C. Dodds](https://kentcdodds.com/blog/how-to-optimize-your-context-value)
* [Dan Abramov’s comment on preventing re-renders](https://github.com/facebook/react/issues/15156#issuecomment-474590693)