コンポーネントとフックを純粋に保つ
純関数 (pure function) とは計算を行うだけで、それ以上のことはしない関数です。これによりコードの理解やデバッグが容易になり、React が自動的にコンポーネントとフックを最適化できるようになります。
- 純粋性が重要である理由
- コンポーネントとフックを冪等にする
- 副作用はレンダーの外で実行する
- props と state はイミュータブル
- フックの引数と返り値はイミュータブル
- JSX に渡された値はイミュータブル
純粋性が重要である理由
React を React たらしめる重要な概念のひとつが純粋性 (purity) です。純粋なコンポーネントやフックとは、以下のような特徴を持つものです。
- 冪等 (idempotent) であること – 同じ入力で実行するたびに常に同じ結果が得られること。コンポーネントの入力とは props と state とコンテクスト。フックの入力とはその引数。
- レンダー時に副作用がない – 副作用 (side effect) を伴うコードはレンダーとは別に実行する必要がある。例えばユーザが UI を操作しそれによって UI が更新される場合はイベントハンドラとして、またはレンダー直後に動作させる場合はエフェクトとして実行する。
- ローカルな値以外を変更しない:コンポーネントとフックは、レンダー中にローカルに作成されたものではない値を決して変更してはならない。
レンダーが純粋に保たれていれば、React はどの更新を優先してユーザに最初に提示すべきか理解することができます。これができるのはレンダーの純粋性のお陰です。コンポーネントがレンダー時に副作用を持たないなら、React は更新がそれほど重要でないコンポーネントのレンダー処理を一時停止し、後で必要になったときに再開できます。
具体的にはこれは、React がユーザに快適な体験を提供できるよう、レンダーのロジックが複数回実行されることがあるという意味です。しかしコンポーネントがレンダー時に React が把握できない副作用、例えばグローバル変数の書き換えのようなことを行っている場合、React がレンダーコードを再実行した際にその副作用が望ましくない形でトリガされることになります。これはしばしば予期せぬバグを引き起こし、ユーザ体験を悪化させます。「コンポーネントを純粋に保つ」のこちらの例を参照してください。
React はどのようにコードを実行するのか
React は宣言型 (declarative) です。あなたは何 (what) をレンダーしたいのかだけを React に伝え、それをどうやって (how) ユーザにうまく表示するのかについては React が考えます。これを実現するため、React は複数のフェーズに分けてコードを実行します。React を使いこなすためにこれらのフェーズすべてを知っておく必要はありません。しかしどのコードがレンダー中に実行され、どのコードがそれ以外のタイミングで実行されるのかについては、概要を知っておくべきです。
レンダーとは、UI の次のバージョンとして何が見えるべきかを計算する作業を指します。レンダーの後、エフェクトがフラッシュ (flush)(つまり未処理分がなくなるまで実行)され、それらがレイアウトに影響を与える場合は計算の更新を行います。React はこの新しい計算結果を受け取り、UI の以前のバージョンを作成する際に使われた計算結果と比較し、最新バージョンに追いつくために必要な最小限の変更を DOM(ユーザが目にするもの)にコミット (commit) します。
さらに深く知る
コードがレンダー中に走るかどうかを判断する簡単な方法は、そのコードがどこに書かれているかを見ることです。以下の例のようにトップレベルに書かれている場合、レンダー中に実行される可能性が高いでしょう。
function Dropdown() {
const selectedItems = new Set(); // created during render
// ...
}
イベントハンドラやエフェクトはレンダー中には実行されません。
function Dropdown() {
const selectedItems = new Set();
const onSelect = (item) => {
// this code is in an event handler, so it's only run when the user triggers this
selectedItems.add(item);
}
}
function Dropdown() {
const selectedItems = new Set();
useEffect(() => {
// this code is inside of an Effect, so it only runs after rendering
logForAnalytics(selectedItems);
}, [selectedItems]);
}
コンポーネントとフックを冪等にする
コンポーネントは、その入力である props、state、およびコンテクストに対して常に同じ出力を返さなければなりません。これを冪等性 (idempotency) と呼びます。冪等性は関数型プログラミングで広まった用語であり、同じ入力でそのコードを実行するたびに常に同じ結果が得られるという考え方を指します。
これは、レンダー中に実行されるあらゆるコードは冪等でなければならないという意味です。例えば以下のコードは冪等ではありません(したがって、コンポーネントも冪等ではありません)。
function Clock() {
const time = new Date(); // 🔴 Bad: always returns a different result!
return <span>{time.toLocaleString()}</span>
}
new Date()
は常に現在の日時を返し、呼び出すたびに結果が変わるため、冪等ではありません。上記のコンポーネントをレンダーすると、画面に表示される時間はコンポーネントがレンダーされた瞬間の時間に固定されます。同様に、Math.random()
のような関数も冪等ではありません。なぜなら、入力が同じでも呼び出すたびに異なる結果を返すからです。
これは、new Date()
のような冪等ではない関数を絶対に使用してはならないという意味ではありません。レンダー中にだけは使用できないということです。上記の場合、最新の日付をこのコンポーネントと同期するために、エフェクトが使用できます。
import { useState, useEffect } from 'react'; function useTime() { // 1. Keep track of the current date's state. `useState` receives an initializer function as its // initial state. It only runs once when the hook is called, so only the current date at the // time the hook is called is set first. const [time, setTime] = useState(() => new Date()); useEffect(() => { // 2. Update the current date every second using `setInterval`. const id = setInterval(() => { setTime(new Date()); // ✅ Good: non-idempotent code no longer runs in render }, 1000); // 3. Return a cleanup function so we don't leak the `setInterval` timer. return () => clearInterval(id); }, []); return time; } export default function Clock() { const time = useTime(); return <span>{time.toLocaleString()}</span>; }
非冪等な new Date()
の呼び出しをエフェクトでラップすることで、その計算をレンダーの外側に移動させているのです。
React と外部状態を同期する必要がなく、ユーザの操作に応じて更新を行うだけの場合は、イベントハンドラの使用を考慮してください。
副作用はレンダーの外で実行する
副作用はレンダー中に実行してはいけません。React が最適なユーザ体験のためにコンポーネントを複数回レンダーする可能性があるためです。
レンダーは純粋に保つ必要がある一方で、アプリが何か面白いことをするためには、いずれかの時点で副作用が必要です。これには画面に何かを表示することも含まれます! このルールの大事なところは、副作用はレンダー時に起きてはならない、ということです。React がコンポーネントを複数回レンダーすることがあるからです。大抵の場合、副作用はイベントハンドラを使用して処理します。イベントハンドラを使用することで、そのコードはレンダー中に実行しなくてよいと React に明示的に伝えていることになり、レンダーが純粋に保たれます。他の選択肢がない場合に最後の手段としてのみ、useEffect
を使用して副作用を処理することもできます。
書き換えを行ってもよいタイミング
ローカルミューテーション
副作用の一般的な例はミューテーション (mutation) です。JavaScript では、これは非プリミティブ値の内容を書き換えることを指します。一般的に React では変数の書き換えを避けるべきですが、ローカル変数のミューテーション (local mutation) はまったく問題ありません。
function FriendList({ friends }) {
const items = []; // ✅ Good: locally created
for (let i = 0; i < friends.length; i++) {
const friend = friends[i];
items.push(
<Friend key={friend.id} friend={friend} />
); // ✅ Good: local mutation is okay
}
return <section>{items}</section>;
}
ローカルミューテーションを避けるために無理にコードを変える必要はありません。Array.map
を使用して短く書くこともできますが、レンダー時にローカル配列を作成してアイテムを push していくのでも何ら問題ありません。
items
を書き換えているように見えますが、このコードはローカルでのみそうしているという点が重要です。コンポーネントの再レンダー時にこの書き換えは「記憶」されていません。言い換えると、items
はコンポーネントの実行の最中にのみ存在します。<FriendList />
がレンダーされるたびに items
は再作成されるので、コンポーネントは常に同じ結果を返します。
一方で、items
がコンポーネントの外部で作成されている場合、以前の値を保持しつづけることで、変更の記憶が起きてしまいます。
const items = []; // 🔴 Bad: created outside of the component
function FriendList({ friends }) {
for (let i = 0; i < friends.length; i++) {
const friend = friends[i];
items.push(
<Friend key={friend.id} friend={friend} />
); // 🔴 Bad: mutates a value created outside of render
}
return <section>{items}</section>;
}
<FriendList />
が再実行されると、このコンポーネントが実行されるたびに friends
を items
に追加し続け、結果の重複が生じます。この <FriendList />
にはレンダー時に外部から観測可能な副作用があり、そのためルールに違反しているというわけです。
遅延初期化
厳密には「純粋」ではありませんが、遅延初期化 (lazy initialization) は問題ありません。
function ExpenseForm() {
SuperCalculator.initializeIfNotReady(); // ✅ Good: if it doesn't affect other components
// Continue rendering...
}
DOM の書き換え
ユーザに直接見えるような副作用は、React コンポーネントのレンダーロジックでは許可されていません。言い換えると、単にコンポーネント関数を呼び出すこと自体が、画面上の変化を生じさせてはいけません。
function ProductDetailPage({ product }) {
document.title = product.title; // 🔴 Bad: Changes the DOM
}
document.title
を更新するという望ましい結果をレンダーの外で達成する方法のひとつは、document
とコンポーネントを同期させることです。
コンポーネントを複数回呼び出しても安全であり、他のコンポーネントのレンダーに影響を与えないのであれば、React はそれが厳密な関数型プログラミングの意味で 100% 純粋であるかどうかを気にしません。より重要なのは、コンポーネントは冪等でなければならないということです。
props と state はイミュータブル
コンポーネントの props と state はイミュータブルなスナップショットです。これらは決して直接書き換えてはいけません。代わりに新しい props を渡すか、useState
のセッタ関数を使用してください。
props と state の値は、レンダーが終わってから更新されるスナップショットと考えることができます。したがって props や state 変数を直接書き換えることはありません。代わりに新しい props を渡すか、あるいはセッタ関数を使用して React に state をコンポーネントの次回レンダー時に更新する必要があると伝えます。
props を書き換えない
props はイミュータブルです。props を変更すると、アプリケーションが一貫性のない出力を生成し、状況によって動作したりしなかったりするためデバッグが困難になるからです。
function Post({ item }) {
item.url = new Url(item.url, base); // 🔴 Bad: never mutate props directly
return <Link url={item.url}>{item.title}</Link>;
}
function Post({ item }) {
const url = new Url(item.url, base); // ✅ Good: make a copy instead
return <Link url={url}>{item.title}</Link>;
}
state を書き換えない
useState
は state 変数とその state を更新するためのセッタ関数を返します。
const [stateVariable, setter] = useState(0);
state 変数はその場で書き換えるのではなく、useState
によって返されるセッタ関数を使用して更新する必要があります。state 変数の中身を書き換えてもコンポーネントが更新されるわけではないため、ユーザに古くなった UI が表示されたままになります。セッタ関数を使用することで、state が変更され、UI を更新するため再レンダーをキューに入れる必要があるということを React に伝えます。
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
count = count + 1; // 🔴 Bad: never mutate state directly
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1); // ✅ Good: use the setter function returned by useState
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}
フックの引数と返り値はイミュータブル
一度値がフックに渡されたならそれを書き換えてはいけません。JSX の props と同様、フックに渡された時点でその値はイミュータブルです。
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
if (icon.enabled) {
icon.className = computeStyle(icon, theme); // 🔴 Bad: never mutate hook arguments directly
}
return icon;
}
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
const newIcon = { ...icon }; // ✅ Good: make a copy instead
if (icon.enabled) {
newIcon.className = computeStyle(icon, theme);
}
return newIcon;
}
React における重要な原則のひとつは、ローカル・リーズニング、つまりコンポーネントやフックが何をしているのかそのコードだけを見て理解できることです。フックを呼び出す際には中身を「ブラックボックス」として扱うべきです。例えば、カスタムフックが引数を内部で値をメモ化するための依存値として使用していたらどうでしょう。
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
return useMemo(() => {
const newIcon = { ...icon };
if (icon.enabled) {
newIcon.className = computeStyle(icon, theme);
}
return newIcon;
}, [icon, theme]);
}
フックの引数を書き換えた場合、カスタムフック内のメモ化が正しく動作しなくなります。これを避けることが重要です。
style = useIconStyle(icon); // `style` is memoized based on `icon`
icon.enabled = false; // Bad: 🔴 never mutate hook arguments directly
style = useIconStyle(icon); // previously memoized result is returned
style = useIconStyle(icon); // `style` is memoized based on `icon`
icon = { ...icon, enabled: false }; // Good: ✅ make a copy instead
style = useIconStyle(icon); // new value of `style` is calculated
同様に、フックからの返り値はメモ化されている可能性があるため、それらを書き換えないことも重要です。
JSX に渡された値はイミュータブル
JSX で使用された後に値を書き換えてはいけません。ミューテーションは JSX が作成される前に行ってください。
式として JSX を使用する際、React はコンポーネントのレンダーが完了する前に JSX を先行して評価してしまうかもしれません。つまり JSX に渡された後で値を変更した場合、React がコンポーネントの出力を更新する必要があることを認識しないため、古い UI が表示され続ける可能性があるということです。
function Page({ colour }) {
const styles = { colour, size: "large" };
const header = <Header styles={styles} />;
styles.size = "small"; // 🔴 Bad: styles was already used in the JSX above
const footer = <Footer styles={styles} />;
return (
<>
{header}
<Content />
{footer}
</>
);
}
function Page({ colour }) {
const headerStyles = { colour, size: "large" };
const header = <Header styles={headerStyles} />;
const footerStyles = { colour, size: "small" }; // ✅ Good: we created a new value
const footer = <Footer styles={footerStyles} />;
return (
<>
{header}
<Content />
{footer}
</>
);
}