【React】 memoとuseCallbackで無駄な再レンダを防ぐ
useCallback
は、レンダリング時にコンポーネントの関数が呼び出されるときに無駄な再レンダリングを防ぐものです。
目次
レンダリングの挙動の復習
Reactでは、コンポーネントがレンダリングされる際、そのコンポーネントの関数全体が毎回実行されます。
// xが変わる度に毎回Counterが呼ばれる export default function Counter() { const [x, setX] = useState(10); const countUp = () => { setX(x + 1); }; console.log("called"); return ( <div> <p>{x}</p> <button onClick={countUp}>カウントアップ</button> </div> ); }
そして、コンポーネントが更新されると、子のコンポーネントの関数も呼ばれます。例えば、
import Counter from './components/Counter'; export default function App() { const [s, setS] = useState(""); const setString = (event) => { setS(event.target.value); } return ( <div> <input type="text" value={s} onChange={setString} /> <Counter id={1} /> <Counter id={2} /> <Counter id={3} /> </div> ); }
/** * @typedef {{ * id: number * }} Props */ /** * @param {Props} props * @returns */ export default function Counter(props) { // 上の例と同じ console.log(`called: ${props.id}`); return ( // ... ); }
というコードがあった場合、App
関数内にあるinput
の入力状態を更新すると、子コンポーネントである全てのCounter
関数も呼ばれます。
propsで値を渡した場合
上の例では自分自身で値も管理していましたが、例えば親が値を持っていて、子がpropsで受け取った関数を呼ぶことで値を更新したとします。
export default function App() { const [x, setX] = useState(10); const [y, setY] = useState(10); const countUpX = () => { setX(x + 1); }; const countUpY = () => { setY(y + 1); }; return ( <div> <p>x: {x}</p> <p>y: {y}</p> <Counter id={1} countUp={countUpX} /> <Counter id={2} countUp={countUpY} /> </div> ); }
export default function Counter(props) { console.log(`called: ${props.id}`); return ( <div> <button onClick={props.countUp}>+1</button> </div> ); };
実際に動作させてみると、x
を更新するボタン1とy
を更新するボタン2のどちらを押しても、2つのボタンのコンポーネントが更新されることがわかります。
x
しか更新していないのに、y
しか影響しないボタンまで更新されるのは明らかに無駄です。そこで、useCallback
とmemo
を使用します。
React.memo
memo
は、コンポーネントに付与するものです。再レンダされそうになったときに状態をチェックし、再レンダ不要であれば再レンダされないようにするというものです。Props
の関係で使用することが多いと思います。
無駄に再呼び出しされる例
まずは無駄にコンポーネントが再呼び出しされる例を見てみましょう。
export default function App() { const [x, setX] = useState(10); const [y, setY] = useState(10); const inputX = (event) => { setX(event.target.value); } const inputY = (event) => { setY(event.target.value); } return ( <div> <label> inputX: <input type="text" value={x} onChange={inputX} /> </label> <label> inputY: <input type="text" value={y} onChange={inputY} /> </label> <ViewValue id="x" value={x} /> <ViewValue id="y" value={y} /> </div> ); }
export default function ViewValue(props) { console.log(`called: ${props.id}`); return ( <div> <p>{props.id}: {props.value}</p> </div> ); };
実際に動かしてみると、x
だけを更新したにもかかわらず、値に変化がないy
の方まで再度コンポーネントが呼び出されていることがわかります。(下の例でconsole
を開いてみてください。)
memoを使う
このような場合に子コンポーネント側でmemo
を使用することで、再呼び出しを防ぐことができます。
import { memo } from 'react'; export default memo((props) => { console.log(`called: ${props.id}`); return ( <div> <p>{props.id}: {props.value}</p> </div> ); });
実際に例を動かすと、x
を変更したときはx
の表示を行うコンポーネントだけが更新されていることがわかります。
useCallback
上の場合は、useState
した変数に関するものでした。もちろんconst
な変数であれば値が不変なため特に考える必要はありません。しかし、関数をPropで渡した場合は異なります。
なぜconstなのに値が変わった扱いになるのか
const
な値は、プリミティブ型であればその値そのものが入ります。しかし、オブジェクト型の場合、値の参照が入ります。中の値が同じでも代入する度にオブジェクトが新規作成され、異なる参照が代入されてしまいます。
Javascriptの比較演算子では、オブジェクト型の場合は中の値で比較されるわけではないということです。
例えば、
const x = 10; const y = 10; console.log(x == y);
はtrue
が出力されますが、
const x = [1, 2, 3]; const y = [1, 2, 3]; console.log(x == y);
では、[1, 2, 3]
はオブジェクト型であるため参照が異なり、false
が出力されます。これは関数でも同じことがいえます。
const x = () => {}; const y = () => {}; console.log(x == y); // false
しかし、使用しているオブジェクトが同じものを指している場合は、同じものとして認識されます。
const x = () => {}; const y = x; console.log(x == y); // true
厳密にはObject.is
が使用されています。Object.is
仕様に関してはMDNを参照してください。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object
useCallbackを使う
useCallback
を使用することで、関数をメモ化します。これを使用することで、同じ関数を使い回すことができるということです。上の例で言うy = x
ができるということです。
useCallback
の第1引数には動作させたい関数を、第2引数には関数を作り直す条件を入れます。
関数を更新する理由は、クロージャの仕様によるものです。レンダリングの度に関数が呼び出されるため、useState
した値などを更新すると、関数も作り直さないと古い変数を参照してしまうことになります。
export default function App() { const [x, setX] = useState(10); const [y, setY] = useState(10); // xの値が変わらない限り使い回す const countUpX = useCallback(() => { setX(x + 1); }, [x]); // yの値が変わらない限り使い回す const countUpY = useCallback(() => { setY(y + 1); }, [y]); return ( <div> <p>x: {x}</p> <p>y: {y}</p> <Counter id="x" countUp={countUpX} hoge={hoge} /> <Counter id="y" countUp={countUpY} hoge={hoge} /> </div> ); }
呼び出す際は、いつも通りcountUpX()
のようにして呼び出します。
上の例では、関数をuseCallback
したことで、1つの関数を使い回せるようになりました。
constな配列などでも同じことが言える?
はい、言えます。次の例を動かすと、useCallback
しなかった関数と同じような挙動を取ります。
export default function App() { // ... const hoge = [1, 2, 3]; return ( <div> <p>x: {x}</p> <p>y: {y}</p> {/* 追加でhogeを渡す */} <Counter id="x" countUp={countUpX} hoge={hoge} /> <Counter id="y" countUp={countUpY} hoge={hoge} /> </div> ); }
そうすると、どうやっても両方のCounter
が再呼び出しされることがわかります。この場合、
const [hoge, setHoge] = useState([1, 2, 3]); // 配列を変更する際は、setHoge([1, 2, 3, 4]); のように配列を作り直す必要あり
のようにしてオブジェクトを使い回すことで解決します。
まとめ
memo
とuseCallback
は、どちらも負荷を軽減するためのものです。基本的にはコンポーネントにmemo
を付与し、関数をコンポーネント内で作る際はuseCallback
を使用することをおすすめします。
