Edited at

react-hooksのuseStateでfunctionを管理させたい場合のTips

react-hooksのuseStateはこれまでのstateのように値を保持してくれる重要な関数だ。

例えば単純なカウンターならこんな具合になるだろう

const useCounter = () => {

const [count, setCounter] = useState(0)
return { count, setCounter }
}
export const MyApp = () => {
const { count, setCounter } = useCounter()
return (
<div>
<div>{count}</div>
   <button onClick={() => setCounter(count + 1)}>+</button>
</div>
)
}

このuseStateに関数を保持させたい場合、ちょっと注意が必要になる。

例えばこんな風に書くと、意図しない挙動になるだろう。

const initialHelloFn = () => {

console.log("initial")
}
const useCounter = () => {
const [count, setCounter] = useState(0)
const [helloFn, setHelloFn] = useState(initialHelloFn)
// このnewFnが何度も呼び出される
const newFn = useCallback(() => {
console.log("hello!", count)
}, [count])
useEffect(() => {
setHelloFn(newFn)
}, [newFn, setHelloFn])
console.log(helloFn)
return {
count,
setCounter,
helloFn
}
}

export const MyApp = () => {
const { count, setCounter, helloFn } = useCounter()
return (
<div>
<div>{count}</div>
<button onClick={() => setCounter(count + 1)}>+</button>
<button onClick={() => helloFn()}>hello</button>
</div>
)
}

おそらくこのようにすると、hello! 0のような出力が大量に出てしまうだろう


正しく動かす場合はどうするか?

{fn: 管理したい関数} のようにobjectで管理する。

const useCounter = () => {

const [count, setCounter] = useState(0)
// {fn: Function}などobjectの形にラップする
const [helloFn, setHelloFn] = useState({ fn: initialHelloFn })
const newFn = useCallback(() => {
console.log("hello!", count)
}, [count])

useEffect(() => {
setHelloFn({ fn: newFn })
}, [newFn, setHelloFn])

return {
count,
setCounter,
helloFn: helloFn.fn
}
}


なぜこうする必要があるか?

結論から言えばuseStateから返ってくるsetFooのようなハンドラーはFunctional Updateに対応しているため、単純に関数を渡してしまうとFunctional Updateとして処理されてしまうからになる。

https://reactjs.org/docs/hooks-reference.html#functional-updates

Functional Update自体は便利で、下記のように、現在の値を引き継がずに関数だけで利用することができる

<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>

これはhooksで新しく入ったわけではなく、React.Component.setStateにも同様の機能が存在していたものだ

https://reactjs.org/docs/react-component.html#setstate

ただComponentの場合はstate自体がobjectなのでほとんどこのような引っかかり方をすることは無かった。useStateが単純な値を格納するものとして利用できてる分、この点は気をつけるべき部分だろう