byxor
byxor

Reputation: 6349

Why does the 'useState' hook invoke the initial state when it's a function reference?

React has a hook called useState, which is used when adding state to functional components.

The Hooks API Reference states:

useState:

const [state, setState] = useState(initialState);

Returns a stateful value, and a function to update it.

During the initial render, the returned state (state) is the same as the value passed as the first argument (initialState).

The setState function is used to update the state. It accepts a new state value and enqueues a re-render of the component.

The React Documentation states:

What do we pass to useState as an argument?

The only argument to the useState() Hook is the initial state. Unlike with classes, the state doesn’t have to be an object. We can keep a number or a string if that’s all we need. In our example, we just want a number for how many times the user clicked, so pass 0 as initial state for our variable. (If we wanted to store two different values in state, we would call useState()twice.)

Unexpected behaviour:

However, I've noticed some strange, seemingly undocumented, behaviour.

If I try to use the useState hook to store a function as state, react will invoke the function reference. e.g.

const arbitraryFunction = () => {
    console.log("I have been invoked!");
    return 100;
};

const MyComponent = () => {

    // Trying to store a string - works as expected:
    const [website, setWebsite] = useState("stackoverflow"); // Stores the string
    console.log(typeof website);                             // Prints "string"
    console.log(website);                                    // Prints "stackoverflow"

    // Trying to store a function - doesn't work as expected:
    const [fn, setFn] = useState(arbitraryFunction);         // Prints "I have been invoked!"
    console.log(typeof fn);                                  // Prints "number" (expecting "function")
    console.log(fn);                                         // Prints "100"

    return null; // Don't need to render anything for this example...
};

When we call useState(arbitraryFunction), react will invoke arbitraryFunction and use its return value as the state.

As a workaround:

We can store functions as state by wrapping our function reference in another function. e.g.

const [fn, setFn] = useState(() => arbitraryFunction)

I haven't yet come across any real-world reasons to store functions as state, but it seems weird that somebody made the explicit choice to treat function arguments differently.

This choice can be seen in multiple places throughout the React codebase:

initialState = typeof initialArg === 'function' ? initialArg() : initialArg;

Why does this seemingly undocumented feature exist?

I can't think of any good reasons why anybody would want/expect their function reference to be invoked, but perhaps you can.

If this is documented, where is it documented?

Upvotes: 15

Views: 7326

Answers (1)

tkausl
tkausl

Reputation: 14269

This is documented here:

Lazy initial state

The initialState argument is the state used during the initial render. In subsequent renders, it is disregarded. If the initial state is the result of an expensive computation, you may provide a function instead, which will be executed only on the initial render:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

Passing a callback to setState also calls the callback, but for a different reason:

Functional updates

If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value. Here’s an example of a counter component that uses both forms of setState:

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  );
}

Upvotes: 16

Related Questions