DEV Community ðŸ‘Đ‍ðŸ’ŧðŸ‘Ļ‍ðŸ’ŧ

Cover image for Global vs Local State in React
Franciszek Krasnowski
Franciszek Krasnowski

Posted on

Global vs Local State in React

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>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

What if I'll do something like this somewhere in the app:

<>
  <Counter />
  <Counter />
</>
Enter fullscreen mode Exit fullscreen mode

You're right. Both counters are showing up the same count:

counters animation

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

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

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>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

And finally:

<>
  <Counter />
  <Counter part={1} />
</>
Enter fullscreen mode Exit fullscreen mode

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

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

Next:

<>
  <Counter counter={counter} />
  <Counter counter={counter2} />
</>
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

And voila! counters do not share the state anymore!

<>
  <Counter />
  <Counter />
</>
Enter fullscreen mode Exit fullscreen mode

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

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>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
pierre profile image
Pierre-Henry Soria âœĻ

Great read article Franciszek 👌

👋 Welcome new DEV members in our Welcome Thread

Say hello to the newest members of DEV.