LoginSignup
69
66

React再レンダリングガイド: 一度に全て理解する

Last updated at Posted at 2023-09-28

この記事は 『React re-renders guide: everything, all at once』 の翻訳記事です。
ご本人( Nadia Makarevich さん)に許可をいただいて翻訳しています。

スクリーンショット 2023-09-28 21.59.08.png

以下、翻訳記事となります。

React再レンダリングガイド: 一度に全て理解する

Reactの再レンダリングを詳細に網羅したガイドです。
このガイドでは、次の内容を説明します。

  • 再レンダリングとは何か?
  • 必要な再レンダリングと不要な再レンダリングとは何か?
  • 何がReactコンポーネントの再レンダリングをトリガーするのか?

また、以下の内容を含みます。

  • 再レンダリングを防ぐための最も重要なパターン
  • 不要な再レンダリングと、パフォーマンスの低下につながるアンチパターン

すべてのパターンとアンチパターンには、図とコード例が付いています。

この記事のいくつかの章は動画でも見ることができます。
https://www.youtube.com/channel/UCxz8PLj1ld6y-zpJmpqpOrw

目次

  1. Reactの再レンダリングとは何か?
  2. Reactコンポーネントはいつ再レンダリングされるか?
  3. コンポジションを使って再レンダリングを防ぐ
  4. React.memo を使って再レンダリングを防ぐ
  5. useMemo/useCallback を使って再レンダリングのパフォーマンスを向上させる
  6. リストの再レンダリングのパフォーマンスを向上させる
  7. Context によって引き起こされる再レンダリングを防ぐ

1. React の再レンダリングとは何か?

この章は動画でも見ることができます。
https://www.youtube.com/watch?v=ARWX1XdghLk&t=39

Reactのパフォーマンスについて話すとき、2つの段階(stage)について気をつける必要があります。

  • 初期レンダリング - コンポーネントが最初に画面に表示されるときに発生するレンダリングのこと
  • 再レンダリング - すでに画面に表示されているコンポーネントで発生する2回目以降のレンダリングのこと

再レンダリングは、Reactが新しいデータでアプリケーションを更新するときに発生します。たいていの場合、ユーザーがアプリケーションを操作したり、非同期のリクエストやサブスクリプションモデルによって取得した外部データを操作することによって起こります。

非同期のデータ更新がないようなインタラクティブでないアプリケーションにおいては、再レンダリングが発生することは 決してありません。したがって、そのようなアプリケーションについて再レンダリングのパフォーマンス最適化を考える必要はありません。

🧐 必要な再レンダリングと不要な再レンダリングとは何か?

必要な再レンダリング - 変更元であるコンポーネントや、新しい情報を直接使用するコンポーネントの再レンダリングです。たとえば、ユーザーが入力フォームに何か入力した場合、その状態を管理しているコンポーネントは、ユーザーがキーを押すごとに自身を更新する必要があります。これが必要な再レンダリングです。

不要な再レンダリング - 間違った非効率的なアプリケーションの設計から発生する、さまざまな再レンダリングによってアプリケーション全体に伝搬してしまうような再レンダリングです。たとえば、ユーザーが入力フィールドに何か入力し、ユーザーがキーを押すごとにページ全体が再レンダリングされた場合、そのページは不必要に再レンダリングされています。

不要な再レンダリング自体は 問題ではありません。Reactは非常に高速であるため、たいていの場合ユーザーにとっては気にならないものでしょう。

しかし、再レンダリングがあまりにも多く発生していたり、非常に重いコンポーネントで実行されたりすると、ユーザー体験としては「ラグっている(遅延している)」ように見えたり、すべての操作で明白な遅延が発生したり、アプリケーションがまったく反応しなくなったりするかもしれません。

2. Reactコンポーネントはいつ再レンダリングされるか?

この章は動画でも見ることができます。
https://www.youtube.com/watch?v=ARWX1XdghLk&t=244

