知っておくべき動作があったのでメモとして残しておきます。
もともとは次の記事に記載されていた挙動を自分でも確認しようと思って
いろいろ試したときに発見しました。
[破壊的メソッドとミュータブル・イミュータブル [React,Javascript] - Qiita]
(https://qiita.com/takeiin/items/448d76ba1cd65dbc585a)
動作確認コード
create-react-app で作成したあとに次のように記載します。
import logo from './logo.svg';
import './App.css';
import { useState } from "react";
function App() {
const [num, setNum] = useState([0]);
// // 1.正常動作
// const addNum = () => {
// num.push(1);
// setNum([...num])
// };
// // 2.正常動作
// const addNum = () => {
// setNum((preNum) => {
// const newNum = [...preNum];
// newNum.push(1);
// return newNum;
// });
// };
// 3.異常動作
const addNum = () => {
setNum((preNum) => {
preNum.push(1);
console.log({preNum})
return [...preNum];
});
};
return (
<>
<button onClick={addNum}>1増えるよ</button>
<p>{num}</p>
</>
);
}
export default App;
useState取得関数にuseState取得値を指定する方法と、useState取得関数に関数を渡す方法とがあります。Reactのドキュメントのここに記載があります。
[関数型の更新 フック API リファレンス – React]
(https://ja.reactjs.org/docs/hooks-reference.html#functional-updates)
正常動作、とコメントに記載したコードは正常に動作します。動作結果は次のようになります。
0
0 1
0 1 1
0 1 1 1
異常動作、とコメントに記載したコードは次のようになります。ボタンを押すごとに2回pushが実行されてしまいます。
0
0 1
0 1 1 1
0 1 1 1 1 1
しかも内部に仕込んでいるconsole.logが1回しか呼び出されていないのに、2回pushされている動作になっていて「何これ?」と非常に驚きました。
React useState の所に解説がありました。
QiitaのQAで聞いて教えてもらいました。
[[Q&A] ReactのuseStateを使ったstate更新関数に関数を渡したときの変な挙動について知りたい - Qiita]
(https://qiita.com/standard-software/questions/d97aa97479d66c67204a#answer-7cbb3bb42546f88b66cb)
Reactのドキュメントのこちらに説明があります。
[strict モード – React]
(https://ja.reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects)
strict モードでは自動的には副作用を見つけてはくれませんが、それらの副作用をほんの少し決定的にすることによって特定できる助けになります。これは、以下の関数を意図的に 2 回呼び出すことによって行われます。
補足
React 17 以降で、React は console.log() のようなコンソールメソッドを自動的に変更し、ライフサイクル関数の 2 回目のコールでログが表示されないようにします。これにより特定のケースで意図しない動作を引き起こすことがありますが、回避策も存在します。
なるほどそういう仕組なんですね。
回避策は
const console_log = console.log;
として、内部で console.log ではなく console_log を使えという単純なもの。
これを踏まえて調べてみると、create-react-app で作成したプロジェクトの index.js は次のようになっていました。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>
,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
確かに StrictMode というのが適用されていて、こんな挙動になったというわけ。
React.StrictMode を解除すれば[3.異常動作]の書き方をしても発生しなくなっていた。がそのままでいいこともなく[1.正常動作][2.正常動作]のどちらかの書き方にしておいたほうがよいです。
[3.異常動作] での書き方は、渡された関数の引数は更新前のstateの値なので、その値に対してpushして変更をかけると、よくない副作用を起こすということらしい。
気をつけてコード書いておきましょう。