Last Updated Date: 18.10.2023 Version: 1.0.0 Author: Onur Dayibasi
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.
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
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
const globalState = {
themeMode,
sidebarStatus,
}
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
const globalState = {
projects,
teams,
tasks,
users,
}
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 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 ;
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;
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.
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
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
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.
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.
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 (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
This resulted in rendering almost all components. We can optimize this part a bit. For this
In this method, we will try to realize the same scenario with React 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.
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>
);
};
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
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.
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.
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
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.
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
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.
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
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.
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.
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.
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>
);
};
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.
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.
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.
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)?
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
What are the advantages
Sample: https://onurdayibasi.dev/state-redux-connect2
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 functions will update Comp props when a state of interest in the store changes.
We will need 3 types of such mapStateToProps here
mapDispatchToProps functions are the functions that we want to send store news and update the state.
In the same way we will need 3 types of outbound speech mapDispatchToProps
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.
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);
In terms of rendering, it worked at the performance we wanted and only rendered something relevant through the architecture.
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.
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.