コンポーネントが再レンダリングされる理由(原因)は4つあります。

  1. 状態(state)の変更
  2. 親 (または子/children) の再レンダリング
  3. Context の変更
  4. React hooks の変更

また、コンポーネントが再レンダリングされる原因について、大きな誤解もあります。

  • コンポーネントの props が変更されると再レンダリングが発生する

というものです。これは事実ではありません (以下の説明を参照)。

🧐 再レンダリングの原因: 状態(state)の変更

コンポーネントの状態が変化すると、そのコンポーネントは再レンダリングされます。
たいていの場合、コールバックか useEffect フックのどちらかで発生します。

状態の変更は、すべての再レンダリングの「最も根本的な」原因("root" source)です。

codesandbox の例を参照してください

const Component = () => { // 2. 再レンダリング
  const [state, setState] = useState(); // 1. 状態の変更

  return ...
};

🧐 再レンダリングの原因: 親の再レンダリング

コンポーネントは、そのコンポーネントの親コンポーネントが再レンダリングされると、自身も再レンダリングします。
これは逆に言えば、コンポーネントが再レンダリングされると、そのコンポーネントのすべての子コンポーネントも再レンダリングされるということです。

コンポーネントの再レンダリングは、コンポーネントツリーを常に「下方向」に進みます。
すなわち、子コンポーネントの再レンダリングは、親コンポーネントの再レンダリングをトリガーしません。(ただし、いくつか注意点と特殊ケースがあります。詳細については次のガイドで十分に説明しています: The mystery of React Element, children, parents and re-renders)。

codesandbox の例を参照してください

const Parent = () => { // 1. 再レンダリング
  return <Child /> // 2. 再レンダリング
};

🧐 再レンダリングの原因: Context の変更

Context API の Context Provider の値が変更されると、その Context を使用するすべてのコンポーネントが再レンダリングされます。たとえ、変更されたデータの部分を直接的に使用していなくてもです。これらの再レンダリングをメモ化によって防ぐことはできませんが、近い方法を用いたいくつかの回避策があります (Part 7: preventing re-renders caused by Contextを参照)。

codesandbox の例を参照してください

const useValue = useContext(Context) // 1. Context の値の変更

const Component1 = () => {
  const value = useValue(); // 2. 再レンダリング

  return ...
};

const Component2 = () => {
  const value = useValue(); // 2. 再レンダリング

  return ...
};

🧐 再レンダリングの原因: React hooks の変更

フック内で起こっているすべてのことは、それを使用しているコンポーネントに「属して」います。
「Context の変更」「状態(state)の変更」と同じルールがここでも当てはまります。

  • フック内の状態の変更は、"ホスト"コンポーネント(フックを使用しているコンポーネント)の再レンダリングをトリガーします。これは 防ぎようがありません
  • フックが Context を使用し、Context の値が変更されると、"ホスト"コンポーネントの再レンダリングをトリガーします。これは 防ぎようがありません

フックはチェーン(あるフック内で別のフックを使用)することができます。チェーンされていたとしても、各フックは"ホスト"コンポーネントに「属して」おり、どのフックにも同じ上記のルールが適用されます。

codesandbox の例を参照してください

const useSomething = useContext(Context) // 1. Context の値の変更
const useValue = {
  useSomething() // 2. チェーンが反応
}

const Component = () => { // 3. 再レンダリング
  const value = useValue();

  return ...
};

⛔️ 再レンダリングの理由: props の変更 (大きな間違い)

メモ化されていないコンポーネントの再レンダリングについて、コンポーネントの props が変更されるかどうかは関係ありません。

props を変更するには、親コンポーネントによって更新される必要があります。つまり、親コンポーネントが再レンダリングされなければならないということです。これにより子コンポーネントの再レンダリングがトリガーされます。props は関係ありません。

codesandbox の例を参照してください

