props は書き換えられない

Part 2 · コンポーネントと props

props は書き換えられない

書き換えたら何が起きるのか、実際に壊してみる

props で値を受け取れるようになると、次に必ずこう考えます。「受け取った値を書き換えればいいのでは」と。

これはうまくいきません。しかもエラーも警告も出ません。 ただ、押しても何も起こらないだけです。

この章では、なぜそうなるのかと、では何をすればいいのかを見ます。

書き換えても、何も起きない

受け取った count を、ボタンを押したときに 1 増やしています。 いかにも動きそうですが、押してみてください。

props を書き換えようとするうまくいかない例

何度押しても 0 のまま

0

数字は動きません。ただし変数の中身は実際に増えています。 ブラウザのコンソールを開くと、増えた値が出力されているのが分かります。

つまり計算は成功していて、画面への反映だけが起きていないのです。

broken-props.tsx

"use client";

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

function Counter({ count }: { count: number }) {
  const increase = () => {
    // 受け取った値をここで書き換えている。
    // 変数の中身は増えるが、React は何も知らないので画面は変わらない。
    // (この書き方は lint に止められる。ここでは説明のために黙らせている)
    // eslint-disable-next-line react-hooks/immutability
    count = count + 1;
    console.log("count は", count, "になった。でも画面はそのまま");
  };

  return (
    <div className="flex items-center gap-3">
      <span className="font-mono text-lg font-semibold tabular-nums">
        {count}
      </span>
      <Button size="sm" onClick={increase}>
        増やす
      </Button>
    </div>
  );
}

export function BrokenProps() {
  return <Counter count={0} />;
}

なぜ反映されないのか

Part 1 でやったことを思い出してください。React は状態が変わったときに画面を描き直します

ここで書き換えているのは、関数の中のただの変数です。 React はその存在すら知りません。知らない値が変わっても、描き直すきっかけになりません。

しかも、仮に描き直されたとしても意味がありません。 描き直すというのは関数をもう一度実行することなので、count はまた親から渡された 0 に戻ります。 書き換えた結果はどこにも残りません。

broken-props.tsx

"use client";

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

function Counter({ count }: { count: number }) {
  const increase = () => {
    // 受け取った値をここで書き換えている。
    // 変数の中身は増えるが、React は何も知らないので画面は変わらない。
    // (この書き方は lint に止められる。ここでは説明のために黙らせている)
    // eslint-disable-next-line react-hooks/immutability
    count = count + 1;
    console.log("count は", count, "になった。でも画面はそのまま");
  };

  return (
    <div className="flex items-center gap-3">
      <span className="font-mono text-lg font-semibold tabular-nums">
        {count}
      </span>
      <Button size="sm" onClick={increase}>
        増やす
      </Button>
    </div>
  );
}

export function BrokenProps() {
  return <Counter count={0} />;
}

値を持っている側が変える

では、どうすればいいのか。答えは単純です。値を持っている側が変える。それだけです。

子は「押されました」と伝えるだけにして、 実際に値を変えるのは親の仕事にします。

値を持っているのはここだけ値を渡す押されたと伝える受け取って表示するだけ
子は値を書き換えない。書き換えるのは、値を持っている親の仕事

子に渡すものが 2 つになります。表示するための値と、押されたことを伝えるための関数です。

<Counter count={count} onIncrease={() => setCount(count + 1)} />

Part 0 でやった「関数を値として渡す」が、ここで効いてきます。onIncrease に渡しているのは関数で、 子はそれをいつ呼ぶかを決めるだけです。

親が値を持ち、子は伝えるだけ直した例

今度はちゃんと増える

0

子のコードを見ると、書き換えている場所がどこにもありません。 受け取って表示し、押されたら渡された関数を呼ぶ。それだけです。

fixed-props.tsx

"use client";

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

// 子は値を受け取り、押されたことを伝えるだけ。自分では書き換えない
function Counter({
  count,
  onIncrease,
}: {
  count: number;
  onIncrease: () => void;
}) {
  return (
    <div className="flex items-center gap-3">
      <span className="font-mono text-lg font-semibold tabular-nums">
        {count}
      </span>
      <Button size="sm" onClick={onIncrease}>
        増やす
      </Button>
    </div>
  );
}

// 値を持っているのは親のほう
export function FixedProps() {
  const [count, setCount] = useState(0);

  return <Counter count={count} onIncrease={() => setCount(count + 1)} />;
}

なぜこの形が良いのか

面倒に見えるかもしれませんが、この形には大きな利点があります。

値の出どころが 1 か所に決まるということです。

もし子が自由に書き換えられたら、 その値がどこで変わったのかを探すのに、 関わっている部品を全部見る必要が出てきます。 部品が 10 個あれば、容疑者は 10 人です。

値を変えられるのが持ち主だけなら、おかしくなったときに見る場所は 1 か所です。 React の書き方が窮屈に感じられる部分の多くは、 この「あとで困らないため」の設計です。

fixed-props.tsx

"use client";

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

// 子は値を受け取り、押されたことを伝えるだけ。自分では書き換えない
function Counter({
  count,
  onIncrease,
}: {
  count: number;
  onIncrease: () => void;
}) {
  return (
    <div className="flex items-center gap-3">
      <span className="font-mono text-lg font-semibold tabular-nums">
        {count}
      </span>
      <Button size="sm" onClick={onIncrease}>
        増やす
      </Button>
    </div>
  );
}

// 値を持っているのは親のほう
export function FixedProps() {
  const [count, setCount] = useState(0);

  return <Counter count={count} onIncrease={() => setCount(count + 1)} />;
}

理解できたか確かめる

確認クイズ

子で props を書き換えても画面が変わらないのはなぜ?

確認クイズ

子から値を変えたいとき、親は何を渡す?

fixed-props.tsx

"use client";

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

// 子は値を受け取り、押されたことを伝えるだけ。自分では書き換えない
function Counter({
  count,
  onIncrease,
}: {
  count: number;
  onIncrease: () => void;
}) {
  return (
    <div className="flex items-center gap-3">
      <span className="font-mono text-lg font-semibold tabular-nums">
        {count}
      </span>
      <Button size="sm" onClick={onIncrease}>
        増やす
      </Button>
    </div>
  );
}

// 値を持っているのは親のほう
export function FixedProps() {
  const [count, setCount] = useState(0);

  return <Counter count={count} onIncrease={() => setCount(count + 1)} />;
}

この章のまとめ

  • props は読み取り専用。子が書き換えても画面は変わらない
  • エラーも警告も出ず、ただ動かないので気づきにくい
  • 値は持っている側が変える。子は「押された」と伝えるだけ
  • 値が下へ、知らせが上へ。この一方向の流れが、おかしくなったときに見る場所を 1 か所に保つ

fixed-props.tsx

"use client";

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

// 子は値を受け取り、押されたことを伝えるだけ。自分では書き換えない
function Counter({
  count,
  onIncrease,
}: {
  count: number;
  onIncrease: () => void;
}) {
  return (
    <div className="flex items-center gap-3">
      <span className="font-mono text-lg font-semibold tabular-nums">
        {count}
      </span>
      <Button size="sm" onClick={onIncrease}>
        増やす
      </Button>
    </div>
  );
}

// 値を持っているのは親のほう
export function FixedProps() {
  const [count, setCount] = useState(0);

  return <Counter count={count} onIncrease={() => setCount(count + 1)} />;
}