learn react ui logoLearnReactUI
Mastering React Client State Management

React Client State Management

Last Updated Date: 18.10.2023 Version: 1.0.0 Author: Onur Dayibasi

1. Introduction

Source Code The source code for the app is available by downloading the content.

Demos You can access this knowledgemap from the You can access this knowledgemap from the link

Concepts Before starting this topic, I would like to talk about some concepts.

  • Global State (Client and Server State)
  • Client State
  • Server State

Before starting all these topics, I would like to talk about what Client State is and what it is not. For this, I would like to start by giving some examples

Client State

It only refers to the state that is kept in the browser, in the client part, it applies to states where you keep that data in the memory above the browser, localStorage or sessionStorage, but do not forward it to the server,

For example

  • You change the theme and keep it only in the browser,
  • SideBar is keeping the scanner in open and close state,
const globalState = {
   themeMode,
   sidebarStatus,
 }

Server State

These also include the state that the server is the primary owner of the control over which data you keep on the server,

For example

  • Objects related to your project that you keep in databases
const globalState = {
   projects,
   teams,
   tasks,
   users,
 }

Global State

In summary, we call Global State the state of the data kept in both servers and databases.

const globalState = {
   projects,
   teams,
   tasks,
   users,
   themeMode,
   sidebarStatus,
 }

While we used to manage all data as Global State in libraries such as Redux, MobX, etc., over time, as it was understood that 2 types of states are different from each other, special libraries started to emerge.

Client State ! = Server State

Client State and Server State are not similar in behavior. Server state is a bit more difficult to manage than Client and includes some extra concepts, for example ;

  • Async Status
  • Caching
  • Callback Handling
  • Mutation & Optimistic Update
  • Background Updates
  • Pagination /Incremental Loading
  • Outdated Requests
  • Deduping Request
  • Garbage Collection ve Memory Implications
  • vb..

However, managing Client data is quite simple compared to the above situations. Due to these differences in requirements, libraries have also been differentiated.

This is what we want to focus on here. In simple terms, it is to analyze the use of recent Client State methods and libraries.

Client State Management Methods that we will focus on;

  • useState
  • useContext
  • Redux
  • Zustand
  • Jotai
  • Signal

2. Playground Environment

In order to experiment with different State Management environments I needed to create a playground. In this playground let's have a component, for example a counter component. This component consists of 2 parts.

  • A Button and
  • A Value Displayer.

Untitled

I want to create a separate layout using this component, i.e. hierarchically under different remote nodes, so that we can look at the operations we do to move data, and the components they affect.

Hierarchical Representation of Components

Hierarchical Representation of Components

Here, we can say that only Inc and ValDisp components that have State communication are the only ones that concern us, and the Main, Counter, CompA, CompB, CompC Layout related objects that have nothing to do with State.

Let's complicate the situation a bit more and use 2 Counters. Let's show that State can work more independently. The result is a visual like the one below.

Playground Env

Playground Env

3. Props Drilling (useState)

In the Props Drilling method, we will keep Counter1 and Counter 2 in the StatePropDrillingPage component at the top by making State Lift State Up. With the Props drilling method, we will ensure that it transfers to all its elements and its child elements.

function StatePropDrillingPage() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const renderDemo = () => {
    return <CompMain count1={count1} count2={count2} onClick1={() => setCount1(count1 + 1)} onClick2={() => setCount2(count2 + 1)} />;
  };

Of course, this structure has troublesome and problematic sides.

  • Need to Delegate All Props: If the number of props is too high, you need to host codes that are not related to your component but delegate to the subcomponent in the form of {...props} to all delegate to the subcomponent or {...props, x:x} if you have your own additions in between. But since the props content is not opened, the code is not too complicated.
  • Delegating Props Piece by Piece: The number of props is too many, you need to delatate some of them down, but if the number of props is still too many, you need to create components like <A x1 x2 x3 ... xn> <B y1 y2 y3 ... xn>, this time the component that makes these fragments in the intermediate classes will act like a Manager instead of a Dummy Layout etc..
  • If the component you want to deliver the props is very low in the tree hierarchy: So in a situation where we need to pass these props to many components, the code in each component becomes more fragile, that is, open to change.

