How to make all local state globally accessible and how much it costs in terms of performance in React

roggc

roggc

Posted on June 5, 2023

How to make all local state globally accessible and how much it costs in terms of performance in React

The use case

We will be coding the following scenario:

use case

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, 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 library, we must create a slices.js file like this:

import getHookAndProviderFromSlices from "react-context-slices";

export const { useSlice, Provider } = getHookAndProviderFromSlices({
  globalState: { initialArg: {} },
});
Enter fullscreen mode Exit fullscreen mode

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.

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

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

import { useSlice } from "@slices";

export const useGetState = (id) => {
  const [globalState] = useSlice("globalState");
  return globalState[id] ?? [];
};
Enter fullscreen mode Exit fullscreen mode

This hook recovers state values from Context.

The App component

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

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

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

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.

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

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

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.

💖 💪 🙅 🚩
roggc
roggc

Posted on June 5, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related