How to make all local state globally accessible and how much it costs in terms of performance in React
Published: 2023-06-05 16:56:41

## The use case

We will be coding the following scenario:

![use case](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qe12pkex9elqcubxyx25.png)

So we have a use case where we input the number of counters, and then they will be rendered. Each counter has two buttons, the first one controls its own local state, the second one controls the local state of next counter. And also, they show the value of the input text entered (in this case a number) and its name (each instance of a component have a unique name).

We will compare how much it costs to make all local state globally accessible in terms of performance. We will see that the curve is exponential, that is, the time it takes to render all components vs the number of components.

## Considerations

Because we don't know what number of counters will be rendered (can be any), we can not lift up state declaration to the upper component in the tree, because it will be variable, and we cannot call each time a different number of hooks. 

So we must declare state locally. To make it globally accessible we will use the library [react-context-slices](https://www.npmjs.com/package/react-context-slices), which allows to manage state through Context in an easy and fast way.

Using this library, we will create a slice of context to store there all the values of state (and setters). For this we will create two hooks, `useAddState` and `useGetState`. The first one will create state with `useState` and add the values (value and setter) to the slice of context. The second one will recover those values. 

## `slices.js`

First, when using [this](https://www.npmjs.com/package/react-context-slices) library, we must create a `slices.js` file like this:

```javascript
import getHookAndProviderFromSlices from "react-context-slices";

export const { useSlice, Provider } = getHookAndProviderFromSlices({
  globalState: { initialArg: {} },
});
```

As you can see we are creating a slice of Context named `globalState`, initialising it with an empty object.

## `useAddState` and `useGetState` hooks

Now we define those hooks.

```javascript
import { useEffect, useState } from "react";
import { useSlice } from "@slices";

export const useAddState = (initialValue, id) => {
  const [, setGlobalState] = useSlice("globalState");
  const [value, setValue] = useState(initialValue);

  useEffect(() => {
    setGlobalState((gS) => ({
      ...gS,
      [id]: [value, setValue],
    }));
  }, [value, id, setGlobalState, setValue]);

  return [value, setValue];
};
```

This hook creates state with `useState` and stores it in the Context.

```javascript
import { useSlice } from "@slices";

export const useGetState = (id) => {
  const [globalState] = useSlice("globalState");
  return globalState[id] ?? [];
};
```

This hook recovers state values from Context.

## The `App` component

```javascript
import Counter from "@components/counter";
import { useAddState, useGetState } from "@src/hooks";
import { useDeferredValue, useEffect, useMemo, useRef } from "react";
import CounterVisualiser from "@components/counter-visualiser";

const App = () => {
  const timeRef = useRef(new Date().getTime());
  const [numberOfCounters, setNumberOfCounters] = useAddState(
    0,
    "app-number-of-components"
  );
  const [text, setText] = useAddState("", "app-input-text");
  const [count] = useGetState("counter0-counter");

  useEffect(() => {
    if (count === 0) {
      const duration = (new Date().getTime() - timeRef.current) / 1000;
      console.log("duration", duration, numberOfCounters);
    }
  }, [count, numberOfCounters]);

  const slicesJsx = useMemo(
    () =>
      Array.from({ length: numberOfCounters }, (_, i) => i).map(
        (v, i, array) => (
          <Counter
            key={v}
            componentName={`counter${v}`}
            otherComponent={`counter${v === array.length - 1 ? 0 : v + 1}`}
          />
        )
      ),
    [numberOfCounters]
  );

  const jsx = useMemo(() => {
    return (
      <>
        <div>
          <input
            type="text"
            value={text ?? ""}
            onChange={(e) => setText(e.target.value)}
          />
          <button onClick={() => setNumberOfCounters(parseInt(text))}>
            set number of counters
          </button>
        </div>
        <CounterVisualiser id="counter10-counter" />
        {slicesJsx}
      </>
    );
  }, [text, slicesJsx, setNumberOfCounters, setText]);

  return jsx;
};

export default App;
```

What this component does is render the jsx and compute the time it takes to the page to be operative, ready to be used and to interact with.

## `Counter` component

```javascript
import { useGetState, useAddState } from "@src/hooks";
import { useMemo, useEffect, useDeferredValue } from "react";

const Counter = ({ componentName, otherComponent }) => {
  const [count, setCount] = useAddState(0, `${componentName}-counter`);
  const [, setCountOtherComponent] = useGetState(`${otherComponent}-counter`);
  const [inputText] = useGetState("app-input-text");
  const defferredInputTexT = useDeferredValue(inputText);

  const jsx = useMemo(() => {
    return (
      <div>
        <button onClick={() => setCount((c) => c + 1)}>+</button>
        {count}
        <button onClick={() => setCountOtherComponent((c) => c + 1)}>+</button>
        {defferredInputTexT}
        {componentName}
      </div>
    );
  }, [
    count,
    defferredInputTexT,
    componentName,
    setCountOtherComponent,
    setCount,
  ]);

  return jsx;
};

export default Counter;
```

Is important to note the use of `useMemo` for the rendered jsx. If we do not use `useMemo`, it will compute each time the rendered jsx for all counters, because all counters will render each time any of them change its state, because all of them react to the global state change. So to avoid cost of computation of jsx each time (because only one counter of all of them will effectively change its jsx), we use `useMemo`. We do not avoid that each counter renders each time, but at least we optimise the process of rendering a little bit.

Also note how we use `useDefferredValue` hook for the input text of the `App` component. This allows the user to enter text and deffer the update of this in the counters.

## The `CounterVisualiser` component

The `CounterVisualiser` component, rendered in the jsx of the `App` component, is just here to prove that state in this project is reactive, that is, that components consuming the global state updates.

```javascript
import { useGetState } from "@src/hooks";
import { useDeferredValue } from "react";

const CounterVisualiser = ({ id }) => {
  const [count] = useGetState(id);
  const defferredCount = useDeferredValue(count);
  return <div>{`${id}: ${defferredCount}`}</div>;
};

export default CounterVisualiser;
```

It just get state from the global state and renders it.

## Costs in terms of performance

Here is a plot of the time it takes to the page to be operative on first render vs the number of components:

![costs in terms of performance, time vs number of components](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hzwacsn9162ytcvowb5p.png)

This is approximately, but we can observe an exponential shape. Let's say that until 4000 it's almost linear. More than 4000 components (counters in this case) the costs in terms of performance are evident.

## Summary

The purpose of this project is to know how much it costs in terms of performance make all local state globally accessible. 

This implies that all components will render each time any of them change the state (I say 'the state' because it's global and all components are reactive to it).

We see that when there are more than 4000 components in the page performance gets penalised quite a lot. Even 4000 takes the page around 30 seconds to be operative (3000 components takes 20 seconds, 2000 components takes 13 seconds and 1000 components 5 seconds).

So it's cool to have all the components of the page reacting to state changes of any part in the component tree, but we pay a price in terms of performance when we have thousands of components in the page, because all of them will render each time any part of the global state changes.

Thanks for reading and happy coding.