メモ化のテクニック( React.memouseMemo )が使用される場合にのみ、 props の変更が重要になります。

const Parent = () => { // 1. 再レンダリング
  return <Child value={{ value }} /> // 2. 再レンダリング(props の `value` は関係なし)
};

3. コンポジションを使って再レンダリングを防ぐ

この章は動画でも見ることができます。
https://www.youtube.com/watch?v=7sgBhmLjVws

⛔️ アンチパターン: render 関数でコンポーネントを作成する

あるコンポーネントを別のコンポーネントの render 関数内で作成することは、パフォーマンスを最大限に低下させるアンチパターンです。
再レンダリングのたびに、Reactはそのコンポーネントを再マウントします (つまり、コンポーネントを破棄して、また最初から再作成します)。これは通常の再レンダリングよりもはるかに遅くなります。さらにこれにより、次のようなバグを引き起こす可能性があります。

  • 再レンダリング中にコンテンツが「フラッシュする」可能性がある
  • 再レンダリングのたびにコンポーネント内の状態がリセットされる
  • 依存関係のない useEffect が再レンダリングのたびにトリガーされる
  • コンポーネントがフォーカスされていた場合、フォーカスが失われる

codesandbox の例を参照してください

追加リソース: How to write performant React code: rules, patterns, do's and don'ts

bad

const Component = () => { // 1. 再レンダリング
  const SlowComponent = () => <Something />; // 2. 再作成されたコンポーネント

  return (
    <SlowComponent /> // 3. 再マウント!!
  )
}

good

const SlowComponent = () => <Something />; // 2. (再マウントされない)同じコンポーネントのまま

const Component = () => { // 1. 再レンダリング

  return (
    <SlowComponent /> // 3. 再レンダリングされるだけ
  )
}

✅ コンポジションを使って再レンダリングを防ぐ: 状態を下(子コンポーネント)に移動する

この章は動画でも見ることができます。
https://www.youtube.com/watch?v=7sgBhmLjVws&t=106s

このパターンは、重いコンポーネントが状態を管理しており、この状態がレンダリングツリーの一部のみで使用されている場合に有効です。典型的な例としては、ページの大部分をレンダリングする複雑なコンポーネントで、ボタンをクリックしてダイアログを開閉するような場合です。

この場合、モーダルダイアログの表示を制御している状態、ダイアログ自身、また更新をトリガーするボタンを、より小さなコンポーネントにカプセル化するとよいでしょう。そうすることで、大きなコンポーネントは、それらの状態が変更されても再レンダリングされなくなります。

codesandbox の例を参照してください

追加リソース: The mystery of React Element, children, parents and re-rendersHow to write performant React code: rules, patterns, do's and don'ts

bad

const Component = () => {
  const [open, setOpen] = useState(false); // 1. 再レンダリングをトリガーする

  return (
    <Something>
      <Button onClick={() => setOpen(true)} /> {/* 1. 再レンダリングをトリガーする */}
      {isOpen && <ModalDialog />}
      <VerySlowComponent /> {/* 2. 再レンダリング */}
    </Something>
  );
};

good

const ButtonWithDialog = () => {
  const [open, setOpen] = useState(false); // 1. 再レンダリング

  return (
    <>
      <Button onClick={() => setOpen(true)} /> {/* 1. 再レンダリング */}
      {isOpen && <ModalDialog />}
    </>
  );
};

const Component = () => {

  return (
    <Something>
      <ButtonWithDialog /> 
      <VerySlowComponent /> {/* 2. 影響を受けない */}
    </Something>
  );
};

✅ コンポジションを使って再レンダリングを防ぐ: props としての children

この章は動画でも見ることができます。
https://www.youtube.com/watch?v=7sgBhmLjVws&t=272s

