オブジェクトと配列の更新

Part 4 · 状態 (state)

オブジェクトと配列の更新

push してはいけない。なぜ毎回新しく作るのか

Part 0-1 で、こう書きました。

const なのに中身が変わるのは、React で最もよく起きるバグの原因のひとつ」。

その回収をします。state がオブジェクトや配列のとき、中身を書き換えても画面は変わりません

push しても画面が変わらない

配列に要素を足すなら push。 そのあと setItems も呼んでいます。 動きそうに見えますが、押してみてください。

push してから setItems するうまくいかない例

何度押しても表示は変わらない

りんご

変わりません。ただし配列の中身は実際に増えています(コンソールに出ています)。

mutate-array.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

const fruits = ["みかん", "ぶどう", "もも", "いちご"];

export function MutateArray() {
  const [items, setItems] = useState(["りんご"]);

  const add = () => {
    // 配列そのものに足している。中身は増えるが、配列は同じもののまま
    items.push(fruits[items.length % fruits.length]);

    // 同じ配列を渡しても、React は「変わっていない」と判断する
    setItems(items);

    console.log("中身は", items, "になっている。でも画面は変わらない");
  };

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">{items.join(", ")}</div>
      <Button size="sm" onClick={add}>
        追加する
      </Button>
    </div>
  );
}

React は「同じものか」だけを見る

React は setItems を受け取ったとき、新しい値と前の値を比べます。 このとき中身を 1 つずつ見比べたりはしません同じものかどうかだけを見ます。

push は、配列そのものを別のものに置き換えません。同じ配列の中身が増えるだけです。 Part 0-1 の言い方をすれば、 箱の中身を変えただけで、箱は同じもののままです。

だから React から見ると「前と同じものが渡ってきた」ことになり、描き直す必要なしと判断されます。

mutate-array.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

const fruits = ["みかん", "ぶどう", "もも", "いちご"];

export function MutateArray() {
  const [items, setItems] = useState(["りんご"]);

  const add = () => {
    // 配列そのものに足している。中身は増えるが、配列は同じもののまま
    items.push(fruits[items.length % fruits.length]);

    // 同じ配列を渡しても、React は「変わっていない」と判断する
    setItems(items);

    console.log("中身は", items, "になっている。でも画面は変わらない");
  };

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">{items.join(", ")}</div>
      <Button size="sm" onClick={add}>
        追加する
      </Button>
    </div>
  );
}

新しく作って渡す

Part 0-4 でやったスプレッド構文の出番です。 元の配列は触らず、中身を展開した新しい配列を作ります。

// ✕ 同じ配列のまま
items.push("みかん");
setItems(items);

// ○ 新しい配列を作って渡す
setItems([...items, "みかん"]);
新しい配列を渡す直した例

追加も削除も反映される

りんご

これで React は「別のものが来た」と判断し、描き直します。

new-array.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

const fruits = ["みかん", "ぶどう", "もも", "いちご"];

export function NewArray() {
  const [items, setItems] = useState(["りんご"]);

  const add = () => {
    // 元の配列は触らず、中身を展開した新しい配列を作って渡す
    setItems((current) => [...current, fruits[current.length % fruits.length]]);
  };

  const removeLast = () => {
    // slice は元を変えずに、切り取った新しい配列を返す
    setItems((current) => current.slice(0, -1));
  };

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        {items.length === 0 ? "(空)" : items.join(", ")}
      </div>
      <div className="flex gap-2">
        <Button size="sm" onClick={add}>
          追加する
        </Button>
        <Button size="sm" variant="outline" onClick={removeLast}>
          最後を消す
        </Button>
      </div>
    </div>
  );
}

よく使う書き換え方

配列を扱うメソッドには、元を変えるもの新しく返すものがあります。 React で使うのは後者です。

// 使わない(元の配列を変えてしまう)
push  pop  shift  unshift  splice  sort  reverse

// 使う(新しい配列を返す)
map  filter  slice  concat  スプレッド構文

操作ごとの定番はこうなります。

