本文へスキップ
children で切り離す

Part 8 · パフォーマンス

42 / 64

children で切り離す

memo を使わずに、描き直しの範囲を狭める

前の章で memo をやりました。 ですが、memo を使わずに描き直しを止める方法があります。

しかも新しい道具は要りません。使うのは Part 2 でやった children です。

知っている人と知らない人で、書くコードがはっきり変わる話です。

まず、ふつうに書いた場合

state を持つコンポーネントの中に、重い子を置きます。 いちばん自然な書き方です。

子を中に直接書く

count を押すと、子も一緒に光る

重い子
親の中で作られている

count しか変えていないのに、子も毎回描き直されています。 Part 7 でやったとおり、親が描き直されれば子も描き直されるからです。

flat.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { useState } from "react";

// 重い処理をしている想定の子
function Heavy() {
  return <RenderBox title="重い子">親の中で作られている</RenderBox>;
}

export function Flat() {
  useTrackDemoRender();

  const [count, setCount] = useState(0);

  return (
    <div className="flex flex-col gap-4">
      <Button size="sm" onClick={() => setCount((c) => c + 1)}>
        count: {count}
      </Button>

      {/* この行は Flat の中にある。Flat が描き直されるたびに作り直される */}
      <Heavy />
    </div>
  );
}

子を外で作って、渡す

同じものを、子を外側で作ってから渡す形に書き換えます。 子の中身には手を触れません(表示する文言だけ、どちらの形か分かるように変えてあります)。

// 変更前:子を中で作る
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <Button onClick={...}>{count}</Button>
      <Heavy />
    </div>
  );
}

// 変更後:子を受け取って置くだけ
function Counter({ children }) {
  const [count, setCount] = useState(0);
  return (
    <div>
      <Button onClick={...}>{count}</Button>
      {children}
    </div>
  );
}
children として受け取る直した例

count を押しても、子は光らない

重い子
外で作られて、children として渡されている

子が光らなくなりました。memo は使っていません。 子の中身も変えていません。

split.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";

// さっきと同じ構造の子(文言だけ、見分けがつくように変えてある)
function Heavy() {
  return (
    <RenderBox title="重い子" tone="highlight">
      外で作られて、children として渡されている
    </RenderBox>
  );
}

// state を持つ側。子が何かは知らず、受け取って置くだけ
function Counter({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);

  return (
    <div className="flex flex-col gap-4">
      <Button size="sm" onClick={() => setCount((c) => c + 1)}>
        count: {count}
      </Button>

      {/* Counter が描き直されても、children はすでに作られたものが渡ってくる */}
      {children}
    </div>
  );
}

export function Split() {
  useTrackDemoRender();

  return (
    <Counter>
      <Heavy />
    </Counter>
  );
}

なぜ止まるのか

鍵は「その <Heavy /> が、どの関数の中に書いてあるか」です。

親の中で作る描き直される範囲重い子一緒に作り直される外で作って渡す描き直される範囲重い子作り直されない
点線が描き直される範囲。右は子がその外側で作られているので巻き込まれない

<Heavy /> という JSX は、 Part 1 でやったとおりただの値です。 ここまではいいとして、その値がいつ作られるのかが問題になります。

関数の中に書いた行は、その関数が呼ばれるたびに、上から順に実行されます。 Part 6 で見た const options = { ... } が 毎回新しいオブジェクトになったのと、まったく同じことです。

つまり <Heavy /> も、Counter の中に書いてあれば、Counter が動くたびに作り直されます。外に書いてあれば、作り直されません。

  • 中に直接書いた場合、<Heavy />Counter の中にある。 Counter が実行されるたびに、新しく作り直される
  • children として渡した場合、<Heavy />Counter の外にある。 Counter がいくら実行されても、そこは実行されない

Counter が描き直されるとき、React が受け取る children前回とまったく同じものです。

split.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";

// さっきと同じ構造の子(文言だけ、見分けがつくように変えてある)
function Heavy() {
  return (
    <RenderBox title="重い子" tone="highlight">
      外で作られて、children として渡されている
    </RenderBox>
  );
}