このパターンは「children の周りに状態をラップする」とも言えます。「状態を下(子コンポーネント)に移動する」パターンと似ており、状態の変更をより小さなコンポーネントにカプセル化します。ここで違うところは、状態はレンダリングツリーの遅い部分をラップする要素において使用されているため、抽出するのがそれほど簡単ではないことです。典型的な例として、コンポーネントのルート要素にアタッチされた onScrollonMouseMove コールバックなどがあるでしょう。

このような状況では、状態管理とその状態を使用するコンポーネントは小さなコンポーネントに抽出して、遅いコンポーネントはそのコンポーネントの children として渡してあげるとよいでしょう。小さなコンポーネントから見ると、children は単なる props であるため、状態の変更の影響を受けません。つまり、再レンダリングは発生しないということです。

codesandbox の例を参照してください

追加リソース: The mystery of React Element, children, parents and re-renders

bad

const Component = () => {
  const [value, setValue] = useState({}); // 1. 再レンダリングをトリガーする

  return (
    <div onScroll={(e) => setValue(e)}> {/* 1. 再レンダリングをトリガーする */ }
      <VerySlowComponent /> {/* 2. 再レンダリング */}
    </div>
  );
};

good

const ComponentWithScroll = () => {
  const [value, setValue] = useState({}); // 1. 再レンダリングをトリガーする

  return (
    <div onScroll={(e) => setValue(e) }> {/* 1. 再レンダリングをトリガーする */ }
      {children} {/* 2. 単なる props なので、影響を受けない */}
    </div>
  );
};

const Component = () => {

  return (
    <ComponentWithScroll>
      <VerySlowComponent /> {/* 3. 影響を受けない */}
    </ComponentWithScroll>
  );
};

✅ コンポジションを使って再レンダリングを防ぐ: props としてのコンポーネント

この章は動画でも見ることができます。
https://www.youtube.com/watch?v=7sgBhmLjVws&t=462s

前のパターンとほとんど同じで、動作も同じです。小さいコンポーネント内に状態をカプセル化し、重いコンポーネントが props として渡されます。props は状態の変更の影響を受けないため、重いコンポーネントが再レンダリングされることはありません。

いくつかの重いコンポーネントは状態から独立しているが、children のようなグループとして抽出できない場合に便利です。

codesandbox の例を参照してください

コンポーネントを props として渡す方法について、詳しくはこちらをご覧ください: React component as prop: the right way™️The mystery of React Element, children, parents and re-renders

bad

const Component = () => {
  const [value, setValue] = useState({}); // 1. 再レンダリングをトリガーする

  return (
    <div onScroll={(e) => setValue(e)}> {/* 1. 再レンダリングをトリガーする */ }
      <SlowComponent1 /> {/* 2. 再レンダリング */}
      <Something /> {/* 2. 再レンダリング */}
      <SlowComponent2 /> {/* 2. 再レンダリング */}
    </div>
  );
};

good

const ComponentWithScroll = ({ left, right }) => {
  const [value, setValue] = useState({}); // 1. 再レンダリングをトリガーする

  return (
    <div onScroll={(e) => setValue(e) }> {/* 1. 再レンダリングをトリガーする */ }
      {left} {/* 2. 単なる props なので、影響を受けない */}
      <Something />
      {right} {/* 2. 単なる props なので、影響を受けない */}
    </div>
  );
};

const Component = () => {

  return (
    <ComponentWithScroll>
      left={<SlowComponent1 />} {/* 2. 影響を受けない */}
      right={<SlowComponent2 />} {/* 2. 影響を受けない */}
    />
  );
};

4. React.memo を使って再レンダリングを防ぐ

この章は動画でも見ることができます。
https://youtu.be/feEY3Qajrwg

React.memo でコンポーネントをラップすると、このコンポーネントの props が変更されない限り、レンダリングツリーの上部でトリガーされる再レンダリングの連鎖を下流で防ぐことができます。

これは、再レンダリングのソース (つまり、状態や変更されたデータ) に依存しない重いコンポーネントをレンダリングするときに役立ちます。

