はじめに
Reactのドキュメント useState(state:コンポーネントのメモリ)についての備忘録です。
https://react.dev/learn/state-a-components-memory
うまくいかない例
import { sculptureList } from './data.js';
export default function Gallery() {
let index = 0;
function handleClick() {
index = index + 1;
}
let sculpture = sculptureList[index];
return (
<>
<button onClick={handleClick}>
Next
</button>
<h2>
<i>{sculpture.name} </i>
by {sculpture.artist}
</h2>
<h3>
({index + 1} of {sculptureList.length})
</h3>
<img
src={sculpture.url}
alt={sculpture.alt}
/>
<p>
{sculpture.description}
</p>
</>
);
}
なぜ動かないのか
index=0
だと新しいデータで更新されないから。
handleClick イベントハンドラが、ローカル変数 index を更新しても、目に見える変化が起こらない。
- データが保持されない
- 新しいデータで再レンダリングされない
その問題を解決するために、useState フックは2つの機能を提供する。
- レンダー間でデータを保持するstate変数
- 変数を更新し、Reactコンポーネントを再度レンダリングするようにトリガーする
useState やその他の use で始まる関数はフック (Hook) と呼ばれる。
useState を呼び出すということは、このコンポーネントに何かを覚えさせるよう React に指示を出すということ。
const [index, setIndex] = useState(0);
この場合、React には index を覚えてもらう。
0が初期値
コンポーネントがレンダーされるたびに、useState は以下の 2 つの値を含む配列を返す。
- 保存した値を保持している state 変数 (index)。
- state 変数を更新し、React にコンポーネントの再レンダーをトリガする state セッタ関数 (setIndex)。
流れのおさらい
- 最初読み込んだ時にコンポーネントがレンダーされる useStateは初期値の状態でレンダーされる・ 今回の場合だったら0
- 次に、ユーザーがボタンを更新すると、handleClickのメソッドが呼び出され、stateが更新される。setIndex(index + 1) が呼び出される。React は index が 1 になったことを覚え、再レンダーがトリガされる。
- コンポーネントの 2 回目のレンダー。React は再び useState(0) というコードに出会うが、React は index を 1 にセットしたことを覚えているので、代わりに [1, setIndex] を返す。
良くない例
import React, { useState, useEffect } from 'react';
function ExampleComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`Effect: ${count}`);
}, [count]);
if (someCondition) {
// 条件が満たされた場合にcountをインクリメント
setCount(count + 1);
}
return (
<div>
<p>Count: {count}</p>
</div>
);
}
このコードの問題点:
Reactはレンダリング中にuseStateの呼び出しをトラッキングし、その後にuseEffectを呼び出すことが期待されている。しかし、条件分岐内でsetCountが呼ばれる場合、Reactはその呼び出しを確実にトラッキング(追跡)できない。
条件が満たされた瞬間にsetCountが呼ばれ、その後のuseEffectがどのように振る舞うかは不確かになってしまう。
これにより、useEffectが期待通りに動作しない可能性がある。Reactは、同じ順序でフックを呼び出すことで、変更が正確に反映され、バグや未定義の動作を防ぐことができる。
解決策:
条件分岐内で直接setCountを呼ぶのではなく、条件が満たされた場合に新しい値を計算し、その後でsetCountを呼ぶようにする。これにより、Reactが一貫した順序でフックをトラッキングでき、正しい動作が期待される。
useState に「識別子」のようなものを渡さないのに、どの state 変数が返されるべきなのか、どのようにしてわかるのか?
どの state 変数を参照しているかに関する情報が含まれていない。
useState に「識別子」のようなものを渡さないのに、どの state 変数が返されるべきなのか、どのようにしてわかるのか。
答えは、 フックは同一コンポーネントの各レンダー間で同一の順番で呼び出されることに依存しているから。
useState が内部的にどのように機能するのか
let componentHooks = [];
let currentHookIndex = 0;
// How useState works inside React (simplified).
function useState(initialState) {
let pair = componentHooks[currentHookIndex];
if (pair) {
// This is not the first render,
// so the state pair already exists.
// Return it and prepare for next Hook call.
currentHookIndex++;
return pair;
}
// This is the first time we're rendering,
// so create a state pair and store it.
pair = [initialState, setState];
function setState(nextState) {
// When the user requests a state change,
// put the new value into the pair.
pair[0] = nextState;
updateDOM();
}
// Store the pair for future renders
// and prepare for the next Hook call.
componentHooks[currentHookIndex] = pair;
currentHookIndex++;
return pair;
}
useState
フックを単純化して再現している。
componentHooks
配列は、各コンポーネントの状態を格納するために使用され、currentHookIndex
は現在のフックの呼び出し位置を示す。
最初のuseState呼び出し時、componentHooksに格納された状態のペアを順番に取得する。
最初の呼び出し時には、componentHooksにまだ状態のペアが格納されていない場合、新しい状態のペアを生成する。
二回目以降の呼び出し時には、componentHooksにすでに同じインデックスに状態のペアが格納されている場合、それを再利用し、既存のペアを返す。
状態が変更されると、対応する setState
関数を呼び出して updateDOM
をトリガーする。
これにより、同じコンポーネント内で同じ順番で複数回useStateが呼ばれた場合でも、同じ状態を参照できるようになる。
function Gallery() {
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);
function handleNextClick() {
setIndex(index + 1);
}
function handleMoreClick() {
setShowMore(!showMore);
}
let sculpture = sculptureList[index];
return {
onNextClick: handleNextClick,
onMoreClick: handleMoreClick,
header: `${sculpture.name} by ${sculpture.artist}`,
counter: `${index + 1} of ${sculptureList.length}`,
more: `${showMore ? 'Hide' : 'Show'} details`,
description: showMore ? sculpture.description : null,
imageSrc: sculpture.url,
imageAlt: sculpture.alt
};
}
Gallery コンポーネントでは、useState を使用して状態(index と showMore)を管理。これらの状態に基づいて次のクリックや詳細表示の切り替えなどのイベントハンドラを提供し、それらをまとめたオブジェクトを返す。
function updateDOM() {
currentHookIndex = 0;
let output = Gallery();
nextButton.onclick = output.onNextClick;
header.textContent = output.header;
moreButton.onclick = output.onMoreClick;
moreButton.textContent = output.more;
image.src = output.imageSrc;
image.alt = output.imageAlt;
if (output.description !== null) {
description.textContent = output.description;
description.style.display = '';
} else {
description.style.display = 'none';
}
}
updateDOM 関数は、Gallery コンポーネントを呼び出して得られた出力をもとに、DOM(Document Object Model)を更新する。これはReactが行う仕事を手動で行うもので、特定のイベント(ボタンのクリックなど)に基づいてコンポーネントの状態を更新し、それに応じてDOMを更新する。
注意: 条件分岐、ループ、ネストされた関数の中でフックを呼び出すことはできない
理由: 条件分岐、ループ、ネストされた関数の中でフックを呼び出すと、Reactがその順序を正確にトラッキングできなくなり、予測できない動作が発生する可能性があるため。
Reactは、各コンポーネントのレンダリング中に発生するすべてのフックの呼び出しを記録し、その順序を覚える。これにより、Reactはコンポーネントがマウントされたり更新されたりするたびに、同じ順序でフックを呼び出し、それによってコンポーネントの状態や表示が正しく更新されることを確保する。
条件分岐やループ内でフックを呼び出すと、特定の条件が満たされるかどうかによってはじめてフックが呼ばれる可能性が生じ、Reactが予測不可能な動作を引き起こし、バグの原因となる。
そのためReactは条件分岐やループ内でフックを呼び出すことを許可していない。
代わりに、条件分岐やループの中で状態を変更する場合は、その変更を条件分岐やループの外で行い、その後でフックを使用して状態を更新する。これにより、Reactが一貫した順序でフックをトラッキングしやすくなる。
参考