関数の実行結果として別の関数が return されるようなケースがあります。

例えばイベントリスナーに処理を登録して、その結果としてリスナーを解除するための関数が return されるといった感じですね。外部のライブラリを使用していると遭遇することも多いと思います。

これを何も考えずに state に保持しようとすると気付かぬうちにハマりがちなのでこの記事を作成しました。

注意点:useState の初期値が関数だと Lazy initial state

関数を渡した場合は Lazy initial state 扱いになります。

state に関数がセットされるわけではなく、関数の return 値がセットされることに注意しましょう。

const getTwo = () => 1 + 1;
const [state, setState] = useState(getTwo); // 2

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:

https://reactjs.org/docs/hooks-reference.html#lazy-initial-state

補足

  • Lazy initial state を利用する場合、初期化関数は初回レンダリングでのみ実行されます。
  • Lazy initial state を利用しない場合、初期化関数は毎回のレンダリングで実行されます。

どちらもレンダリングの結果は変わりません。初期値は2回目以降のレンダリングでは無視されるからです。ただし Lazy initial state を利用しない場合は、2回目以降の無視される初期値も毎レンダリング生成していることになり、パフォーマンスが劣後します。

// Lazy initial state ではない初期化方法
// -> someExpensiveComputation は毎回のレンダリングで実行されるが、2回目以降のレンダリングでは戻り値が無視される。
const [state, setState] = useState(someExpensiveComputation());

// Lazy initial state な初期化方法
// -> someExpensiveComputation は初回のレンダリングでのみ実行される。
const [state, setState] = useState(() => someExpensiveComputation());

// 関数を渡していることを明示するために上記のように書きましたが、以下でも Lazy initial state です。
const [state, setState] = useState(someExpensiveComputation);

注意点:setState に関数を渡すと Functional updates

関数を渡した場合は Functional updates 扱いになります。

state に関数がセットされるわけではなく、関数の return 値がセットされることに注意しましょう。

const [state, setState] = useState(1);
const increment = (prev) => prev + 1;
setState(increment); // 2

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:

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

解決策:関数を useState に set したい場合は関数でラップする

state 関数をセットしたい場合、その関数を関数でラップすることで意図したように設定できます。

const getTwo = () => 1 + 1;
const getFour = () => 2 + 2;
const [state, setState] = useState(() => getTwo);
setState(() => getFour);

つまり以下のようになっているということです。

const [state, setState] = useState(() => () => 1 + 1); // state には () => 1 + 1 が登録されます。
setState(() => () => 2 + 2); // state は () => 2 + 2 で更新されます。

補足

その関数を配列なりオブジェクトに格納してセットしたり、あるいは useState ではなく useRef を使用しても解決は可能です。

以上

Lazy initial state も Functional updates も意図的に使う場合はその機能を認識しているのに、無意識に関数を登録しようとした場合にこの挙動がすっかり頭から抜け落ちていることがあります。常に頭の片隅に置いておきたいですね。

参考

How to store a function with the useState hook in React | by Hannes Petri | The Startup | Medium