codesandbox の例を参照してください

bad

const Parent = () => { // 1. 再レンダリング

  return <Child /> // 1. 再レンダリング
};
const ChildMemo = React.memo(Child);

const Parent = () => { // 1. 再レンダリング

  return <ChildMemo /> // 2. 再レンダリングされない
};

✅ React.memo: props を持つコンポーネント

React.memo が機能するには、プリミティブな値でない すべての props をメモ化する必要があります。

codesandbox の例を参照してください

bad

const ChildMemo = React.memo(Child);

const Parent = () => { // 1. 再レンダリング

  return (
    <ChildMemo
      value={{ value }} // 2. `value` が変更され、再レンダリングされる
    />
  )
};

good

const ChildMemo = React.memo(Child);

const Parent = () => { // 1. 再レンダリング
  const value = useMemo(() => ({ value }), []) // 2. `value` は変更されない

  return (
    <ChildMemo
      value={value} // 3. 再レンダリングされない
    />
  )
};

✅ React.memo: props や children としてのコンポーネント

React.memo は、 children や props として渡される要素に使用しなければなりません。
親コンポーネントをメモ化しても効果は得られません。children や props はオブジェクトになるため、再レンダリングのたびに変更されてしまいます。

子/親の関係に関するメモ化の仕組みの詳細については、こちらを参照してください: The mystery of React Element, children, parents and re-renders

codesandbox の例を参照してください

bad

const ChildMemo = React.memo(Child);

const Parent = () => { // 1. 再レンダリング

  return (
    <ChildMemo left={
      <Something /> {/* 
2. 再レンダリング */}
    }> 
      <GrandChild /> {/* 
2. 再レンダリング */}
    </ChildMemo>
  )
}

good

const SomethingMemo = React.memo(Something);
const GrandChildMemo = React.memo(GrandChild);

const Parent = () => { // 1. 再レンダリング

  return (
    <Child left={
      <SomethingMemo /> {/* 
2. 再レンダリングされない */}
    } />
        <GrandChildMemo /> {/* 
2. 再レンダリングされない */}
    </Child>
  )
}

5. useMemo/useCallback を使って再レンダリングのパフォーマンスを向上させる

⛔️ アンチパターン: props に対する不要な useMemo/useCallback

props 自体をメモ化しても、子コンポーネントの再レンダリングは防げません。
親コンポーネントが再レンダリングされれば、props に関係なく、子コンポーネントの再レンダリングがトリガーされます。

codesandbox の例を参照してください

