こんにちは、とまだです。
React は Hook の効果もあり、書けばとりあえず動いてくれるので楽しいですよね!!!!!!!
ただ、本当に正しいコードを書いているか不安になったことはありませんか?
React は自由度が高い分、書き方によってはバグが発生しやすかったり、パフォーマンスが低下することもあります。
(その結果としてバグが発生し、しかも全く原因がわからない事態に何度も出会いました...)
そんなときに役に立つのが「Rules of React」です。
これは、React 公式が提言している、React を使う時の「お約束」です。
私は今まで断片的にしか知らなかったので、特に重要そうなものを抜粋してまとめてみました。
直感的に理解してもらうことを大事にしているため、ニュアンスが正確ではない箇所もあるかもしれません。
あらかじめご了承ください。
1. コンポーネントとHookは「ピュア」であるべし!
「ピュア」とは?
Props と State は、React でデータを扱うための重要な概念です。
「ピュア」と言うと、純粋で清潔なイメージがありますよね。
一方 Reactで言う「ピュア」というのは、「きれいで、予測しやすい」という意味です。
例えば、自動販売機を想像してみてください。100円入れたらいつも同じジュースが出てくるのが「ピュア」な状態です。
良い例と悪い例
では実際にコードを見てみましょう。
悪い例:
こちらのコンポーネントは、毎回違うジュースが出てくるので「ピュア」ではありません。
function RandomJuice() {
// 🙅♂️ ダメな例:毎回違うジュースが出てくる
const juices = ['オレンジ', 'アップル', 'グレープ'];
const random = Math.floor(Math.random() * juices.length);
return <div>今日のジュース: {juices[random]}</div>;
}
また、時間によって結果が変わるのも「ピュア」ではありません。
function TimeJuice() {
// 🙅♂️ ダメな例:時間によって結果が変わる
const now = new Date();
return <div>今日のジュース: {now.getSeconds() % 3}</div>;
}
一方、以下のコンポーネントは、同じ味を指定したら、いつも同じ結果が返ってくるので「ピュア」です。
良い例:
function Juice({ flavor }) {
// 🙆♂️ いい例:同じ味を指定したら、いつも同じ結果
return <div>今日のジュース: {flavor}</div>;
}
また、仮にランダムな値を使いたい場合は、コンポーネントの外で作るようにしましょう。
// 先にランダムな値を作る
const juices = ['オレンジ', 'アップル', 'グレープ'];
const random = Math.floor(Math.random() * juices.length);
function RandomJuice({flavor}) {
// 作った値をコンポーネントに渡す
return <Juice flavor={juices[random]} />;
}
// 引数として渡す
<Juice flavor={juices[random]} />
なぜピュアじゃないとダメ?
ピュアなコンポーネントを作ると、アプリの動きが予測しやすくなって、バグを見つけやすくなります。
逆にピュアじゃ無いコンポーネントの場合、予期せぬ動きをすることがあるので、バグを見つけるのが難しくなります。
その他にも、ピュアなコンポーネントは、再利用しやすく、テストしやすいというメリットがあります。
2. 副作用はレンダリング外で実行し、必要に応じてクリーンアップする
副作用とは?
副作用は、コンポーネントの主な仕事(レンダリング)以外のことです。
日常生活で例えると、料理をする時に、食材を買いに行くことが副作用です。料理をすることが主な仕事で、買い物はそのための準備ですよね。
React での副作用は、データの取得や更新、タイマーのセットなどがあります。
良い例と悪い例
以下の例は、コンポーネントの中に副作用がある悪い例です。
悪い例:
function BirthdayCake() {
// 🙅♂️ ダメな例:ケーキを焼きながらタイマーをセット
setTimer(30);
return <div>ケーキを焼いています...</div>;
}
このような場合、コンポーネントの中で副作用を行っているため、コンポーネントの動作が予測しにくくなります。
React の場合、副作用は useEffect
フックを使って行います。useEffect
フックは、レンダリング後に副作用を実行し、必要に応じてクリーンアップ関数を提供します。
良い例:
function BirthdayCake() {
useEffect(() => {
// 🙆♂️ いい例:ケーキを焼き始めた後にタイマーをセット
const timerId = setTimer(30);
// クリーンアップ関数:コンポーネントがアンマウントされる時にタイマーをクリア
return () => clearTimer(timerId);
}, []);
return <div>ケーキを焼いています...</div>;
}
なぜレンダリング外で実行するの?
副作用をレンダリング外で実行する理由は、レンダリングをクリーンに保ち、予測可能にするためです。
料理をするときを想像してください。
まず料理を作って、お皿に盛り付けます(これがレンダリングです)。
その後で、使った道具を洗ったり、残り物を冷蔵庫に入れたりします(これが副作用です)。
このように、副作用はコンポーネントの主な仕事が終わった後に行うのが自然で、アプリケーションの動作を予測しやすくします。
3. PropsとStateは「変えちゃダメ」
PropsとStateとは?
Props と State は、React でデータを扱うための重要な概念です。
Props は、コンポーネントに渡されるデータのことです。
一方、State は、コンポーネント内で管理されるデータのことです。
良い例と悪い例
では、Props と State を変えてしまう悪い例を見てみましょう。
悪い例:
function Counter({ count }) {
// 🙅♂️ ダメな例:友達からもらったcount(プロップス)を勝手に変更
count = count + 1;
return <div>{count}</div>;
}
こちらは、Props で受け取った count を勝手に変更してしまっています。
では、Props と State を変えない良い例を見てみましょう。
良い例:
function Counter({ count }) {
// 🙆♂️ いい例:もらったcountはそのまま。新しい値を作る
const newCount = count + 1;
return <div>{newCount}</div>;
}
なぜPropsとStateを変えちゃダメ?
友達から借りた本に、勝手に書き込みをしたらダメですよね?
同じように、PropsやStateも「借りもの」だと思ってください。変更したいなら、新しいコピーを作るんです。
実際、たとえば、Props を変更してしまうと、親コンポーネントのデータも変わってしまうため、予期せぬバグが発生する可能性があります。
例:
function Parent() {
const [count, setCount] = useState(0);
return <Child count={count} />;
}
function Child({ count }) {
// 🙅♂️ ダメな例:親のcountを勝手に変更
count = count + 1;
return <div>{count}</div>;
}
この場合、Child コンポーネントで count を変更してしまうと、Parent コンポーネントの count も変わってしまいます。
言い換えると、コンポーネントの外にまで影響を及ぼすことがあるので、Props や State を変更することは避けるべきです。
4. Hookから返される値は「変えちゃダメ」
Hookから返される値とは?
Hook は React の機能で、関数コンポーネントで状態などの React の機能を使えるようにします。
Hook から返される値は、コンポーネント内で使うデータや関数のことです。
例えば、以下のコードで useUser
フックを使って、ユーザー情報を取得しています。
function ProfilePage() {
const user = useUser();
return <div>{user.name}さん</div>;
}
良い例と悪い例
では、Hook から返される値を変えてしまう悪い例を見てみましょう。
悪い例:
function ProfilePage() {
const user = useUser();
// 🙅♂️ ダメな例:占いの結果(user情報)を勝手に変更
user.lucky = true;
return <div>{user.name}さんは今日ラッキー!</div>;
}
user
は useUser
フックで取得した情報です。
この情報はあくまで読み取り専用で、変更する前提で作られていません。
言うなれば、もらったおみくじの結果を勝手に変えるのは失礼ですよね。
では、Hook から返される値を変えない良い例を見てみましょう。
良い例:
function ProfilePage() {
const user = useUser();
// 🙆♂️ いい例:新しい情報を作る
const luckyUser = { ...user, lucky: true };
return <div>{luckyUser.name}さんは今日ラッキー!</div>;
}
このように、新しいオブジェクトを作成することで、Hook から返される値を変更せずに、新しい情報を作ることができます。
なぜHookから返される値は「変えちゃダメ」?
Hook から返される値を変更してはいけない理由は、Props や State を変更してはいけない理由と似ています。
たとえば、先述した悪い例の場合、user
は useUser
フックで取得した情報です。
この情報はあくまで読み取り専用で、変更する前提で作られていません。
そのため、Hook から返される値を変更するとやはりコンポーネントの動作が予測しにくくなり、バグが発生する可能性があります。
5. Hookはトップレベルでのみ呼び出す
トップレベルとは?
コンポーネントの関数の一番外側のことを指します。具体的には、条件分岐やループ、ネストされた関数の中ではない部分です。
function Counter() {
// ここはトップレベル
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
if (count > 0) {
// ここはトップレベルではない
}
for (let i = 0; i < count; i++) {
// ここもトップレベルではない
}
//...
良い例と悪い例
では、トップレベルでHookを呼び出す良い例と悪い例を見てみましょう。
良い例:
function Counter() {
// ✅ Good: トップレベルでHookを呼び出している
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
// ...
}
// カスタムHook内でのHook呼び出しも可能
function useCustomHook() {
// ✅ Good: カスタムHook内でのHook呼び出し
const [value, setValue] = useState(0);
// ...
return value;
}
悪い例:
function Counter() {
if (count > 0) {
// 🔴 Bad: 条件分岐の中でHookを呼び出している
useEffect(() => {
document.title = `Count: ${count}`;
});
}
for (let i = 0; i < count; i++) {
// 🔴 Bad: ループの中でHookを呼び出している
const [item, setItem] = useState(null);
}
// ...
}
なぜトップレベルで呼び出す必要があるの?
Hookをトップレベルで呼び出す必要がある理由は、Reactが各レンダリングで同じ順序でHookが呼び出されることを期待しているからです。
例えば、レゴブロックを組み立てるときを想像してください。説明書では、常に同じ順序でブロックを積み上げていきますよね。しかし、し途中で順番を変えたり、ブロックを抜いたりすると、完成品が想定通りにならないかもしれません。
同じように、ReactはHookの呼び出し順序を覚えていて、それぞれのHookの状態を正しく管理しています。もし条件分岐やループの中でHookを呼び出すと、レンダリングごとにHookの順序が変わる可能性があり、Reactが正しく状態を管理できなくなってしまいます。
まとめ
今回は、React の「お約束」である「Rules of React」について紹介しました。
- コンポーネントとHookは「ピュア」であるべし!
- 副作用は「後片付け」のタイミングで実行する
- PropsとStateは「変えちゃダメ」
- Hookの結果も「変えちゃダメ」
- Hookはトップレベルでのみ呼び出す
React は自由に書ける反面、油断するとバグが発生しやすいかったり、予期せぬパフォーマンスの低下が起こりやすかったりします。
そのため、React の「お約束」を守ることで、正しく動きやすいコードを書くことができます。