Part 2 · コンポーネントと props
props は書き換えられない
書き換えたら何が起きるのか、実際に壊してみる
props で値を受け取れるようになると、次に必ずこう考えます。「受け取った値を書き換えればいいのでは」と。
これはうまくいきません。しかもエラーも警告も出ません。 ただ、押しても何も起こらないだけです。
この章では、なぜそうなるのかと、では何をすればいいのかを見ます。
書き換えても、何も起きない
受け取った count を、ボタンを押したときに 1 増やしています。 いかにも動きそうですが、押してみてください。
何度押しても 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 に渡しているのは関数で、 子はそれをいつ呼ぶかを決めるだけです。
今度はちゃんと増える
子のコードを見ると、書き換えている場所がどこにもありません。 受け取って表示し、押されたら渡された関数を呼ぶ。それだけです。
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)} />;
}