// state を持つ側。子が何かは知らず、受け取って置くだけ
function Counter({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);

  return (
    <div className="flex flex-col gap-4">
      <Button size="sm" onClick={() => setCount((c) => c + 1)}>
        count: {count}
      </Button>

      {/* Counter が描き直されても、children はすでに作られたものが渡ってくる */}
      {children}
    </div>
  );
}

export function Split() {
  useTrackDemoRender();

  return (
    <Counter>
      <Heavy />
    </Counter>
  );
}

どういうときに効くか

Part 7 で state を使う場所まで下ろしたのと同じ発想です。 違うのは、重い表示を入力と同じ階層から動かせないときにも 使えることです。

この形が効くのは、状態が頻繁に変わる場所と、重い表示が同じ階層にいるときです。

// 入力のたびに全部が描き直される
function Page() {
  const [keyword, setKeyword] = useState("");
  return (
    <div>
      <input value={keyword} onChange={...} />
      <HeavyChart />   {/* 打つたびに描き直される */}
    </div>
  );
}

検索欄に文字を打つたび、関係のないグラフまで描き直されます。 ここで「入力を持つ部分」を切り出し、 グラフを children として渡せば、巻き込まれなくなります。

function Page() {
  return (
    <SearchBox>
      <HeavyChart />   {/* SearchBox の外で作られる */}
    </SearchBox>
  );
}

split.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";

// さっきと同じ構造の子(文言だけ、見分けがつくように変えてある)
function Heavy() {
  return (
    <RenderBox title="重い子" tone="highlight">
      外で作られて、children として渡されている
    </RenderBox>
  );
}

// state を持つ側。子が何かは知らず、受け取って置くだけ
function Counter({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);

  return (
    <div className="flex flex-col gap-4">
      <Button size="sm" onClick={() => setCount((c) => c + 1)}>
        count: {count}
      </Button>

      {/* Counter が描き直されても、children はすでに作られたものが渡ってくる */}
      {children}
    </div>
  );
}

export function Split() {
  useTrackDemoRender();

  return (
    <Counter>
      <Heavy />
    </Counter>
  );
}

理解できたか確かめる

確認クイズ

children として渡した子が描き直されないのはなぜ?

確認クイズ

この方法と memo、どちらを先に検討する?

split.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";

// さっきと同じ構造の子(文言だけ、見分けがつくように変えてある)
function Heavy() {
  return (
    <RenderBox title="重い子" tone="highlight">
      外で作られて、children として渡されている
    </RenderBox>
  );
}

// state を持つ側。子が何かは知らず、受け取って置くだけ
function Counter({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);

  return (
    <div className="flex flex-col gap-4">
      <Button size="sm" onClick={() => setCount((c) => c + 1)}>
        count: {count}
      </Button>

      {/* Counter が描き直されても、children はすでに作られたものが渡ってくる */}
      {children}
    </div>
  );
}

export function Split() {
  useTrackDemoRender();

  return (
    <Counter>
      <Heavy />
    </Counter>
  );
}

この章のまとめ

  • JSX は書かれた場所で作られる値
  • children として渡した子は親の外で作られるので、 親が描き直されても巻き込まれない
  • memo を使わず、state を持つ範囲を狭くするだけで描き直しが減る
  • 判定はいつも「同じものかどうか」。 state 更新も memo もこの章も、すべて同じ仕組み

split.tsx

"use client";

import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { RenderBox } from "@/components/lesson/render-box";
import { Button } from "@/components/ui/button";
import { type ReactNode, useState } from "react";

// さっきと同じ構造の子(文言だけ、見分けがつくように変えてある)
function Heavy() {
  return (
    <RenderBox title="重い子" tone="highlight">
      外で作られて、children として渡されている
    </RenderBox>
  );
}

// state を持つ側。子が何かは知らず、受け取って置くだけ
function Counter({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);

  return (
    <div className="flex flex-col gap-4">
      <Button size="sm" onClick={() => setCount((c) => c + 1)}>
        count: {count}
      </Button>

      {/* Counter が描き直されても、children はすでに作られたものが渡ってくる */}
      {children}
    </div>
  );
}

export function Split() {
  useTrackDemoRender();

  return (
    <Counter>
      <Heavy />
    </Counter>
  );
}