reactでリストをレンダリングする際に気をつけること – 配列操作

reactでリストをレンダリングするためにstateに配列を持たせる。
この配列の操作を謝ると、リストは期待通りに更新されない。
reactを始めたばかりの頃はハマってしまうことも多いと思われる。

だめな例

App.tsx

import React, { useState, useCallback } from 'react';
import './App.css';

function App() {
  const [list, setList] = useState<string[]>([]);
  const [title,] = useState("title");

  // ボタンをクリックするごとにリストに1行追加
  const addList = useCallback(() => {
    const item: string = Date.now().toString();
    list.push(item);
    setList(list);
  }, [list]);

  return (
    <div className="App">
      <button onClick={addList}>ADD LIST</button>
      <div>{title}</div>
      <div>
        {list.map((item, index) => (<div>{item}</div>))}
      </div>
    </div>
  );
}

export default App;

※本来keyを設定すべきだがここでは省略

このソースではボタンをクリックするごとにstateに持たせたlistにpushを行い、その内容がレンダリングされることを期待している。

しかし実際はレンダリングが行われない。
これは、setListで単に同じlistを再設定しているためである。

list内の要素はpushにより追加されているが、listそのものは変更されていない。この場合reactはレンダリングを行わない。レンダリングの判断では浅いチェックしか行われない。

更新される例

import React, { useState, useCallback } from 'react';
import './App.css';

function App() {
  const [list, setList] = useState<string[]>([]);
  const [title,] = useState("title");

  // ボタンをクリックするごとにリストに1行追加
  const addList = useCallback(() => {
    const item: string = Date.now().toString();
    setList([...list, item]);
  }, [list]);

  return (
    <div className="App">
      <button onClick={addList}>ADD LIST</button>
      <div>{title}</div>
      <div>
        {list.map((item, index) => (<div>{item}</div>))}
      </div>
    </div>
  );
}

export default App;

※本来keyを設定すべきだがここでは省略

このように要素を1つ追加した新たな配列を作成してsetListに渡した場合は変更が検知されレンダリングされる。

配列以外にも、プロパティを持ったオブジェクトでも同様の注意が必要である。

だめな例(特殊)

import React, { useState, useCallback } from 'react';
import './App.css';

function App() {
  const [list, setList] = useState<string[]>([]);
  const [clickCount,setClickCount] = useState(0);

  // ボタンをクリックするごとにリストに1行追加
  const addList = useCallback(() => {
    const item: string = Date.now().toString();
    setList(oldList =>{
      oldList.push(item);
      return [...oldList];
    });
    setClickCount(oldCount => oldCount + 1);
  }, []);

  return (
    <div className="App">
      <button onClick={addList}>ADD LIST</button>
      <div>{clickCount}回クリックしました</div>
      <div>
        {list.map((item, index) => (<div>{item}</div>))}
      </div>
    </div>
  );
}

export default App;

※本来keyを設定すべきだがここでは省略



setListには、新たなstateを得るための関数を渡すこともできる。しかしこの実装だと手元環境では期待通りに動作しなかった。

ボタンを押すごとに(初回を除いて)リストが2行ずつ増えていくのである。


調べてみると、setListで渡したlistを更新するための関数が2回呼び出されていた。加えてoldList.push()により元々のリストの方に要素を追加してしまっているために問題が生じている。これは、

    setList(oldList =>{
      return [...oldList, item];
    });

このように元々のリストを破壊せずに新たなリストを返すように関数を実装すれば、関数が何度呼ばれようとも正しく動作する。

stateで管理するオブジェクトは不変なものとして扱い、これを直接破壊するような操作は避けるべきだろう。また、冪等性のある関数にしておくのが無難と思われる。

バージョン16.13.1でこの動作を確認したが、バージョンによってこのあたりの挙動は変わるかも知れない。


コメント

タイトルとURLをコピーしました