はじめに
React を使用して TODO リストを作成していた時に、setState の変更が反映されない事態に陥りました。
状況としてはコンポーネント間のデータの受け渡しをしていたのですが、
- 子コンポーネントから親コンポーネントにイベントを通知
- 親コンポーネントのデータを変更
- 子コンポーネントの状態も変更!
。。。。
されるはずだったのですが、なぜか変更されない!
結果としては初歩的なミスが原因でした。今回はそのハマった箇所と解決方法を記述していこうと思います。
結論:props を state にコピーしてはダメ
修正前のコードがこちらです。constructor の中で state の初期値に props の値を直接代入しています。
そして、その state をタスク ID と共に親コンポーネントに渡しています。
子コンポーネントから渡ってきた state を元に isDone を反転させてものを、新しいオブジェクトとして親コンポーネントの data にセットしています。
// 子コンポーネント
constructor(props) {
super(props);
this.state = {
id: this.props.id,
text: this.props.text, // 親からpropsとして受け取った値を初期としてセット
isDone: this.props.isDone,
editMode: false
};
// 中略
}
// Todoリストの完了を通知する
handleClickToggleDone () {
// 親コンポーネントに通知する
this.props.onHandleDone(this.state.id, this.state.isDone)
}
// 親コンポーネント
// isDoneを変更するメソッド
callBackDone(id, data){
let newItem = this.state.data.map( item => {
if(item.id === id){
// 子コンポーネントから渡ってきた、isDoneの状態(true か false)を使用している
return Object.assign({} , item, {isDone: !data})
}
return item
})
// 新しいオブジェクトをセットする
this.setState({
data: newItem
})
}
そして、修正後のコードがこちらです。
子コンポーネントから通知するのはタスク ID のみとし、constructor からも除外しています。タスク ID は変化することは無いと思ったので初期化時に子コンポーネントの state として props を渡しています。
constructor(props) {
super(props);
this.state = {
id: this.props.id,
editMode: false
};
// 中略
}
// Todoリストの完了を通知する
handleClickToggleDone () {
// 親コンポーネントに通知する
this.props.onHandleDone(this.state.id)
}
// 親コンポーネント
// isDoneを変更するメソッド
callBackDone(id){
let newItem = this.state.data.map( item => { // mapメソッドは新しい配列を返す
// 子コンポーネントから渡って来たidと同じタスクだった場合は下記の処理を行う
if(item.id === id){
// 親コンポーネントで管理しているデータの状態を直接変更する
return Object.assign({} , item, {isDone: !item.isDone})
}
// 違うidだった場合は、mapで分解された要素をそのまま返却する
return item
})
// 新しいオブジェクトをセットする
this.setState({
data: newItem
})
}
props を state にコピーするのは駄目だということは、公式ドキュメントにも書かれていました。
補足
props を state にコピーしないでください。これはよくある間違いです。constructor(props) { super(props); // してはいけません this.state = { color: props.color }; }
この問題はそれが不要(代わりに this.props.color を直接使用することができるため)であり、バグの作成につながる(color プロパティの更新は state に反映されないため)ことです。
意図的にプロパティの更新を無視したい場合にのみ、このパターンを使用してください。 その場合は、プロパティの名前を initialColor または defaultColor に変更してください。
その後、必要に応じてキーを変更することで、コンポーネントにその内部の state を 「リセット」させることができます。 もしあなたが props に依存する何らかの state が必要だと思うなら、どうすればいいのか学ぶために私達の派生 state を避けることについてのブログ記事を読んでください。