// 末尾に足す
setItems([...items, newItem]);

// 先頭に足す
setItems([newItem, ...items]);

// 条件に合うものを消す
setItems(items.filter((item) => item.id !== targetId));

// 特定のものだけ書き換える
setItems(
  items.map((item) =>
    item.id === targetId ? { ...item, done: true } : item
  )
);

最後の例では、配列だけでなく中のオブジェクトも新しく作っている点に注目してください。{ ...item, done: true } がそれです。書き換えたいものは、すべて新しく作ります。

オブジェクトも同じ

// ✕ 中身を書き換えても、同じオブジェクトのまま
user.name = "すずき";
setUser(user);

// ○ 新しいオブジェクトを作る
setUser({ ...user, name: "すずき" });
// user.address.city を変えたい場合
setUser({
  ...user,
  address: { ...user.address, city: "大阪" },
});

入れ子が深くなると、この書き方はつらくなります。 そうなったらstate の形そのものを見直すのが先です。 深い入れ子は、たいてい state の設計に無理がある合図です。

new-array.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

const fruits = ["みかん", "ぶどう", "もも", "いちご"];

export function NewArray() {
  const [items, setItems] = useState(["りんご"]);

  const add = () => {
    // 元の配列は触らず、中身を展開した新しい配列を作って渡す
    setItems((current) => [...current, fruits[current.length % fruits.length]]);
  };

  const removeLast = () => {
    // slice は元を変えずに、切り取った新しい配列を返す
    setItems((current) => current.slice(0, -1));
  };

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        {items.length === 0 ? "(空)" : items.join(", ")}
      </div>
      <div className="flex gap-2">
        <Button size="sm" onClick={add}>
          追加する
        </Button>
        <Button size="sm" variant="outline" onClick={removeLast}>
          最後を消す
        </Button>
      </div>
    </div>
  );
}

理解できたか確かめる

確認クイズ

items.push(x) のあとに setItems(items) しても画面が変わらないのはなぜ?

確認クイズ

配列から id が 3 の要素を消したい。正しいのは?

new-array.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

const fruits = ["みかん", "ぶどう", "もも", "いちご"];

export function NewArray() {
  const [items, setItems] = useState(["りんご"]);

  const add = () => {
    // 元の配列は触らず、中身を展開した新しい配列を作って渡す
    setItems((current) => [...current, fruits[current.length % fruits.length]]);
  };

  const removeLast = () => {
    // slice は元を変えずに、切り取った新しい配列を返す
    setItems((current) => current.slice(0, -1));
  };

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        {items.length === 0 ? "(空)" : items.join(", ")}
      </div>
      <div className="flex gap-2">
        <Button size="sm" onClick={add}>
          追加する
        </Button>
        <Button size="sm" variant="outline" onClick={removeLast}>
          最後を消す
        </Button>
      </div>
    </div>
  );
}

この章のまとめ

  • React は同じものかどうかだけを見る。中身は見比べない
  • push などは中身を変えるだけなので、変化として伝わらない
  • 書き換えたいときは新しく作って渡す
  • 使うのは map / filter / slice / スプレッド構文
  • スプレッドのコピーは 1 段目だけ。入れ子が深くなったら state の形を見直す

new-array.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

const fruits = ["みかん", "ぶどう", "もも", "いちご"];

export function NewArray() {
  const [items, setItems] = useState(["りんご"]);

  const add = () => {
    // 元の配列は触らず、中身を展開した新しい配列を作って渡す
    setItems((current) => [...current, fruits[current.length % fruits.length]]);
  };

  const removeLast = () => {
    // slice は元を変えずに、切り取った新しい配列を返す
    setItems((current) => current.slice(0, -1));
  };

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        {items.length === 0 ? "(空)" : items.join(", ")}
      </div>
      <div className="flex gap-2">
        <Button size="sm" onClick={add}>
          追加する
        </Button>
        <Button size="sm" variant="outline" onClick={removeLast}>
          最後を消す
        </Button>
      </div>
    </div>
  );
}