間違い探し
以下のコードにはReactのルールに違反する部分があります。
どの箇所がどのようなルールに違反しているかを考えてみましょう。
-
コンポーネントの定義とその利用(ルール違反: 2箇所)
// 引数で受け取ったitemの内容を返すコンポーネント function Post({ item }) { item.url = new Url(item.url, base); return <Link url={item.url}>{item.title}</Link>; } ... const blogPosts = [ { id: 1, title: "First Post", url: "posts/first" }, { id: 2, title: "Second Post", url: "posts/second" }, { id: 3, title: "Third Post", url: "posts/third" }, ]; return ( <div> <h1>My Blog Posts</h1> // 項目ごとに表示 {blogPosts.map(post => ( {Post(post)} ))} </div> );
-
フックの定義とその利用(ルール違反: 2箇所)
// 引数で受け取ったiconの内容を返すカスタムフック function useIconStyle(icon) { const theme = useContext(ThemeContext); if (icon.enabled) { icon.className = computeStyle(icon, theme); } return icon; } ... style = useIconStyle(icon); // 条件を変えてstyleを書き換え icon.enabled = false; style = useIconStyle(icon);
-
フックを利用するコンポーネントの定義とその利用(ルール違反: まとめて一つ)
// Buttonコンポーネント function Button({ useData }) { // useData フックを props から受け取り、それを呼び出す const data = useData(); return ( <button onClick={() => console.log(data)}> クリック </button> ); } function useDataWithLogging() { // ... データを取得するカスタムフックの処理 return data; } ... <Button useData={useDataWithLogging} />
React初心者脱却したと思ったときに勉強になったこと
上記に挙げたものは一部の例題に過ぎず、
私が初心者の状態からある程度Reactを触れるようになったタイミングで
「Reactについてもっと理解を深めたい」
と思った時に改めて勉強してよかったのは公式ドキュメントの「Reactのルール」です。
これは言葉の通りReactのルールなので、もちろん理想は初心者の段階で理解して開発に取り入れることですが、
アプリを一つ作り終えたとき、またはその過程でこちらの内容を読むとより理解が深まりました。
これらのルールは違反をしていても見た目上の動作には問題がないこともありますので、自分がこれまで違反をしていたことに気がつかなかったルールもありました。
「開発できるようになったし、ある程度React初心者は脱却したかなぁ」と思っていたら全くそうではなかったと思い知らされました。
その一部を紹介します。以下、ドキュメントの内容を私の解釈と言葉でアウトプットしたものです。
propsは書き換え不可能
公式ドキュメント内ではよく「ミュータブル」「イミュータブル」という言葉を見かけますが、ご存知の通りこれは書き換え可能、書き換え不可能と解釈すれば良いでしょう。イミュータブルは書き換え不可能という意味です。
stateがイミュータブルであることは最初の方に勉強して理解していました。
// お馴染みのやつ
const [count, setCount] = useState(0);
count = count + 1; // 🔴 Bad: 直接書き換えることは不可能(意味がない)
setCount(count + 1); // ✅ Good: セッタ関数を使って更新する
それと同じようにコンポーネントのprops(引数)もイミュータブルです。
よって、1. の間違い探しの答え一つ目はこちらです。
// 引数で受け取ったitemの内容を返すコンポーネント
function Post({ item }) {
// item.url = new Url(item.url, base); // 🔴 Bad: propsは書き換え不可能
const url = new Url(item.url, base); // ✅ Good: コピーを作成して再定義
return <Link url={url}>{item.title}</Link>;
}
// ... 省略
propsは親コンポーネントから子コンポーネントに受け渡される引数ですが、子コンポーネント側でその中身を変えてしまうとアプリケーションとして一貫性のない出力を生成して、動作しなくなる可能性があります。
イミュータブルとかクロージャの話の時に個人的に分かりやすくて好きな表現として、「値を閉じ込める」というものがあります。
コンポーネントやフック、その他コールバック関数などを実行した時、引数やstateが閉じ込められるのです。そのため、それらは書き換え不可能(イミュータブル)だという理解です。
フックの引数と返り値は書き換え不可能
コンポーネントだけでなく、フックの引数と返り値もイミュータブルです。
注意したいのは、引数がイミュータブルなのはフックの定義の中だけではなく、呼び出し側でもそうだということです。
フック定義の外側だとしても、フックの引数に使った値は書き換えないようにします。(詳しい理由は後述)
順番が前後しますが、2. の間違い探しの答え(二つ)はこちらです。
// 引数で受け取ったiconの内容を返すカスタムフック
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
const newIcon = { ...icon }; // ✅ Good: コピーを作成して再定義
if (icon.enabled) {
// icon.className = computeStyle(icon, theme); // 🔴 Bad: 引数は書き換え不可能
newIcon.className = computeStyle(icon, theme); // ✅ Good: コピーを書き換え
}
return newIcon;
}
...
style = useIconStyle(icon);
// 条件を変えてstyleを書き換え
// icon.enabled = false; // 🔴 Bad: フックの引数に渡したことのある値は書き換え不可能
icon = { ...icon, enabled: false }; // ✅ Good: コピーを作成して書き換え
style = useIconStyle(icon);
上記でなぜicon.enabled = false;
がダメなのか
もしカスタムフックuseIconStyle
が引数をメモ化の依存値として設定していた場合を考えます。
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
return useMemo(() => { // useMemoでメモ化
const newIcon = { ...icon };
if (icon.enabled) {
newIcon.className = computeStyle(icon, theme);
}
return newIcon;
}, [icon, theme]); // 依存値にiconを指定
}
その上で
style = useIconStyle(icon);
icon.enabled = false; // 直接書き換え
style = useIconStyle(icon);
としても、参照するiconオブジェクトが変わっていないのでReactのメモ化システムに検出されず、古いiconで処理されます。
フックの中身はブラックボックスとして扱うべきであり、そのためフックの呼び出し側でも一度引数に使った値を書き換えるべきではありません。
style
はフックの返り値で、最後の行で書き換えているように見えますがstyle
丸ごと再定義しているため問題ないみたいです。
返り値も引数同様に、
style.someProperty = newValue;
のように直接書き換えるとまずいとうことです。
コンポーネント関数を直接呼び出さない
これは私が実際にやってしまったことがあり、しばらく気づかなかったものですが、コンポーネントは関数として直接呼び出してはいけません。JSX内の一部としてのみ使用すべきです。
// CustomButtonというコンポーネントがあるとき
{CustomButton()} // 🔴 Bad: 関数として実行
<CustomButton /> // ✅ Good: JSXタグで実行
そうとは知らず、「どっちでもいいんだろうけど、他のコード見てると大体JSXタグ< />
で呼び出してるから自分もそうしとくか」みたいなノリでした。
知らなかったのですが、コンポーネントをJSXとして呼び出すことでReact にレンダーの指揮権を与えることができるみたいです。
その利点を公式ドキュメント内で以下のように述べています。
- コンポーネントが単なる関数以上のものになる。
- ツリー内のコンポーネントの同一性に基づいた処理を行うフックを通じて、React はローカル state などの機能を追加できます。
- コンポーネントの型情報を差分検出処理時に利用できる。
- React にコンポーネントの呼び出しを任せることで、ツリーの概念的構造について React はより多くの情報を得られます。たとえば <Feed> から <Profile> ページへとレンダーが移行するとき、React はそれらを再利用しようとせずに済みます。
- React がユーザ体験を向上させられる。
- たとえば、大きなコンポーネントツリーの再レンダーがメインスレッドをブロックしないよう、複数のコンポーネントのレンダーの合間にブラウザに一部の作業を行わせることができます。
- より良いデバッグ体験。
- ライブラリがコンポーネントのことを基本部品として認識していれば、開発中の調査のためのリッチな開発者ツールを構築できます。
- より効率的な差分検出処理。
- React は、ツリー内のどのコンポーネントを再レンダーすべきか正確に把握し、残りをスキップします。これによりアプリの動作は高速でキビキビとしたものになります。
よって間違い探し 1. のもう一つの答えはこちらになります。
// 引数で受け取ったitemの内容を返すコンポーネント
function Post({ item }) {
const url = new Url(item.url, base); // ※修正済み
return <Link url={url}>{item.title}</Link>;
}
...
const blogPosts = [
{ id: 1, title: "First Post", url: "posts/first" },
{ id: 2, title: "Second Post", url: "posts/second" },
{ id: 3, title: "Third Post", url: "posts/third" },
];
return (
<div>
<h1>My Blog Posts</h1>
// 項目ごとに表示
{blogPosts.map(post => (
// {Post(post)} // 🔴 Bad: 関数として直接実行してはいけない
<Post key={post.id} item={post} /> // ✅ Good: JSXの一部として呼び出す
))}
</div>
);
フックを動的に使用しない
「フックを動的に使用する」とは、つまり通常の値のようにpropsとして渡したり、条件付きで呼び出したり、ループ内でフックを呼び出したりなどすることです。
つまり、間違い探しの 3. はコンポーネントのpropsにフックを渡してしまっているのがルール違反です。
// Buttonコンポーネント
// function Button({ useData }) { // 🔴 Bad: フックをpropsとして受け取っている
function Button() { // ✅ Good: propsにフックは受け取らない
// const data = useData();
const data = useDataWithLogging(); // ✅ Good: コンポーネント内で直接使用する
return (
<button onClick={() => console.log(data)}>
クリック
</button>
);
}
function useDataWithLogging() {
// ... データを取得するカスタムフックの処理
return data;
}
...
// <Button useData={useDataWithLogging} /> // 🔴 Bad: フックをpropsとして渡している
<Button /> // ✅ Good: propsにフックを含めない
おわりに
これらのルールを守っていると、React開発でよくある(少なくとも私にはよくある)「なぜかバグが起こる」とか「思ったように動かない」みたいな現象が減ると感じました。