title |
---|
レンダーとコミット |
コンポーネントは画面上に表示される前に、React によってレンダーされる必要があります。このプロセスを段階ごとに理解すると、コードがどのように実行されるのか考える際や、コードの振る舞いを説明する際に役立ちます。
- React での「レンダー」の意味
- いつ、なぜ React はコンポーネントをレンダーするのか
- 画面上にコンポーネントが表示されるステップ
- なぜレンダーしても必ずしも DOM 更新が起きないのか
コンポーネントは厨房で材料から美味しい料理を作る料理人だと想像してください。このシナリオでは React はお客様のリクエストを受け付け、注文された料理を運ぶウェイターです。注文を受けて UI 要素を「配膳」するプロセスには、次の 3 つのステップが存在します:
- レンダーのトリガ(キッチンに注文を送る)
- コンポーネントのレンダー(キッチンで注文を準備する)
- DOM へのコミット(テーブルに注文を置く)
コンポーネントがレンダーされる理由には 2 つあります。
- コンポーネントの初回レンダー。
- コンポーネント(またはその祖先のいずれか)の state の更新。
アプリが開始するときには、初回のレンダーをトリガする必要があります。フレームワークやサンドボックスでは、このコードが隠されたりすることがありますが、これはターゲットとなる DOM ノードで createRoot
を呼び出し、コンポーネントでその render
メソッドを呼び出すことによって行われます。
import Image from './Image.js';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'))
root.render(<Image />);
export default function Image() {
return (
<img
src="https://i.imgur.com/ZF6s192.jpg"
alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
/>
);
}
root.render()
の呼び出しをコメントアウトして、コンポーネントが消えるのを確認してみてください!
コンポーネントが最初にレンダーされた後、set
関数を使って state を更新することで、さらなるレンダーをトリガすることができます。コンポーネントの state を更新すると、自動的にレンダーがキューイングされます。(これは、レストランの客が最初の注文の後に、喉の渇きや空腹の状態に応じてお茶やデザートなどいろいろなものを注文するようなものだと考えることができます。)
あなたがレンダーをトリガした後、React はコンポーネントを呼び出して画面に表示する内容を把握します。「レンダー」とは、React がコンポーネントを呼び出すことです。
- 初回レンダー時、React はルート (root) コンポーネントを呼び出します。
- 次回以降のレンダーでは、state の更新によってレンダーのトリガが起きた関数コンポーネントを、React がコールします。
このプロセスは再帰的に発生します。更新されたコンポーネントが他のコンポーネントを返す場合、次にそのコンポーネントを React がレンダーし、そのコンポーネントも何かコンポーネントを返す場合、そのコンポーネントも次にレンダーし、といった具合に続きます。このプロセスは、ネストされたコンポーネントがなくなり、React が画面に表示されるべき内容を正確に把握するまで続きます。
次の例では、React は Gallery()
を呼び出した後、Image()
を何度も呼び出します。
export default function Gallery() {
return (
<section>
<h1>Inspiring Sculptures</h1>
<Image />
<Image />
<Image />
</section>
);
}
function Image() {
return (
<img
src="https://i.imgur.com/ZF6s192.jpg"
alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
/>
);
}
import Gallery from './Gallery.js';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'))
root.render(<Gallery />);
img { margin: 0 10px 10px 0; }
- 初回レンダー時には、React は
<section>
、<h1>
、および 3 つの<img>
タグの DOM ノードを作成します。 - 再レンダー時には、React は前回のレンダーからどの部分が変わったのか、あるいは変わらなかったのかを計算します。次のステップであるコミットフェーズまでこの情報は使われません。
レンダーは常に純粋な計算であるべきです。
- 同じ入力には同じ出力。同じ入力が与えられた場合、コンポーネントは常に同じ JSX を返す必要がある。(トマトサラダを注文した人がオニオンサラダを受け取ってはいけない!)
- 自分の仕事に専念する。レンダー前に存在したオブジェクトや変数を変更しない。(ある注文が他の誰かの注文を変更してはいけない。)
さもないと、コードベースが複雑になるにつれて、混乱するバグや予測不能な挙動に遭遇することになります。"Strict Mode" で開発している場合、React は各コンポーネントの関数を 2 回呼び出し、純粋でない関数による誤りに気づきやすいようにしてくれます。
更新されたコンポーネントがツリー内で非常に高い位置にある場合、その内部にネストされたすべてのコンポーネントを再レンダーするというデフォルトの挙動は、パフォーマンスにとって理想的ではありません。パフォーマンスの問題に遭遇した場合、パフォーマンスセクションで述べられているいくつかのオプトインによる解決方法があります。最適化は急いでやってはいけません!
あなたのコンポーネントをレンダー(関数として呼び出し)した後、React は DOM を変更します。
- 初回レンダー時には、React は
appendChild()
DOM API を使用して、作成したすべての DOM ノードを画面に表示します。 - 再レンダー時には、React は最新のレンダー出力に合わせて DOM を変更するため、必要な最小限の操作(レンダー中に計算されたもの!)を適用します。
React はレンダー間で違いがあった場合にのみ DOM ノードを変更します。例えば、以下のコンポーネントは親から渡された異なる props で毎秒再レンダーされます。<input>
にテキストを追加して value
に変化があった場合でも、コンポーネントが再レンダーされたときにテキストが消えないことに注意してください:
export default function Clock({ time }) {
return (
<>
<h1>{time}</h1>
<input />
</>
);
}
import { useState, useEffect } from 'react';
import Clock from './Clock.js';
function useTime() {
const [time, setTime] = useState(() => new Date());
useEffect(() => {
const id = setInterval(() => {
setTime(new Date());
}, 1000);
return () => clearInterval(id);
}, []);
return time;
}
export default function App() {
const time = useTime();
return (
<Clock time={time.toLocaleTimeString()} />
);
}
これが動作するのは、最終ステップの部分で React は、新しい time
を使って <h1>
の中身だけを更新するからです。<input>
は JSX 内で前回と同じ場所にあるので、React は <input>
やその value
に触れません!
レンダーが完了し、React が DOM を更新した後、ブラウザは画面を再描画します。このプロセスは「ブラウザレンダリング」として知られていますが、ドキュメント全体での混乱を避けるために、我々は「ペイント」と呼ぶことにします。
- React アプリでの画面更新は、以下の 3 つのステップで行われる:
- トリガ
- レンダー
- コミット
- Strict Mode を使って、コンポーネントの間違いを発見できる。
- レンダー結果が前回と同一である場合、React は DOM を触らない。