概要:setStateでpropsが更新されてしまう問題
前提としてReduxなどのフレームワークは利用していませんのでフレームワーク上の挙動については未確認です。
React.jsでチェックボックスのリストなどを1つのコンポーネントとして定義して使いまわしたい場合。
オブジェクトの配列をpropsとして渡すと管理がしやすいと思うのですが、checkedの変更などでstateの状態を更新する際にpropsの値まで更新されてしまうという問題に苦戦しました。
この問題の解決方法が日本語でなかなか出てこない&求めていない情報が多く検索でヒットしてしまい解決するまでに苦戦してましたが、freeCodeCampで解決法を発見したので、備忘録の意味も込めて共有したいと思います。
結論のみ確認したい方は解決方法まで飛んでいたただければと思います。
Reactの状態管理についてはまだ理解が完全ではない部分もあって間違った認識などあったらご指摘頂けると幸いですm(_ _)m
propsを参照したロールバックができない
後述しますが、公式のドキュメントやググってヒットする内容でpropsとして受け取ったオブジェクト配列をstateとして扱って更新するとpropsまで更新されてしまいます。
ユーザーデータの編集の際に更新とキャンセルのボタンを設置したとしてもpropsが変更されてしまうのでpropsを参照したrollbackができなくなりますし、propsが更新されることはreactの作法に反しています。(間違っていたらすみません)
コンポーネントの例
例えば以下のようなチェックボックスのリストをRenderするコンポーネントがあったとします。
import React, { Component } from 'react'
class CheckboxList extends Component {
constructor(props) {
super(props);
const { checkboxes } = this.props;
this.state = {
checkboxes = checkboxes
}
}
handleChange(index) {
// ここに更新の処理が入る
}
render() {
const { checkboxes } = this.state;
const checkboxList = checkboxes.map((item, index) =>
<li key={index}>
<input
id={index}
type='checkbox'
value={item.value}
checked={item.checked}
onChange={() => {this.handleChange(index)}
/>
</li>
);
return (
<div>
<p>チェックボックスリスト</p>
<ul>{checkboxList}</ul>
</div>
);
}
}
// エクスポート
export default CheckboxList;
このコンポーネントに以下のようなオブジェクト配列をpropsとして渡します。
// Reactのimport
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
// コンポーネントのimport
import CheckboxList from "../path/to/CheckboxList";
const checkboxes = [
{
name: A,
checked: true,
value: 0,
},
{
name: B,
checked: false,
value: 1,
},
]
ReactDOM.render(
<CheckboxList
checkboxes={checkboxes}
/>,
document.getElementById('root')
);
受け取ったpropsをstateとして扱う場合に、更新方法を調べるといろいろな方法が見つかると思います。
- stateをコピーしてkey(index)を参照して上書きする方法
handleChange(index) {
let newState = this.state.checkboxes;
newState[index].checked = !newState[index].checked;
this.setState({
checkboxes: newState
});
}
- stateをsliceして更新する方法
handleChange(index) {
let newState = this.state.checkboxes.slice();
newState[index].checked = !newState[index].checked;
this.setState({
checkboxes: newState
});
}
上記のような方法でnewStateとしてstateを再定義してからsetStateで状態を更新しようとするとpropsまで変更されてしまいます。
コンポーネント内でstateを定義する場合は有効な方法かもしれませんが、propsで渡したオブジェクト配列をstateとして扱いたい場合はどの方法もpropsまで更新されてしまうので、propsを参照したロールバックなどはできません。
他にもsliceしたりspliceしたりで配列を書き換える方法などがありましたが、順番を担保できないのでリストの仕様には向いていません。
解決方法:文字列にしたstateをJSONに戻して更新する
引用元の通りですが、stateに保持しているオブジェクト配列を文字列に変換し、JSONに戻してから更新することでpropsを維持したままstateを変更できました。
let newState = JSON.parse(JSON.stringify(this.state.recipes));
引用元:Reactjs - Using setState to update a single property on an object
この方法でhandleChangeを定義すると以下のようになります。
handleChange(index) {
let newState = JSON.parse(JSON.stringify(this.state.checkboxes));
newState[index].checked = !newState[index].checked;
this.setState({
checkboxes: newState
});
}
恐らくですが、一度文字列にすることでreactからの参照を切ることができるので、新しい値としてstateを保持することができるということかと思います。
(これも違ってたらごめんなさい)
まとめ
調べている中でReact.jsは状態を更新したときに差分を再Renderするので、オブジェクトの配列などはパフォーマンスの低下(全てのチェックボックスを再レンダリングする)につながるので、推奨されていないような意見もありました。
ただ、僕としてはCheckboxのリストはこの記事で紹介したようにオブジェクトの配列を渡すだけでレンダリングしてくれるComponentにしたほうが管理も再利用もしやすくエコだと思ったのでこの方法を採用しています。
もっと良い方法をご存知の方がいたら是非教えていただけると嬉しいですm(_ _)m