Props Delegation

Props Delegation

We are not following the Open-Closed Principle, which corresponds to the letter SOLID O.

O. Open-Closed Principle:

  • Your classes are open to extension and closed to change. In summary, when we want to add a new function to the system, we can do this by extending AbstractClass/Interface without touching existing classes. In this way, there is no need to update existing classes to add new features.

Rendering

One problem for us Frontend developers is the performance problem caused by the components entering Rendering. If you say how this structure has an effect, you can examine the example below.

Sample: https://onurdayibasi.dev/state-props-drilling

[https://onurdayibasi.dev/state-props-drilling](https://onurdayibasi.dev/state-props-drilling) (State Props Drilling)

https://onurdayibasi.dev/state-props-drilling (State Props Drilling)

In this method when we press the Inc Button in Counter1 it triggers the callback onClick passed to it with props from Main.

export const IncBtn = props => <button onClick={props.onClick}>Inc</button>

As a result, the count1 state on Main is updated. This in turn updates the

  • We called Main → Counter1 → Counter2 → CompA → CompB → C1 → C2 and only increased the value in C1.

This resulted in rendering almost all components. We can optimize this part a bit. For this

4. useContext

In this method, we will try to realize the same scenario with React Context.

Context

Context

Here we create 2 Context and put each Counter context state getter and setter in Provider, then we pass CompMain with Provider.


import { createContext } from 'react';
export const Counter1Context = createContext(null);
export const Counter2Context = createContext(null);

function StateContextPageUI(props) {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const renderDemo = () => {
    return (
      <div>
        <Counter1Context.Provider value={{ count1, setCount1 }}>
          <Counter2Context.Provider value={{ count2, setCount2 }}>{<CompMain/>}</Counter2Context.Provider>
        </Counter1Context.Provider>
      </div>
    );
  };

In this section, wrapping the component in different contexts disrupts the understandability of the code. But when we look at the usage parts, it saves the props transition by skipping the components in between in the hierarchy.

Untitled

  • Disadvantage: So the main State management part is still complicated (Context creation, Wrapping)
  • Advantage: Except for the components that use context, other components do not need to have a relationship with the code.

As can be seen in the code below, none of the components that do not need count1 and count2 state or increment codes are eliminated from the need to prop delegation.

export const CompB = () => {
  return (
    <Square size={200} color="blue" title="CompB">
      <ValueDisp1 />
      <ValueDisp2 />
    </Square>
  );
};

export const CompA = () => {
  return (
    <Square size={350} color="green" title="CompA">
      <CompB />
    </Square>
  );
};

export const CompMain = () => {
  return (
    <Square size="80%" color="red" title="Main">
      <IncBtn1 size={150} color="gray" title="IncBtn1" />
      <IncBtn2 size={150} color="gray" title="IncBtn2" />
      <CompA />
    </Square>
  );
};

They can use the context wherever they need it by pulling relevant information.

export const ValueDisp1 = () => {
  const { count1 } = useContext(Counter1Context);
  return (
    <Square size={50} color="pink" title="C1">
      <span style={{ backgroundColor: 'red' }}>{count1}</span>
    </Square>
  );
};

Rendering

Although we didn't go through the Props components in the code, since Context is kept on the main page, a change here causes all nodes in the tree hierarchy to be rendered.

withMemoization

withMemoization

For this we add memoization. Because our Main → CompA → CompB are Layout components that we don't want to change once rendered.

  const memoizedCompMain = useMemo(() => {
    return <CompMain />;
  }, []);

When we add the above section to the code and run Context.Provider through it, you can see that only the components that benefit from Context.Provider are rendered.

Untitled

What we still can't do in this part is to render only that component, which component is affected by which context.

Conclusion: Even though there are some improvements in the intermediate components and performance, Context still does not have a simple structure to manage the App Client logic and we could not make rendering optimization at the point we wanted.

5. Zustand

Redux benzeri Flux Pattern kullanan Zustand State Management kütüphanesini kullanarak aynı Playground oluşturalım (Linki)

Sample: https://onurdayibasi.dev/state-zustand

Zustand Playground

Zustand Playground

In this one let's create 2 redux similar slice for 2 different Counter…

import create from 'zustand'

export const useCounter1Slice = create((set) => ({
  count1: 0,
  increase: () => set((state) => ({ count1: state.count1 + 1 })),
}))

export const useCounter2Slice = create((set) => ({
    count2: 0,
    increase: () => set((state) => ({ count2: state.count2 + 1 })),
  }))

And only where we need this Slice content, we import and use this Slice.

ValueDisp

export const ValueDisp1 = () => {
    const count1 = useCounter1Slice((state) => state.count1)
    return (
        <Square size={50} color="pink" title="C1">
            <span style={{ backgroundColor: 'red' }}>{count1}</span>
        </Square>
    );
};

Trigger

export const IncBtn1 = props => {
    const inc = useCounter1Store((state) => state.increase)
    return (
        <Square>
            <button onClick={inc}>Inc1</button>
        </Square>
    );
};

Other components are unaware of these structures and are not affected at all.

Rendering

When I clicked the Inc1 button, only the affected C1 component was rendered, just as we wanted. I did not need to use any Memoization.

Jotai Playground

Jotai Playground

Conclusion: Zustand was the State Management library that provided the best rendering performance and code writing style in the basic features I wanted among the libraries I examined in the first 3 articles.

6. Jotai

In this article, let's create the same Playground using Jotai, which allows us to manage state in an Atomic structure.

Sample: https://onurdayibasi.dev/state-jotai

Untitled

First of all, let's create 2 atomic structures that will hold state. Actually, there is no general store making logic in Jotai. There is working on atoms directly in the parts you need, so instead of a top → bottom structure, bottom → top is a structure that spreads from bottom to top. Anyway, we create atoms in a similar way to zustand without breaking our application logic.

import { atom } from 'jotai';

export const count1Atom = atom(0);
export const count2Atom = atom(0);

Here these atoms Orn count1Atom is actually our pointer address which is our getter and settter to that value

export const IncBtn2 = props => {
  const [count2, setCount2] = useAtom(count2Atom);
  return (
      <button onClick={() => setCount2(count2 + 1)}>Inc2</button>
  );
};

Likewise, when we examine ValueDisp

exportconstValueDisp2 = () => {
const [count2] =useAtom(count2Atom);
return (
<spanstyle={{backgroundColor: 'red' }}>{count2}</span>
  );
};

As code, it was almost very close to Zustand usage. The only difference is that since we took the increment business logic of the Zustand value state change process to the place where we defined the Zustand slice, the part of this business logic operation according to the State Machine was out of the Component.

However, Jotai does not deal with this part, so we know the previous state of the value in the component and we do the increment operation in the component.

Rendering

IncBtn1 and C1 were affected in the rendering part. As I wrote above, this is very normal in Jotai's structure because the Inc Button performs the logic of doing business and it needs to know the Count1 val value.

Untitled

Conclusion: I think the code cleanliness and Rendering logic is quite clean like Zustand. If you don't want to build a Reducing, common Store structure, Jotai can be a good Architectural choice.

7. Signal

We will talk about Signals, a State management tool that the Prereact team has created with the Reactive Programming effort and can also be used in React. In its own words ;

Signals are reactive primitives for managing application state.

Note: I shared a blog on Reactive Programming in the What is Svelte? blog post explaining how it differs from React programming logic.

There is a very similar structure to Jotai here. For example ... First creation order

//Signals
import { signal } from "@preact/signals-react";

export const count1Signal = signal(0);
export const count2Signal = signal(0);

Then there are very slight differences in usage compared to Jotai. While Jotai usesAtom to get a setter and getter from it.

In Signal, you can see that the code is written more cleanly because there is no need to extract a getter setter.

export const IncBtn2 = props => {
  return (
      <button onClick={() => count1Signal.value++}>Inc2</button>
  );
};

export const ValueDisp2 = () => {
  return (
      <span style={{ backgroundColor: 'red' }}>{count2Signal.value}</span>
  );
};

Rendering

Let's come to our sample State Management infrastructure example, in this example we expect remote components to trigger each other over a common State and not to render intermediate components.

The Inc1 button only triggers the rendering part of the C1 component.

Untitled

The fact that the Signals library is pure reactive, renders only the ValueDisp part without the need for rendering when using the previous value for Inc, provides a more performant work than Jotai. It is similar to Zustand.

But there are 3 more functions other than signal to manage this State in the API of the Signals library.

  • computed(fn): Creates a new signal calculated based on the values of other signals. The returned computed signal cannot be changed, only its value can be read, and its value is automatically updated whenever any signal connected to it changes.
  • effect(fn): allows you to bind callback functions that allow you to catch signal dependent changes when they happen, just like react useEffect. Like useEffect, it does inference on the signal you are using without specifying dependencies.
  • batch(fn): You may need to make changes to values in batches and then make changes again on a subset of values in response.

8. Redux HoC

Redux Toolkit is almost the same as Zustand and will give the same results. The only difference is that due to the Provider Pattern structure, you need to wrap your App component with Redux Provider at the beginning and store it. Zustand is more lightweight accordingly. But Rendering performances and code writing structures are almost similar. Zustand is already trying to create a lightweight alternative to Redux by resembling it as much as possible.

import { store } from './app/store';
import { Provider } from 'react-redux';

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);

What I want to focus on is the Redux Connect structure. Most probably this HoC structure will be deprecated soon with the Hooks trend, but first I will focus on the benefits of this structure compared to other uses via useSelector or slice.

Untitled

In fact, HoC is a concept that comes from High Order Functions, which is the basis of Functional Programming. And it is frequently used in JS language and libraries.

So are Hooks a concept that will replace Higher Order Component? In the 2023 world, we now say yes. But you can see what advantages HOC has in the following blog written by Eric Elliott in 2019.

Do React Hooks Replace Higher Order Components (HOCs)?

Pros vs Cons

So according to this, how could we make our structure more flexible?

Let's focus on the Advantages and Disadvantages of the structure we will make with React Connect.

Disadvantages

  • Hooks structure is now popular and HoC eliminates or has eliminated it.
  • If we keep the components completely away from the State they will reach and do not make them discrete and connect them to other storages, mapStateToProps, mapDispatchToProps we are writing the same functions twice...
  • The readability of the code decreases a little.

What are the advantages

  • The component works entirely on the props function and parameters given to it. Therefore it is dummy and only knows what is given to it.
  • In this extracted part, the Logics in the preparation of the data that the component will use or in the calling of external functions are trapped in the intermediate Container layers and we have a more flexible use by replacing these parts with other Container structures.
  • Since the flexibility of the component increases, it eliminates the rewriting of components in large projects.

Sample: https://onurdayibasi.dev/state-redux-connect2

Untitled

Let's examine the code a bit. First of all, similar to the Redux Toolkit, we need to define Action and Reducer Slice 2 for Counter. Below you see the one for Counter 1.

const INC_COUNTER1 = 'INC_COUNTER1';

export function incCounter1() {
  return {
    type: INC_COUNTER1,
    payload: {},
  };
}

const counter1InitialState = {
  value: 0,
};

export default function counter1(state = counter1InitialState, action) {
  switch (action.type) {
    case INC_COUNTER1:
      return {
        ...state,
        value: state.value + 1,
      };
    default:
      return state;
  }
}

of course we need to put them into Store in combineReducer

//Reducers
const rootReducer = combineReducers({
  counter1:counter1,
  counter2:counter2,
});

export const store = createStore(rootReducer, applyMiddleware(promise, thunk));

and you need to wrap the App as I described in this store Provider Pattern blog post.

  <Provider store={store}>
    <PersistGate loading={null} persistor={persistor}>
      <Router history={history}>
        <App />
      </Router>
    </PersistGate>
  </Provider>,

Note: Since the above part is not Zustand, Zustand usage and learning curve is simpler than Redux.

Instead of using useSelector, useDispatch or Slice structures, we will develop using the old connect HOC structure

In this section, we have Store codes and functions built on them that allow us to create the structure that we will divide into 2 parts as Component and Container.

import { incCounter1 } from 'store/counter1';
import { incCounter2 } from 'store/counter2';
import { connect } from 'react-redux';

const mapStateToPropsEmpty = () => () => ({});
const mapStateToPropsC1 = () => store => ({ counter: store.counter1 });
const mapStateToPropsC2 = () => store => ({ counter: store.counter2 });

const mapDispatchToPropsEmpty = () => {
  return {}
 };

const mapDispatchToPropsC1 = (dispatch) => {
  return {
    inc() {
      dispatch(incCounter1());
    }
  }
};
const mapDispatchToPropsC2 = (dispatch) => {
  return {
    inc() {
      dispatch(incCounter2());
    }
  }
};

Let's examine these functions a little bit;

mapStateToProps

mapStateToProps functions will update Comp props when a state of interest in the store changes.

Untitled

We will need 3 types of such mapStateToProps here

  • mapStateToPropsEmpty: Container has Inc1 and Inc2 button does not need to know the value itself, so they do not need a props
  • mapDispatchToPropsC1: store => ({ counter: store.counter1 }); component that only shows C1 Value,
  • mapDispatchToPropsC2: store => ({ counter: store.counter2 }); component that only shows C2 Value,

mapDispatchToProps

mapDispatchToProps functions are the functions that we want to send store news and update the state.

Untitled

In the same way we will need 3 types of outbound speech mapDispatchToProps

  • mapDispatchToPropsEmpty : ValueDisp has no such request.
  • mapDispatchToPropsC1: needs a function that abstracts the dispatch operation to increment the first counter
  • mapDispatchToPropsC2: needs a function that abstracts the dispatch operation to increment the second counter

After doing all this, to create the structure that will establish the connection between Container and Component. connect is performing this operation for us

ValueDisp

const ValueDisp1UI = (props) => {
  return (
    <Square size={50} color="pink" title="C1">
      <span style={{ backgroundColor: 'red' }}>{props.counter.value}</span>
    </Square>
  );
};
export const ValueDisp1 = connect(mapStateToPropsC1, mapDispatchToPropsEmpty)(ValueDisp1UI);

IncButton

const IncBtn1UI = props => {
  return (
      <button onClick={props.inc}>
        Inc1
      </button>
  );
};

export const IncBtn1 = connect(mapStateToPropsEmpty, mapDispatchToPropsC1)(IncBtn1UI);

In fact, this structure allows you to create a more flexible structure than the other structure.

  • I can use it exactly the same when defining ValueDisp1UI ValueDisp2. We couldn't do this in Hooks, because we had to tell which Counter state component it was in.
  • I can use IncBtn1UI in IncBtn2 in the same way.

Just like below. This structure is especially suitable for structures where you want to reuse the same View because you have separated the View.

export const ValueDisp1 = connect(mapStateToPropsC1, mapDispatchToPropsEmpty)(ValueDisp1UI);
export const ValueDisp2 = connect(mapStateToPropsC2, mapDispatchToPropsEmpty)(ValueDisp1UI);

export const IncBtn1 = connect(mapStateToPropsEmpty, mapDispatchToPropsC1)(IncBtn1UI);
export const IncBtn1 = connect(mapStateToPropsEmpty, mapDispatchToPropsC2)(IncBtn1UI);

Rendering

In terms of rendering, it worked at the performance we wanted and only rendered something relevant through the architecture.

Untitled

As a result, although this structure may seem complex at first, the components can be reused because the intermediate junctions are removed from the component.

  • axios
  • fetch
  • graphql
  • scync

It is more successful than using Hooks in terms of providing the flexibility to create layers that you can replace with the structure you want and even mock them with test structures.

It's a disadvantage in terms of readability and the tendency of future React projects to use Hooks.