問題
上記のようなコンポーネントを作りました。
CardListコンポーネントの中でCardListItemコンポーネントが繰り返されている形です。
CardListコンポーネントではitemValuesという状態を持ちます。
もしどれかのカードの入力欄に値を入れると、親でitemValuesが更新され、全てのカードが再描画されてしまいます。
それでは、入力が実行されたカードだけ再描画できるようにしていきましょう。
リファクタ前のコード
CardList.tsx↓
import React, { useState } from "react";
import CardListItem from "./CardListItem";
const CardList = () => {
const [itemValues, setItemValues] = useState([
{ id: 1, value: "" },
{ id: 2, value: "" },
{ id: 3, value: "" },
]);
const changeValue = (id: number, value: string) => {
setItemValues(
itemValues.map((item) => {
if (item.id === id) {
item.value = value;
}
return item;
})
);
};
return (
<div>
<div className="mb-3">itemValues: {JSON.stringify(itemValues)}</div>
<div className="w-[50%] flex gap-2">
{itemValues.map((item) => (
<CardListItem
key={item.id}
id={item.id}
value={item.value}
onChange={changeValue}
/>
))}
</div>
</div>
);
};
export default CardList;
CardListItem.tsx↓
type CardListItemProps = {
id: number,
value: string,
onChange: (id: number, value: string) => void,
};
const CardListItem = ({ id, value, onChange }: CardListItemProps) => {
return (
<div className="w-[50%] flex flex-col mb-2 rounded-lg bg-lime-300 p-4">
<label htmlFor={id.toString()}>id: {id}</label>
<input
id={id.toString()}
type="text"
value={value}
onChange={(e) => onChange(id, e.target.value)}
/>
</div>
);
};
export default CardListItem;
リファクタ後のコード
変更1:CardListItemコンポーネントをmemo化する
memo化とは再計算を防ぐためにキャッシュ化することで、
メモ化されたコンポーネントは、props が変化しない限り再レンダリングされません。
import { memo } from "react";
type CardListItemProps = {
id: number,
value: string,
onChange: (id: number, value: string) => void,
};
const CardListItem = ({ id, value, onChange }: CardListItemProps) => {
return (
<div className="w-[50%] flex flex-col mb-2 rounded-lg bg-lime-300 p-4">
<label htmlFor={id.toString()}>id: {id}</label>
<input
id={id.toString()}
type="text"
value={value}
onChange={(e) => onChange(id, e.target.value)}
/>
</div>
);
};
// memo化
export default memo(CardListItem);
これで入力値が変わったCardListItemしか変更しないはずですよね、、
でも実はCardListItemコンポーネントに変更を加えると、
CardListが再描画され、その中で定義しているchangeValue関数も新しく作成され、異なる参照として扱われます。
つまりCardListItemコンポーネントに渡されるpropsのonChangeが変更したこととなり、せっかくmemo化したCardListItemコンポーネントも再描画されてしまいます。
変更2:useCallbackで関数の再描画を防ぐ
useCallback
を使うと、依存配列が変化しない限り、同じ関数の参照を再利用します。
CardListItemに渡すonChangeが変わらなくなるということですね。
これでmemoが活きます。
import React, { useCallback, useState } from "react";
import CardListItem from "./CardListItem";
const CardList = () => {
const [itemValues, setItemValues] = useState([
{ id: 1, value: "" },
{ id: 2, value: "" },
{ id: 3, value: "" },
]);
// useCallback関数内に入れる
const changeValue = useCallback((id: number, value: string) => {
setItemValues(
itemValues.map((item) => {
if (item.id === id) {
item.value = value;
}
return item;
})
);
}, []);
return (
<div>
<div className="mb-3">itemValues: {JSON.stringify(itemValues)}</div>
<div className="w-[50%] flex gap-2">
{itemValues.map((item) => (
<CardListItem
key={item.id}
id={item.id}
value={item.value}
onChange={changeValue}
/>
))}
</div>
</div>
);
};
export default CardList;
まとめ
- 繰り返されるコンポーネントはmemo化することを検討しましょう
- 関数を子供に渡している場合、その関数が不必要に参照が変わらないようuseCallbackで囲みましょう