👻
そのuseCallbackちゃんと効いていますか
onChange に useCallback を使う場合、ちゃんと useCallback が効く書き方がひとクセあったのでその備忘録です。
#1.処理フロー
今回は、よくある以下のような処理について考えていきます。
#2.Coding
まずは処理フローを元に show/onChange の処理を実装します。
※この段階ではまだ パフォーマンスは気にしません。
ParentComponent.tsx
import { useState } from "react";
import type { InputValue } from "~/ChildComponent";
import { ChildComponent } from "~/ChildComponent";
export const ParentComponent: React.VFC<{}> = () => {
const [value, setValue] = useState<InputValue>({
name: "defaultName",
description: "defaultDescription",
});
const handleChange = (newValue: InputValue): void => {
setValue(newValue);
};
return <ChildComponent value={value} onChange={handleChange} />;
};
ChildComponent.tsx
import { GrandchildComponent } from "~/GrandchildComponent";
export type InputValue = {
name: string;
description: string;
};
type Props = {
value: InputValue;
onChange: (value: InputValue) => void;
};
export const ChildComponent: React.VFC<Props> = ({ value, onChange }) => {
const handleChangeName = (newValue: string): void => {
onChange({
...value,
name: newValue,
});
};
const handleChangeDescription = (newValue: string): void => {
onChange({
...value,
description: newValue,
});
};
return (
<>
<GrandchildComponent value={value.name} onChange={handleChangeName} />
<GrandchildComponent value={value.description} onChange={handleChangeDescription} />
</>
);
};
GrandchildComponent.tsx
type Props = {
value: string;
onChange: (value: string) => void;
};
export const GrandchildComponent: React.VFC<Props> = ({ value, onChange }) => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const newValue = event.target.value;
onChange(newValue);
};
return <input value={value} onChange={handleChange} />;
};
#3.Performance tuning 👻
React.memo と useCallbackを使って再レンダーを抑止していきます。
ParentComponent.tsx
import { memo, useCallback, useState } from "react";
import type { InputValue } from "~/ChildComponent";
import { ChildComponent } from "~/ChildComponent";
export const ParentComponent: React.VFC<{}> = memo(() => {
const [value, setValue] = useState<InputValue>({
name: "defaultName",
description: "defaultDescription",
});
const handleChange = useCallback((newValue: InputValue): void => {
setValue(newValue);
}, []);
return <ChildComponent value={value} onChange={handleChange} />;
});
ChildComponent.tsx
import { memo, useCallback } from "react";
import { GrandchildComponent } from "~/GrandchildComponent";
export type InputValue = {
name: string;
description: string;
};
type Props = {
value: InputValue;
onChange: (value: InputValue) => void;
};
export const ChildComponent: React.VFC<Props> = memo(({ value, onChange }) => {
const handleChangeName = useCallback(
(newValue: string): void => {
onChange({
...value,
name: newValue,
});
},
[onChange, value]
);
const handleChangeDescription = useCallback(
(newValue: string): void => {
onChange({
...value,
description: newValue,
});
},
[onChange, value]
);
return (
<>
<GrandchildComponent value={value.name} onChange={handleChangeName} />
<GrandchildComponent value={value.description} onChange={handleChangeDescription} />
</>
);
});
GrandchildComponent.tsx
import { memo, useCallback } from "react";
type Props = {
value: string;
onChange: (value: string) => void;
};
export const GrandchildComponent: React.VFC<Props> = memo(({ value, onChange }) => {
const handleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>): void => {
const newValue = event.target.value;
onChange(newValue);
},
[onChange]
);
return <input value={value} onChange={handleChange} />;
});
#4.Performance tuning 🤔
React.memo
もuseCallback
も導入でき、一見これで不要な再レンダーが抑止できるようになったように見えます。
しかし、以下の箇所では、useCallback のdeps
にvalue
が含まれているため、他方の値が変更された場合も、コールバックが再生成されます。
e.g. value.name
が変更された場合、value
が変更されるためhandleChangeName
だけではなくhandleChangeDescription
も再生成されます。
ChildComponent.tsx
// ・・・
export const ChildComponent: React.VFC<Props> = memo(({ value, onChange }) => {
const handleChangeName = useCallback(
(newValue: string): void => {
onChange({
...value,
name: newValue,
});
},
[onChange, value]
);
const handleChangeDescription = useCallback(
(newValue: string): void => {
onChange({
...value,
description: newValue,
});
},
[onChange, value]
);
});
#5.Performance tuning 🧐
ChildComponent のonChange
の型を変更し、useCallback のdeps
からvalue
を抜くことにより、さらに不要な最レンダーを抑止することができます。
ParentComponent.tsx
import { memo, useCallback, useState } from "react";
import type { InputValue } from "~/ChildComponent";
import { ChildComponent } from "~/ChildComponent";
export const ParentComponent: React.VFC<{}> = memo(() => {
const [value, setValue] = useState<InputValue>({
name: "defaultName",
description: "defaultDescription",
});
const handleChange = useCallback((callback: (prevValue: InputValue) => InputValue): void => {
setValue((prevValue: InputValue): InputValue => {
return callback(prevValue);
});
}, []);
return <ChildComponent value={value} onChange={handleChange} />;
});
ChildComponent.tsx
import { memo, useCallback } from "react";
import { GrandchildComponent } from "~/GrandchildComponent";
export type InputValue = {
name: string;
description: string;
};
type Props = {
value: InputValue;
onChange: (callback: (prevValue: InputValue) => InputValue) => void;
};
export const ChildComponent: React.VFC<Props> = memo(({ value, onChange }) => {
const handleChangeName = useCallback(
(newValue: string): void => {
onChange((prevValue: InputValue): InputValue => {
return {
...prevValue,
name: newValue,
};
});
},
[onChange]
);
const handleChangeDescription = useCallback(
(newValue: string): void => {
onChange((prevValue: InputValue): InputValue => {
return {
...prevValue,
description: newValue,
};
});
},
[onChange]
);
return (
<>
<GrandchildComponent value={value.name} onChange={handleChangeName} />
<GrandchildComponent value={value.description} onChange={handleChangeDescription} />
</>
);
});
GrandchildComponent.tsx
import { memo, useCallback } from "react";
type Props = {
value: string;
onChange: (value: string) => void;
};
export const GrandchildComponent: React.VFC<Props> = memo(({ value, onChange }) => {
const handleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>): void => {
const newValue = event.target.value;
onChange(newValue);
},
[onChange]
);
return <input value={value} onChange={handleChange} />;
});
#6.さいごに
ググってもあまりこれに関する記事が出てこなかったので、情報ありましたら、コメント欄やツイッターで教えていただけると嬉しいです。
Discussion