再レンダリング

JS

Reactのコンポーネントが再レンダリングされるのは以下の3つの場合。

  1. stateが更新された時(同じ値をセットした場合は、Reactが最適化して再レンダリングをスキップします)
  2. propsが変更された時(React.memo()でメモ化されている子コンポーネントはpropsが変わらない限り再描画されません)
  3. 親コンポーネントが再レンダリングされた時(再レンダリングされたコンポーネント配下の子要素は再レンダリング)
    →対策
    React.memo()(子コンポーネントが props 変化しない限りスキップ)
    useMemo()useCallback() で関数・値の再生成を防止
    コンポーネントを key に依存して動的に切り替えない設計

(React18で開発環境でかつStrictModeのとき、2回レンダリングが走る。)

memo()

memo()の使い方はコンポーネントをmemo()で囲みます。

そうすることでpropsが変更されない限り再レンダリングしないで済みます。

import { memo } from "react"

export const ChildBox = memo((props) => { return() })

あるいはこう書きます↓

// React.memo() を使う場合
const ChildBox = React.memo((props) => {
  return <div>{props.name}</div>;
});

ただし、オブジェクトや配列は参照が変わるとレンダリングされます。

const [user, setUser] = useState({ name: 'John' });

setUser({ name: 'John' }); // 見た目は同じでも新しいオブジェクト → 再レンダリングされる

javascritptで↓こうなるのは、2つのオブジェクトは同じ内容でも異なるメモリ上の参照(アドレス)を持っているから。

{ name: 'John' } !== { name: 'John' }
const a = { name: 'John' };
const b = { name: 'John' };

console.log(a === b); // false ❌

===は中身ではなくアドレスを比較するので、falseになります。

const obj = { name: 'John' };
const a = obj;
const b = obj;

console.log(a === b); // true ✅

↑この場合はaもbも同じメモ帳の住所を持っているので、trueになります。

一方で文字列、数値、booleanなどのプリミティブ型は値を直接比較します。

const x = 'John';
const y = 'John';

console.log(x === y); // true ✅

オブジェクトの中身を比較するときはJSON.stringify()が使えます。

const a = { name: 'John' };
const b = { name: 'John' };

console.log(JSON.stringify(a) === JSON.stringify(b)); // true ✅

話がそれましたが、

const Parent = () => {
  const [user, setUser] = useState({ name: 'John' });

  return (
    <>
      <div>{user.name}</div>
      <button onClick={() => setUser({ name: 'John' })}>更新</button>
    </>
  );
};

↑これは押すたびにレンダリングされるので、

このように対処します。↓

const [user, setUser] = useState({ name: 'John' });

const handleClick = () => {
  // オブジェクトの参照が変更されないようにする
  if (user.name !== 'John') {
    setUser({ name: 'John' });
  }
};

配列も同様に毎回新しい配列として見なされ、再レンダリングしてしまいます。

import React from "react";

const Parent = () => {
  const items = [1, 2, 3]; // 毎回新しい配列

  return <Child list={items} />;
};

const Child = React.memo(({ list }) => {
  console.log("Child rendered");
  return <ul>{list.map(item => <li key={item}>{item}</li>)}</ul>;
});

export default Parent;

useCallback()

そこで、memo化しても再レンダリングされるのを防ぐのがuseCallback()です。

useCallback()は処理が変わらない場合は同じ関数として処理し、

メモ化されたコールバック関数を返すために使用され、

子コンポーネントに渡される関数が不要に再生成されるのを防ぎます。

useCallback()は、第2引数に配列を設定できます。

第2引数に依存関係の配列を設定し、この配列の要素が変わらない限り、

コールバック関数は再生成されません。

配列を空にすれば、最初に設定したものをずっと使うという処理になります。

import { useCallback } from "react"
const onClickClose = useCallback(() => setOpen(false) , []);

useMemo()

useMemo()は値(変数や計算結果)をメモ化するために使用します。

第二引数は依存配列を設定でき、その配列の値が変更されたときにのみ計算が再実行されます。

import React, { useState, useMemo } from 'react';

const Parent = () => {
  const [count, setCount] = useState(0);

  const expensiveCalculation = (num) => {
    console.log('Calculating...');
    return num * 2;
  };

  // useMemoを使って計算結果をメモ化
  const result = useMemo(() => expensiveCalculation(count), [count]);

  return (
    <div>
      <p>Result: {result}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default Parent;

key

Reactでのkeyはコンポーネントや要素を再描画するための識別子です。

特に.map()で、リストを描画するときにどの要素が変わったのかをReactが正確に把握するために必要です。

{users.map(user => (
  <UserCard key={user.id} user={user} />
))}

keyにインデックスを使うのは非推奨です。