part-1はこちら
part2はこちら
レンダリング
Reactのレンダリングは、画面描画のことではなくReactがコンポーネントを呼び出すこと(UIが表示される前の段階の処理)。
コンポーネントは画面上に表示される前にReactによってレンダリングされる必要がある。
レンダリングのプロセスをわかりやすくレストランで例えると、以下のようなイメージ。
- トリガー(お客様の注文を厨房に伝える)
- レンダリング(厨房で注文の品を料理する)
- コミット(テーブルに注文の品を提供する)
■ 1.トリガー
アプリの起動、または useState の変化によって「画面更新の注文」が入る。
・ユーザーが画面上のボタンをクリックする。
・イベントハンドラが実行され、ステート関数が呼び出される。
・Appコンポーネントの再レンダリングを予約する。
■ 2.レンダリング
Reactがコンポーネントを実行し、どんなHTMLJSXにするかの「設計図」を作成する段階。
・React内部でstate変数が変更されたコンポーネント(
App())を再度上から下まで実行する。
state変数が変更されたコンポーネントが別のコンポーネント(子要素)を返している場合、Reactは次にその子コンポーネントも確認し、さらにその子が別の子を返していればそれも確認する。
このプロセスは、ネストされたコンポーネントがなくなり、Reactが「画面に何を表示すべきか」を完全に把握するまで繰り返される。
・useStateは新しく計算されたデータを返す。
・AppはuseStateを元に新しいJSXを返す。
・Reactは前回のJSXと今回のJSXの中身を比較して、差分を確認する。
初回レンダリング時は、rootコンポーネントを呼び出し、
次回以降のレンダリングではstate変数が更新されたコンポーネントの差分を計算しにいく。
あくまでReactの頭の中(メモリ上)で、新旧のJSXの差分を計算している段階なので実際のブラウザの画面には反映されない。
また、コンポーネントは純関数でなければならずレンダリング中にState変数を書き換えるなどの処理を行ってはいけない。
■ 3.コミット
Reactが計算した設計図をもとに、ブラウザに表示するDOMの追加・更新・削除を行い、実DOMに反映する。(画面の更新)
・Reactは差分が発生しているDOMの変更部分だけを更新する。
・画面が更新される。
初回レンダリング時には、ReactはappendChild() DOM APIを使用して、作成したすべてのDOMノードを画面に表示する。
再レンダリング時には、Reactは最新のレンダリング出力に合わせてDOMを変更するため、変更があればその変更部分のみを適用、変更がない場合はスキップする。
state更新のバッチ処理とアップデーター関数
バッチ処理を行うことによってイベントハンドラ内の全てのコードが実行されるまでstateの更新を待機することができる。
state更新関数に値をそのまま渡すと意図した挙動にならない。
import { useState } from 'react';
export default function App() {
const [number, setNumber] = useState(0);
function handleCount() {
/*
レンダー時にnumberは書き換わらないので各setNumberで呼び出されるnumberは0のまま、
同じ処理(0+1)を3回繰り返してるだけになる。
*/
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}
return (
<>
<h1>{number}</h1>
<button onClick={() => handleCount()}>+3</button>
</>
)
}
state更新関数にアップデーター関数(n => n + 1)を渡すと前の関数の処理を引き継ぐことができる。
アップデーター関数
setNumber(n => n + 1)のn => n + 1のように、変数名 => 処理のような書き方をする関数。
最初の変数の値はstate変数の値、次に出てくる変数の値は直前の処理の変数の値が入る。
基本的にアップデーター関数はuseStateでしか使用しない。
import { useState } from 'react';
export default function App() {
const [number, setNumber] = useState(0);
function handleCount() {
/*
レンダー時に変数nを準備して、関数呼び出し回数分nに処理が積まれていく。
最終的にsetNumber()にnが渡される。
*/
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
}
return (
<>
<h1>{number}</h1>
<button onClick={() => handleCount()}>+3</button>
</>
)
}
stateを書き換えた後にアップデーター関数を呼び出している場合。
import { useState } from 'react';
export default function App() {
...
function handleCount() {
/*
numberとnは別物。
最初のsetNumber()でnumberの値が決まってnにはnumberの値が入る
nはレンダー中でも書き換えることができるので、+1してnumberの値として最終的にsetする。
*/
setNumber(number + 5);
setNumber(n => n + 1);
}
...
}
state変数とミューテーション(mutation)
ミューテーション(mutation)
オブジェクトや配列の中身などを直接書き換えること。
書き換えが可能なものをミューダブルという。
state変数の値がオブジェクトの場合、以下のような書き換えは技術的には可能。
const [position, setPosition] = useState({ x: 0, y: 0 });
position.x = 5;
しかし、Reactはオブジェクトの中身の変化を比較しないため、オブジェクトが更新されたことを検知できない。そのため、再レンダリングが走らず意図した挙動にならない。
正しく再レンダリングを実行する場合は、新しくオブジェクトを作成し、それをstate更新関数に渡す必要がある。
const [position, setPosition] = useState({ x: 0, y: 0 });
function handleChangeObj() {
setPosition({x: 100, y: 100})
}
スプレッド構文でオブジェクトをコピーする
実際には「オブジェクトの一部だけ更新したい」というケースがよくあるため、その場合はスプレッド構文を使用して対応が可能。
スプレッド構文は、オブジェクトが入れ子になっている場合は1階層目しかコピーできないことに注意。
const [position, setPosition] = useState({ x: 0, y: 0 });
function handleChangeObj() {
setPosition({...position, y: 100})
}
スプレッド構文は、1階層目しかコピーできないので、オブジェクトが入れ子になっている場合は、複数回スプレッド構文を使う必要がある。
const [recipe, setRecipe] = useState({
name: 'ビザ',
ingredients: {
title: 'チーズ',
city: 'トマト',
}
});
function handleChangeObj() {
setRecipe({
...recipe,
ingredients: {
...recipe.ingredients,
city: 'ソーセージ'
}
});
}
配列の要素を変更・追加・削除する
オブジェクト同様に、配列もarray[0] = 'hoge'のようにインデックスを指定して直接要素を書き換えるのはNG。
そのため、中身を更新する場合は新しい配列を作成してstate更新関数に渡す必要がある。
直接オブジェクトを破壊しないメソッドは使用可能、それ以外は使用できない。
const [array, setArray] = useState([{id: 1234, name: '田中'}])
function handleAddArray() {
array[0].name = '吉田' // NG
array.push({ id: nextId++, name: name }); // NG
}
■ 配列の要素を変更する
既存の配列の要素を変更、置換したい場合はmap()メソッドを使用する。
function handleArrayChange() {
const newArray = array.map((a) => {
if (a.name === '田中') {
return { ...a, name: '吉田' }; // 新しいオブジェクトを作って返す
}
return a;
});
setArray(newArray);
}
■ 配列に要素を追加する
配列に新しい要素を追加する場合は、既存の要素をコピーしたもの+追加したい要素を持つ新しい配列を中身をstate更新関数に渡す必要がある。
const [array, setArray] = [{id: 1234, name: '田中'}]
function handleAddArray() {
setArray([
...array,
{ id: nextId++, name: '高橋' }
]);
}
先頭や末尾ではなく、配列の「特定のインデックス」に要素を挿入したい場合は、スプレッド構文とslice()メソッドを組み合わせて、挿入位置の前後の「スライス(破片)」を繋ぎ合わせる。
const insertAt = 1;
function handleAddArray() {
const newArray = [
...array.slice(0, insertAt), // 挿入位置より前の要素をコピー
{ id: nextId++, name: name },
...array.slice(insertAt) // 挿入位置より後ろの要素をコピー
];
setArray(newArray);
}
■ 配列から要素を削除する
配列から要素を削除したい場合は、filter()メソッドを使って指定した要素を除外した新しい配列を作る。
...
function handleDeleteArray() {
// 指定したID以外の要素だけで構成される新しい配列を作成
setArray(
array.filter(a => a.id !== 1234)
);
}