The state and state management is seemingly the most common and interesting topic when it comes to app development on the front-end. Thus everyone is chasing the most efficient and prominent way to manage their application state... are we?
I'm not a guru of the state management world, however; I want to familiarize you with some basic concepts with examples, which are:
- State
- Global state
- Local state (Better put everything in the store 😎)
And further, I'll say:
- When to use global and local state?
- Popular misconceptions about state management
The State
Why we need the state at all? The state is the current data that our app stores to control its behavior. For example, the checkbox stores data (boolean) if it's on or off.
Global State
Global means our state is accessible by every element/component of the app. But the important fact is that it pollutes the whole app since it echoes in every component that accesses it
Release the beast!
To illustrate the problem lets create a simple counter with React and Redux:
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { inc } from './actions'
export const Counter = () => {
const dispatch = useDispatch()
const count = useSelector(store => store.counter.count)
return (
<>
<h1>The count is: {count}</h1>
<button onClick={() => dispatch(inc())}>Increment</button>
</>
)
}
What if I'll do something like this somewhere in the app:
<>
<Counter />
<Counter />
</>
You're right. Both counters are showing up the same count:
With useSelector
hook we are accessing some data stored in the global store previously declared in our app. So the store probably looks like this:
{
counter: {
count: 0
}
}
It is clear that both counters display the same number cause they reflect the same state
The wind of change
To store multiple counts in the global store. We'll need to do something like this:
Change the structure of the store:
{
counters: [{ count: 0 }, { count: 0 }]
}
Change the Counter
:
export const Counter = ({ part = 0 }) => {
const dispatch = useDispatch()
// Now it selects just one of counters
const count = useSelector(store => store.counters[part].count)
return (
<>
<h1>The count is: {count}</h1>
{/*We'll also need to change our action factory and reducer */}
<button onClick={() => dispatch(inc(part))}>Increment</button>
</>
)
}
And finally:
<>
<Counter />
<Counter part={1} />
</>
Nailed it! Just change store, reducer, component, and manually pass the part
property to Counter
...
What can go wrong?
Choose your weapon wisely
I am a big fan of MobX. The MobX team did a great job bending JavaScript to allow you to feel reactive in it:
import React from 'react'
import { observable } from 'mobx'
import { observer } from 'mobx-react'
const counter = observable({ count: 0 })
const Counter = observer(() => (
<>
<h1>The count is: {counter.count}</h1>
<button onClick={() => counter.count++}>increment</button>
</>
))
Wow, it looks so neat!
And with multiple counters:
const counter = observable({ count: 0 })
const counter2 = observable({ count: 0 })
// counter is now a prop:
const Counter = observer(({ counter }) => (
<>
<h1>The count is: {counter.count}</h1>
<button onClick={() => counter.count++}>increment</button>
</>
))
Next:
<>
<Counter counter={counter} />
<Counter counter={counter2} />
</>
We end up with less code, but still, we have to pass state manually for each of component 🤦♀️
The local state
Even if the above examples seem stupid, the problem is real and it shows why we need a local state. Local state is not the state we define locally. It has the goal to encapsulate the dataflow within the component:
const Counter = () => {
const [count, setCount] = useState(0)
const incrememt = () => setCount(count => count + 1)
return (
<>
<h1>The count is: {count}</h1>
<button onClick={increment}>increment</button>
</>
)
}
And voila! counters do not share the state anymore!
<>
<Counter />
<Counter />
</>
The dark nature of the local state
Sadly; the local state seems to be much less manageable and debuggable. What's more, it can also hurt the performance of React app if not managed well. When you pass state many levels down and change state somewhere on the top component, all of its children get rerendered (inside virtual DOM) with it. It also tangles components together and makes them less scalable. Redux isolates state from components lifecycle and I/O. On the other hand, stateful components seem to be more modular - statefulness paradox? No. If your app gets more complex things start to be more connected and it's harder to separate them, whenever it comes to global or local state
Local vs global state
The question you should ask yourself to keep state local or global is not to share or not, it's about to encapsulate or not
Which solution to choose
Well established managers like Redux and MobX that supports tools like time-travel (see mobx-state-tree
) make debugging a pleasure. But it comes with a cost - Redux is known for being verbose and you have to keep discipline when working with it. It's meant to be used in huge projects. If you insist to use Redux in your tiny app. Take a glance at redux-toolkit
- an official tool to reduce Redux boilerplate or search for the other Redux wrapper. Immer is a wonderful library to write reducers. I like Hookstate - a straightforward way to lift the state up. Effector is worth checking and there are plenty of libraries waiting for you to discover them
Don't follow the example
What I'm trying to say is you shoudn't write your code to look exactly like examples in the web. If they want to show how things work they probably sacrifice some good things to be more specific. Reach for Redux Counter
from this article and write some custom hook:
const useCounter = (part = 0) => {
const dispatch = useDispatch()
const count = useSelector(store => store.counters[part].count)
const increment = () => dispatch({ type: 'increment' })
return [count, increment]
}
And our Counter
becomes:
export const Counter = ({ part = 0 }) => {
const [count, increment] = useCounter(part)
return (
<>
<h1>The count is: {count}</h1>
<button onClick={increment}>Increment</button>
</>
)
}
This way we moved most of the state logic outside the component. Hooks are like functions for components. So split your component into hooks and compose them ass (I hope) you do with your functions
Popular misconceptions
- Redux is a bad tool because it's too verbose
Redux is crude - that is correct. It is not designed to seduce you with code examples, but to provide transparent data flow
- Context API can replace Redux (or any other state manager)
Context API is not a state manager itself. Actually, you have to do all the management yourself like a pagan if you'll use it for that purpose. As if that were not enough, unlike several state managers, it does not optimize re-rendering. Instead, it can easily lead to unnecessary re-renders. Reach for this great article
- You can avoid re-renders caused by Context API if you destructure the context value
No! Please, before even thinking of doing that. Read this post written by the Redux maintainer @markerikson
- Context API is made for passing _state down (or lifting up)
The truth is: Context API is just a prop passing solution. I think the source of this popular misconception is that a variety of libraries use context for similar purposes, for example: passing theme state. But the theme is something that changes occasionally, and theme change typically should rerender the whole app
- MobX users practice voodoo
🙊
Conclusion
I have to confess that this section is troublesome. Should I address some advice? I've read a lot of articles touching this matter and I feel like it's so much to say - it's a complex problem to solve. So I'll just ask: what do you think about the current state of state management in React? and what is your current solution to deal with this problem?
Top comments (1)
Great read article Franciszek 👌