How to store a function with the useState hook in React

Hannes Petri
The Startup
Published in
3 min readFeb 19, 2020

Should you ever find yourself in the fairly rare situation where you need to store a function in a state hook, chances are that you’ll lose a couple of hours to seemingly inexplicable crashes and error messages that don’t appear to make sense. At least I did. Keep reading to learn why it happens and how to solve it.

TL;DR Wrap your function in an anonymous function.

With state hooks, functions don’t seem to bite the same way other values do. But why?

Why is storing functions in a state hook difficult?

If you’re familiar with the useState hook in React, you know how to initialise and mutate state in a functional component:

// Initialise with 1000
const [myState, setMyState] = useState(1000);
// Mutate to 2000
setMyState(2000);

But what happens when the value you want to store isn’t the typical number or string, but a function? Keeping a function in component state isn’t something you do often, but it might occur for example when integrating with legacy code or a third-party library. Applying what you already know, you might try:

// Initialise with a function
const [myMultiplier, setMyMultiplier] = useState(x => 3 * x);
// Mutate to another function
setMyMultiplier(x => 5 * x);

After all, a function is a value like any other value. But none of the above lines work — for similar, yet distinct reasons. Let’s start with the first one.

Why can’t you initialise a state hook with a function?

React offers a way to to lazily initialise a state hook, ensuring that it only happens once. You do this by passing an argument-less function to useState which returns the initial value. Example:

// myExpensiveOperation will run on every render, but its return
// value will only be used once:
const [myEagerState] = setState(myExpensiveOperation());
// myExpensiveOperation will only be run once:
const [myLazyState] = setState(() => myExpensiveOperation());

This is the reason passing a function as the initial value doesn’t work — because React cannot tell apart the function you want to store from one used for lazy initialisation. Fortunately, the solution is easy — just pass the initial function lazily by wrapping it in an argument-less function:

const [myMultiplier] = useState(() => x => 3 * x);
const product = myMultiplier(10); // product is 30

The problem is now half solved! But…

Why can’t you mutate a state hook with a function?

What about mutations? Just like initialisation, passing a function to a state setter has a special meaning in React. It is used when you want to compute the next state “reductively,” that is, compute it from the current state. When you do this, the argument of the function you pass contains the current value. For example, a mutation that doubles the current value of a number state looks like this:

const [myState, setMyState] = useState(10);
setMyState(prevMyState => 2 * prevMyState);

Often it works equally well to compute the next state from the variable containing the current one, i.e. setMyState(2 * myState), but the reductive variant can be useful when performing a state mutation in the body of a callback hook or an effect hook, since you have one less variable to care about.

The same problematic situation occurs here — passing a function as the value to store invokes React’s special behaviour. It has no way of knowing that your intention is to store the function that you pass. But again, the simple fix is to wrap your function in an argument-less function:

const [myMultiplier, setMyMultiplier] = useState(() => x => 3 * x);
setMyMultiplier(() => x => 5 * x);

Technically, what you pass is a function that returns the next function based on the previous one. However, since the previous value isn’t relevant, you throw it away by making the outermost function argument-less. Actually, nothing prevents you from doing this with any kind of value:

const [myState, setMyState] = useState(10);// These mutations all have the same effect
setMyState(1000);
setMyState(prev => 1000);
setMyState(() => 1000);

Concluding words

Equipped with these solutions, you can store and mutate functions in state hooks without mysterious crashes and cryptic error messages. However, you should take care not to overuse it. In the majority of situations, there’s a better way — usually a callback hook.

Happy hooking!

--

--