Aimless Blog

React Hooksで配列の要素を削除したあとの再描画について

Tag:
react

削除ボタンを設置したToDoリストやテーブルでボタンを押しても再描画されないときの解決法。

クラス型コンポーネントの場合

削除に関係する部分のコードです。

class TableList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      items: [],
    }

    handleDelete(id, index) {
      const data = { id: id };
      axios
        .delete("http://localhost:5000/api/mydata", { data: data })
        .then(response => {
          console.info(response.data.message);
          this.state.items.splice(index, 1);
          this.setState({ items: this.state.items });
        })
    }
    render() {
    return (
      <table>
        {this.state.items.map((item, index) => (
          <tr key={item.id}>
            <td>{item.name}</td>
              <button onClick={() => this.handleDelete(item.id, index)}>
              Delete
              </button>
            </tr>
        ))}
      </table>
      )
    }
  }

クラス型コンポーネントの場合はsplice(index, 1)のあとにthis.setStateで配列を上書きするだけ。

React Hooksの場合

Hooksも同じだろうと思ってこのようなコードを書く

function TableList() {
  const [items, setItems] = useState([]);
  
  const handleDelete = (id, index) => {
    const data = { id: id };
    axios
      .delete("http://localhost:5000/api/mydata", { data: data })
      .then(response => {
        console.info(response.data.message);
        items.splice(index, 1);
        setItems(items);
      })
  }

      return (
        <Button onClick={() => handleDelete(item.id, index)}>
          Delete
        </Button>
      )
  }

が、うまく行かない。データは削除されているけどリロードをしないと反映されない。 どうしたらいいのかは、スタックオーバーフローにありました。

Reactドキュメントのフック API リファレンス state 更新の回避によると

現在値と同じ値で更新を行った場合、React は子のレンダーや副作用の実行を回避して処理を終了します。(React は Object.is に よる比較アルゴリズム を使用します)

と書いてあります。

Object.isとはなんぞや?と調べてみると

Object.is() メソッドは 2 つの値の同一性を判定します。

との事。items.splice(index, 1);の後にsetItems(items);でstate更新をしてもObject.isの判定ではtrueとなってしまうので再描画されないみたい。
なので、このようにして再描画されるようにします。

const newItems = [...items];
newItems.splice(index, 1);
setItems(newItems);

[...items]の...って何?と調べるとスプレッド構文とかいうものでオブジェクトや配列を展開するものらしい。
つまり、

  1. itemsの中身をスプレッド構文で展開してnewItemsに入れる
  2. newItemsをspliceして配列の要素を削除
  3. itemsとnewItemsはfalseなのでstateが更新されて再描画される

state 変数は 1 つにすべきですか、たくさん使うべきですか?で...が使われていてreact独自の何かだと思って調べずに使っていたけどちゃんと意味があるのですね(当たり前)

フック API リファレンス 関数型の更新の補足に

補足 クラスコンポーネントの setState メソッドとは異なり、useState は自動的な更新オブジェクトのマージを行いません。この動作は 関数型の更新形式をスプレッド構文と併用することで再現可能です:

> setState(prevState => {
> // Object.assign would also work
> return {...prevState, ...updatedValues};
> });

と書いてあったコードを見てこれも行けるのでは?と思い、

items.splice(index, 1);
setPage(items => ({ ...items, items }))

こう書いても上手くいきました。

ページネーション機能のあるテーブル (3月3日追記)

ページネーション機能のあるテーブルで2ページ目以降が上の2つのコードだと再描画されなかったので、
下記のようにしました。

const index = items.findIndex(item => item.id === id);
if (~index) items.splice(index, 1);
setPage(items => ({ ...items, items }));

参考

Reactドキュメントに

要素の並び順が変更される可能性がある場合、インデックスを key として使用することはお勧めしません。パフォーマンスに悪い影響を与え、コンポーネントの状態に問題を起こす可能性があります。

とあるので、findIndexで条件(ここではid)に一致したインデックスを返して、その要素を削除するこの方法がベターなやり方かも。 まぁ他にいい方法が思いつかないのでこれ採用。