const Parent = () => { // 1. 再レンダリング
  const value = useMemo(() => ({ value }))

  return (
    <Child
      value={value}  {/* 関係ない */}
    /> {/* 2. 再レンダリング */}
};

✅ 必要な useMemo/useCallback

子コンポーネントが React.memo でラップされている場合、プリミティブな値ではないすべての props をメモ化する必要があります。

codesandbox の例を参照してください

const ChildMemo = React.memo(Child)

const Parent = () => { // 1. 再レンダリング
  const value = useMemo(() => ({ value })

  return (
    <ChildMemo {/* 3. 関係なし */}
      value={value} {/* 2. メモ化する必要がある */}
    />
};

コンポーネントが、useEffectuseMemouseCallback のようなフックを使用しており、依存関係としてプリミティブでない値を使用する場合、それらはメモ化されるべきです。

codesandbox の例を参照してください

const Parent = () => { // 1. 再レンダリング
  const value = useMemo(() => ({ value }))

  useEffect(() => {
    // do something
  }, [value]) // 2. メモ化する必要がある

  return ...
};

✅ 高コストな計算のための useMemo

useMemo のユースケースの1つは、再レンダリングのたびに高コストな計算が行われるのを避けることです。

しかし useMemo はコストがかかる(メモリを少し消費し、初期レンダリングがわずかに遅くなる)ため、すべての計算に対して使用されるべきではありません。Reactでは、コンポーネントのマウントと更新が、ほとんどの場合で最もコストが高い計算になります (フロントエンドで素数の計算を実際に行うような場合を除きます)。

useMemo の典型的なユースケースは、React要素をメモ化することです。React要素とは、たいてい既存のレンダリングツリーの一部や、新しい要素を返す map 関数のような、生成されたレンダリングツリーの結果です。

配列のソートやフィルタリングなどの「純粋な」JavaScript の操作のコストは、コンポーネントの更新に比べれば、たいてい無視できるほど低コストです。

codesandbox の例を参照してください

bad

const Component = () => { // 1. 再レンダリング

  return (
    <>
      <Something />
      <SlowComponent /> {/* 2. 再レンダリング */}
      <SomethingElse />
    </>
  );
};

good

const Component = () => { // 1. 再レンダリング
  const slowComponent = useMemo(() => {
    return <SlowComponent /> {/* 2. 再レンダリングされない */}
  }, [])

  return (
    <>
      <Something />
      {slowComponent} {/* 2. 再レンダリングされない */}
      <SomethingElse />
    </>
  );
};

6. リストの再レンダリングのパフォーマンスを向上させる

この章は動画でも見ることができます。
https://youtu.be/76OedwmXlYY

通常の再レンダリングのルールとパターンに加え、key 属性はReactのリストのパフォーマンスに影響を与えます。

重要: key 属性を指定するだけでは、リストのパフォーマンスは向上しません。リスト要素の再レンダリングを防ぐには、 React.memo でリスト要素をラップする必要があります。

key の値は、リスト内のすべての要素に対して再レンダリング間で一貫性のある文字列であるべきです。通常、これにはリストアイテムの id または配列の index が使用されます。

リストが 静的 な(要素を追加/削除/挿入/ソートできない)のであれば、キーとして配列の index 使用しても問題ありません。

動的なリストで配列の index を使用すると、次のような問題につながります。

  • リストアイテムが状態を持っていたり(フォーム入力のような)非制御要素を含む場合、バグが発生します
  • リストアイテムが React.memo でラップされている場合、パフォーマンスが低下します

詳細については、「React key attribute: best practices for performant lists」を参照してください。

codesandbox - 静的リスト の例を参照してください。

codesandbox - 動的リスト の例を参照してください。

bad

const Component = () => { // 1. 再レンダリング

  return (
    <>
      {items.map((item) => (
        <Child
          item={item}
          key={item.id} {/* 3. 再レンダリングを防げない */}
        /> {/* 2. 再レンダリング */}
      ))};
    </>;
  );
};

good

const ChildMemo = React.memo(Child)

const Component = () => { // 1. 再レンダリング

  return (
    <>
      {items.map((item) => (
        <ChildMemo
          item={item}
          key={item.id}
        /> {/* 2. 再レンダリングされない */}
      ))};
    </>;
  );
};

⛔️ アンチパターン: リストのキーとしてランダムな値を使用する

ランダムに生成された値は、リストの key 属性の値として使用しないでください。
これらは、再レンダリングのたびに React がリストアイテムを再マウントすることになり、次のようなことが起こります。

  • リストのパフォーマンスが非常に悪くなります
  • リストアイテムが状態を持っていたり(フォーム入力のような)非制御要素を含む場合、バグが発生します

codesandbox の例を参照してください

bad

const ChildMemo = React.memo(Child)

const Component = () => { // 1. 再レンダリング

  return (
    <>
      {items.map((item) => (
        <ChildMemo
          item={item}
          key={Math.random()} {/* 2. レンダリングされるたびに再マウントされてしまう */}
        /> {/* 2. 再マウント!! */} 
      ))}
    </>
  )
}

7. Context によって引き起こされる再レンダリングを防ぐ

✅ Context の再レンダリングを防ぐ: Provider の値のメモ化

Context API の Context Provider がアプリケーションのルートに置かれておらず、Context Provider の祖先コンポーネントの変更により Context Provider 自体が再レンダリングされる場合は、その値をメモ化する必要があります。

codesandbox の例を参照してください

bad

const Component = () => { // 1. 再レンダリング

  return (
    <Context.Provider
     value={{ value }} {/* 2. `value` が変更され */}
   > {/* 2. すべてが再レンダリングされる! */}
      {children}
    </Context.Provider>
  );
};

good

const Component = () => { // 1. 再レンダリング
  const memoValue = useMemo(() => ({ value }), [])

  return (
    <Context.Provider
      value={memoValue} {/* 2. 同じ値のまま */}
    > {/* 3. 何も再レンダリングされない! */}
      {children}
    </Context.Provider>
  );
};

✅ Context の再レンダリングを防ぐ: データと API の分割

Context 内にデータと API (ゲッターとセッター) の組み合わせがある場合、それらを同じコンポーネント内の異なるプロバイダーに分割できます。こうすることで、API のみを使用するコンポーネントは、データが変更されたときに再レンダリングされなくなります。

このパターンの詳細については、こちらをご覧ください:How to write performant React apps with Context

codesandbox の例を参照してください

bad

const Component = () => {
  const [state, setState] = useState(); // 1. 状態が変更される

  const value = useMemo(
    () => ({
      data: state, 
      api: (data) => setState(data),
    }),
    [state],
  );

  return (
    <Context.Provider value={
     { value }
   }> {/* 2. すべての Consumer が再レンダリングされる */}
      {children}
    </Context.Provider>
  );
};

good

const Component = () => {
  const [state, setState] = useState(); // 1. 状態が変更される

  return (
    <DataContext.Provider value={state}>
      <ApiContext.Provider
        value={setState}
      > {/* 2. API の Consumer は再レンダリングされない */}
        {children}
      </ApiContext.Provider>
    </DataContext.Provider>
  );
};

✅ Context の再レンダリングを防ぐ: データをかたまり(chunks)に分割する

Context が複数の独立したデータを管理している場合、それらを同じ Provider 内で小さな Provider に分割できます。こうすることで、変更されたデータの Consumer のみを再レンダリングするようにできます。

このパターンの詳細については、こちらをご覧ください: How to write performant React apps with Context

codesandbox の例を参照してください

bad

const Component = () => {
  const [first, setFirst] = useState(); // 1. first が変更される
  const [second, setSecond] = useState();

  const value = useMemo(
    () => ({
      first: first, 
      second: second,
    }),
    [first, second],
  );

  return (
    <Context.Provider value={
     { value }
   }> {/* 2. すべての Consumer が再レンダリングされる */}
      {children}
    </Context.Provider>
  );
};

good

const Component = () => {
  const [first, setFirst] = useState(); // 1. first が変更される
  const [second, setSecond] = useState();

  return (
    <Data1Context.Provider value={frist}>
      <Data2Context.Provider
        value={second}
      > {/* 2. second の Consumer は再レンダリングされない */}
        {children}
      </ApiContext.Provider>
    </DataContext.Provider>
  );
};

✅ Context の再レンダリングを防ぐ: Context セレクタ(Context の一部の値の選択)

Context の一部の値のみを使用するコンポーネントの再レンダリングを防ぐことはできません。
たとえ、使用されているデータが変更されていなくても、 useMemo フックを使用しても同様です。

しかし、高階コンポーネントと React.memo を使用することで、Context セレクタ(Context の一部の値のみの選択) のようなことができます。

このパターンの詳細については、こちらをご覧ください: Higher-Order Components in React Hooks era

codesandbox の例を参照してください

bad

const useSomething = () => {
  const { something } = useContext(Context); // 1. "something" が変更されていなくても、再レンダリングをトリガーする

  return useMemo(() => something, [something]); // 2. useMemo は意味なし
};

const Component = () => {
  const { something } = useSomething(); // 3. "something" が変更されていなくても、再レンダリングをトリガーする

  return ...
};

good

const withSomething = (Component) => {
  const MemoComponent = React.memo(Component); // 1. Component がここでメモ化される

  return () => {
    const { something } = useContext(Context); 

    return <MemoComponent something={something} /> // 2. "something" が変更された時だけ再レンダリングされる
  }
};

const Component = withSomething(( { something }) => { // 3. "something" が変更された時だけ再レンダリングされる

  return ...
};

訳者あとがき

Reactを使った開発に携わることになり、再レンダリングについてよりはむしろ、良いコンポーネント設計について模索していたところ、こちらのブログ記事に出会いました。

コンパクトな分量でさまざまな再レンダリングを防ぐテクニックがまとめられた本記事ですが、最も基本的なReactコンポーネントの設計原則は、"コンポジション" を適切に使う、ことではないかと思わせてくれます。

"コンポジション"は、オブジェクト指向プログラミングにおいて、"継承"と合わせて語られることが多い概念のように思います。継承が is-a関係(Cat "is a" Animal, Bycicle "is a" Veichle)であるのに対して、コンポジションはhas-a関係(Dialog "has a" Button, Bycicle "has a" Wheel)であり、継承よりコンポジションを使うことでコードの再利用性を高める、といったような文脈で見られる語です。

コンポジションを使ったテクニックについて、この記事では以下の3つの方法が紹介されています。

  • moving state down(状態を下に移動する)
  • childrend as props(props としての children)
  • components as props(props としての component)

同様のテクニックの推奨は、さまざまな記事で見られます。

One simple trick to optimize React re-renders - Kent C. Dodds

  • "Lift" the expensive component to a parent where it will be rendered less often.(コストの高いコンポーネントは、それほど頻繁にはレンダリングされない親コンポーネントに移動する)
  • Then pass the expensive component down as a prop.(そして、 props としてコストの高いコンポーネントを渡してもらう)

Before you memo - Overreacted

  • Move State Down(状態を下に移動する)
  • Lift Content Up(中身を上に持ってくる)

Composition - Material UI

  • Wrapping components(コンポーネントをラップする)
  • Component prop(コンポーネントの props)

Composition vs Inheritance - legacy.reactjs.org

コンポーネントの中には事前には子要素を知らないものもあります。...このようなコンポーネントでは特別な children という props を使い、...受け取った子要素を出力することができます。
...
汎用的なコンポーネントに props を渡して設定することで、より特化したコンポーネントを作成することができます。
...
props とコンポジションにより、コンポーネントの見た目と振る舞いを明示的かつ安全にカスタマイズするのに十分な柔軟性が得られます。

Before you use context - Passing Data Deeply with Context - react.dev

  1. まずは propsを渡す方法から始めましょう。
    ...
  2. コンポーネントを抽出して、children を JSX として渡す方法を検討しましょう。
    もし、何らかのデータを、それを必要とせずただ下に流すだけの中間コンポーネントを何層も経由して受け渡ししているような場合、何かコンポーネントを抽出するのを忘れているということかもしれません。たとえば、 <Layout posts={posts} /> のような形で、データを直接使わないビジュアルコンポーネントに post のようなデータを渡しているのかもしれません。代わりに、Layout は children を props として受け取るようにし、 <Layout><Posts posts={posts} /></Layout> のようにレンダーしてみましょう。これにより、データを指定するコンポーネントとそれを必要とするコンポーネントの間のレイヤ数が減ります。
    これらのアプローチがどちらもうまくいかない場合は、コンテクストを検討してください。

今後Reactの開発に携わることがあれば、ここで出てきた『コンポジション』、『props』、『children』といったキーワードを念頭に、コンポーネント設計に向き合ってみてはいかがでしょうか。

69
66
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
69
66