Skip to content
Photo by Josh Redd on Unsplash
Avatar
Brad Westfall
bradwestfall

How To Use and Not Use State


Tags: ReactBeginnerState

Besides the fact that you can't conditionalize your calls to useState, there's not really a strict set of rules to follow. However, there are some guidelines and some gotcha's to know about.


3-part Series


When I'm teaching React's state to people who are seeing it for the first time, they usually have the same set's of questions:

Can I use useState more than once?

Yes. In fact that's how it was designed to be used. You just can't conditionalize the calls to useState because the call order matters to React. The number of times you called useState is tracked and React expects the same amount of calls each time. Otherwise they'll return the wrong information to you. From React's perspective, they don't know if you're conditionalizing a call to useState, so the built-in linter will check to see if you are.

For example you can't do this:

const [count, setCount] = useState(0)
if (count > 3) {
  const [otherState, setOtherState] = useState(null)
}
const [error, setError] = useState(null)

If you could do the example above, then sometimes the error call to useState would be second and sometimes it would be third. That would be a problem for React.

This always brings up another question...

Can I call useState once for all my component's state?

Yes, but you might not want to.

With class components we would keep all of our state in one object. Then when we changed state with setState, we would provide an object and the API would "merge" its contents into the existing state for us. This doesn't happen with setting state in function components with useState.

If you did try to do it like this:

const [state, setState] = useState({
  count: 0,
  error: null
})

function add() {
  setState({ count: state.count + 1 })
}

You'd end up destroying state you didn't mean to, like the error in this case because calling setState will replace all existing state that you're storing in that particular call to useState.

If you choose to organize your state into a single object, you might have good reasons to. Just be sure to take care of preserving all the state when you make changes:

// The spread operator here takes all the existing state
// and does a shallow copy into this new object:
setState({ ...state, count: state.count + 1 })

If this doesn't make any sense, especially when I start talking about class components, that's okay. Maybe you don't need to know class components. I do go into more detail about how this works in the video above if you need more clarity.

What about using useReducer instead?

The normal convention you'll hear is:

If your state is complex, like a deeply nested object, useReducer is probably better than useState for this type of state.

That's the typical thing you'll hear in the React community. But we have more on that later in this document.

Is that the only reason to call useState separately?

No. Even the official docs give you a hint as to why they intended useState to be called separate times for each piece of state:

"Separating independent state variables also has another benefit. It makes it easy to later extract some related logic into a custom Hook"

In other words, if you wanted to abstract some of your re-usable code into a custom Hook, you might imagine that the hook has it's own state inside. What if this hook called useSomeCustomHook was using two calls to useState?

function MyComponent() {
  const [count, setCount] = useState(0)
  const value = useSomeCustomHook() // might call useState twice inside it

  // ...
}

From React's perspective, they just when they call MyComponent() that they see three calls to useState. One for count and two that are perceivably inside useSomeCustomHook().

Custom hooks were one of the main driving reason hooks were created in the first place. The fact that using state with function components isn't restricted to one object and one instantiation of all state like with classes is what allows us to have components with local state such as count and them other local state from an custom Hooks.

Can I set state from state?

This type of question would only come from someone who had experience with class-based components. That's because it could be error-prone in classes to "set state from state" like this:

// In classes, this could be error prone:
this.setState({ count: this.state.count + 1 })

For many reasons I won't get into here, you were supposed to do a different version of the state setting API when you wanted to set state from state in classes (to avoid bugs):

// The alternative API was to pass a function that
// would receive the current state and return
// new state:
this.setState((currentState) => {
  return { count: currentState.count + 1 }
})

Without knowing too much about classes, just know the types of "state batching" problems that we were avoiding by doing this alternative API wouldn't be a problem in the first place with function-based components. So setting "state from state" is fine with function based components that use useState:

const [count, setCount] = useState(0)

function add() {
  // setting state from state
  setCount(count + 1)
}

The state-setting function returned from useState can still be passed a function which resembles the class-based way of doing it, but the nuances of why you might need this go beyond the scope of this article and just know that you probably won't need to do this often:

const [count, setCount] = useState(0)

function add() {
  setCount((currentCount) => currentCount + 1)
}

When do I use a reducer instead of useState

Here's the quick facts on useReducer vs useState:

  • They both make local state
  • You can use either one

Some will argue that useReducer has some intangible benefits over useState and I think they have some good points. I like using useReducer for certain types of complex state. But just know that neither one has an aspect to its API that is impossible to solve with the other API. Here's what one of the owners of React Training had to say about it:

In other words, Michael is not saying he always uses an object to manage his state. He's just saying that when he has the type of state that most would consider a good use-case for useReducer, he just uses an object with useState instead -- the very thing that we just said to watch out for above.

But that's okay, he knows what he's doing!

We just want you to be aware that you have to manage the "merging" of your state on your own when it comes to function-components vs the older class ones.

How do I "share" my local state with other components?

Oh boy, this is kind of a huge topic. The short answer is you need to lift state.

Let's say I had a little tree-structure where the main App was the owner component of the <Count /> component we've been making and also a new <Report /> component which reports what the current count is:

function App() {
  return (
    <div>
      <Count />
      <Report />
    </div>
  )
}

With the example above, the state for count is inside the <Count /> component and is not being shared anywhere else.

To lift state, we would do this:

function App() {
  // This state has been moved "lifted" up one level:
  const [count, setCount] = useState(0)
  return (
    <div>
      <Count count={count} setCount ={setCount } />
      <Report count={count} />
    </div>
  )
}

The <Count /> component might look more like this now:

function Counter({ count, setCount }) {
  // const [count, setCount] = useState(0)
  
  function add() {
    setCount(count + 1)
  }
  
  // ...
}

It doesn't have it's own state, it has props instead. The add function doesn't care where count and setCount come from. It just needs them to exist. When the button is clicked and add calls setCount and the new series of steps that takes place to update the UI is as follows:

  1. Since setCount came from a useState in App, it's now the component that gets a re-render.
  2. On the re-render, we get the latest state from useState and then pass it down as props to other components.
  3. Components get re-rendered whenever they experience a state change or if their owner component gets re-rendered and therefore might be passing the child components new props.
  4. Therefore, both the Report and Count components get re-rendered to return new descriptions of their UI.

These are all fundamental React concepts -- to lift state in order for two components to share it. From here though, it can turn into a much bigger conversation about how too much lifting state can lead to "prop drilling" and how prop drilling can be avoided with context and maybe you should be using Redux or MobX to handle global state and only use local state sometimes, etc, etc.

But that's all for another time 😎

Did you like this content?

We have regularly scheduled public workshops for beginner though advanced topics. All workshops are remote and your company might help you pay for it, just ask! We also conduct corporate trainings if you need more of a personalized experience.

Attend a Public Workshop
Loading...