DEV Community

Lúcás Meier
Lúcás Meier

Posted on • Originally published at cronokirby.github.io

React Pitfalls: useState initialization

Original

Summary

This is a quick post about a "gotcha" I encountered recently in a React application.
This involved the use of React's useState hook, which had a subtle difference
between how I thought the hook worked, and how it actually worked.

React Hooks

If you're already familiar with hooks in React, feel free to skip this section. This
is just a recap for those out of the loop.

Although the first real framework I worked with was Vue (I don't count the one application
I built with JQuery as having used a framework), I've been using React a ton lately, and
it's really been growing on me. There's a lot of advantages to being the "top dog" in the
framework game, and React's popularity gives you access to a large ecosystem, and great
patterns for working on frontend applications.

One thing I really like about React is the recent "hooks" feature.

Previously, react distinguished between function components, which took in some parameters,
called props, and returned some HTML to be rendered, e.g.

function TitleCard({ name }) {
  return <p>Hi my name is {name}</p>;
}
Enter fullscreen mode Exit fullscreen mode

You also had class components, which at first resemble function components:

class TitleCard extends React.Component {
  render() {
    return <p>Hi my name is {this.props.name}</p>;
  }
}
Enter fullscreen mode Exit fullscreen mode

But class components also have a lot of other features in addition to just rendering some data.
Notably, they have access to state:

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 0};
  }

  render() {
    return (
      <div>
        <p>{this.state.count}</p>
        <button onClick={() => this.state.count++}>+</button>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

This component will render a count, and clicking on the button inside that component will increment
the state of that component.

Having a clear way to use state, and other effects like network requests, etc was something missing
from function components. That's where hooks come in. Hooks, in brief, allow you to use
these things in function components.

For example, if we wanted to take our Counter component from the previous example
using a function component with hooks, it'd look like this:

function Counter() {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The hook provides us with two values: count, and setCount. The idea is that
count always holds the current value of the state, and setCount allows us to change
this value. The cool thing is that React will always "do the right thing" and magically
update this value and rerender the component if necessary. Neat!

Rerendering

One of the core principles when working with React is that the framework strives
to make sure that the values a component logically has is always reflected on screen.
This means that if some value changes, because of a hook, or because one of the props changed,
then a rerender needs to happen to make sure that what's on screen matches what's "in the code".

For example, let's split our counter in two:

function ShowCounter({ count, setCount }) {
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}

function Counter() {
  const [count, setCount] = React.useState(0);
  return <ShowCounter count={count} setCount={setCount}/>
}
Enter fullscreen mode Exit fullscreen mode

Now the Counter component is the one actually holding the state, and the ShowCounter component
is just a simple function that shows the data given to it. In order to make sure that the user is
always seeing the actual value of the counter, React has to rerender the ShowCounter component
whenever the count passed to it changes value.

This means that React will end up executing the code of the ShowCounter function again. This
is why its important to avoid firing off network requests without useEffect inside
a function component, because you only want to do certain effects when they need to be run,
and not just when the component frivously rerenders.

State is initialised only once

Now we come to the main pitfall I want to talk about in this post.

Let's allow the user set a value for the counter:

function Counter({ initial }) {
  const [count, setCount] = React.useState(initial);
  return <ShowCounter count={count} setCount={setCount}/>;
}
Enter fullscreen mode Exit fullscreen mode

Based on our previous model, if initial changes, then the component has to rerender,
and so useState gets called with initial, and so the count becomes initial, right?

Well it turns out that that's not what happens. In fact with the way useState works,
the initial value matters only the first time a component renders. After that, it will
preserve the state between renders.

This means that we need to do this instead:

function Counter({ initial }) {
  const [count, setCount] = React.useState(0);
  React.useEffect(() => setCount(initial), [initial]);
  return <ShowCounter count={count} setCount={setCount}/>;
}
Enter fullscreen mode Exit fullscreen mode

We haven't gone over the useEffect hook in detail, but the way it works is that it executes
the callback function only if the values inside the array have changed. So here it will set the counter
to an initial value, but only when the initial value changes. This is the correct way to do
something like this.

Basically, to avoid this pitfall, you never want to have anything dynamic inside the call to useState.

Why is this the case?

Well, remember how we went over how React "tries to do the right thing". Well, it turns out that
by doing things this way, you actually preserve state between rerenders, which is generally the behavior you want.

For example, let's say we had something like this:

function Counter({ name }) {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <p>{name}</p>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now we're showing a name in addition to the value of the count. We want to rerender if the count or the name
changes, since we want the user to see what the current value actually is, but we don't want the count to be
lost just because the name changed. That's why it makes sense for useState to preserve the
state between rerenders.

It'd require a lot more code to create the behavior of preserving state between rerenders if
it didn't work that way, but it didn't take much effort with useEffect to do what we wanted
in the other case. You generally want to try and make the more common use case easy
with frameworks, don't you.

Further Reading

React Hooks

Top comments (2)

Collapse
 
jacobedawson profile image
Jacob E. Dawson • Edited

Nice and succinct, the dynamic initial value / rerender issue has bitten me a couple of times!

It is worth noting though that there are some issues with the useEffect technique. For example, if you have a parent component passing props to a child (e.g. a form), there is a chance that you can overwrite the child's state on rerender, such as removing a user's input selection.

Another possibility to force a rerender based on outside conditions changing is by using the React key prop - there's a good article by TK Dodo on that method here: tkdodo.eu/blog/putting-props-to-us...

He makes a good point that the useEffect method is possibly an antipattern, although it does apparently fix the issue initially, it could cause hard-to-debug errors down the track.

Worth keeping in mind!

Collapse
 
lh15 profile image
Leibel Hecht

This saved me so much time!!