👻

そのuseCallbackちゃんと効いていますか

2021/08/10に公開

onChange に useCallback を使う場合、ちゃんと useCallback が効く書き方がひとクセあったのでその備忘録です。

#1.処理フロー

今回は、よくある以下のような処理について考えていきます。

uml

#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.memouseCallbackを使って再レンダーを抑止していきます。

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.memouseCallbackも導入でき、一見これで不要な再レンダーが抑止できるようになったように見えます。
しかし、以下の箇所では、useCallback のdepsvalueが含まれているため、他方の値が変更された場合も、コールバックが再生成されます。
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.さいごに

ググってもあまりこれに関する記事が出てこなかったので、情報ありましたら、コメント欄やツイッターで教えていただけると嬉しいです。

GitHubで編集を提案
株式会社ナンバーフォー

Discussion