ReactのsetStateでstateの変更が反映されない

ReactのsetStateでstateの変更が反映されない

はじめに

React を使用して TODO リストを作成していた時に、setState の変更が反映されない事態に陥りました。

状況としてはコンポーネント間のデータの受け渡しをしていたのですが、

  1. 子コンポーネントから親コンポーネントにイベントを通知
  2. 親コンポーネントのデータを変更
  3. 子コンポーネントの状態も変更!

。。。。

されるはずだったのですが、なぜか変更されない!

結果としては初歩的なミスが原因でした。今回はそのハマった箇所と解決方法を記述していこうと思います。

結論: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 を避けることについてのブログ記事を読んでください。