mokajima.com

useCallback の依存リストに props.onChange を渡すと ESLint の警告が出る

useCallbackを使ってコールバックのメモ化を行うとき、依存リストに依存している値を渡します。以下の例では、この依存リストにprops.onChangeを渡しています。

type Props = {
  value: string;
  onChange: (value: string) => void;
};

const Child: React.FC<Props> = (props) => {
  const onChange = React.useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      props.onChange(event.target.value);
    },
    [props.onChange],
  );

  return (
    <input value={props.value} onChange={onChange} />
  );
};

const Parent: React.FC = () => {
  const [value, setValue] = React.useState<string>('');
  const onChange = React.useCallback((value: string) => {
    // do something
  }, []);

  return (
    <Child value={value} onChange={onChange} />
  );
};

一見問題なさそうですが、ESLint のreact-hooks/exhaustive-depsの警告が出てしまいます。警告の内容は次の通りです。

React Hook React.useCallback has a missing dependency: 'props'. Either include it or remove the dependency array. However, 'props' will change when any prop changes, so the preferred fix is to destructure the 'props' object outside of the useCallback call and refer to those specific props inside React.useCallback react-hooks/exhaustive-deps

どうやらprops.onChangeではなくpropsを依存リストに渡す必要があるようです。しかし、propsを依存リストに渡してしまうとonChange以外のpropsのプロパティ(この例ではvalue)に変更があった場合もコールバックが生成されてしまいます。そのため、useCallbackの外で分割代入を行い、onChangeを依存リストに追加するのが望ましいようです。

const Child: React.FC<Props> = ({ value, onChange }) => {
  const handleChange = React.useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      onChange(event.target.value);
    },
    [onChange],
  );

  return (
    <input value={value} onChange={handleChange} />
  );
};

propsではなくonChangeを依存リストに渡すのが望ましいのは理解できます。しかし、そもそもprops.onChangeでは警告が出てしまう理由が不明だったため、調べてみました。

The reason plugin sees it differently is because by doingprops.whatever()you are actually passingpropsitself as athisvalue towhatever. So technically it would see stale props. https://github.com/facebook/react/issues/16265

props.foo()markspropsas a dependency because it has athisvalue. https://github.com/facebook/react/blob/487c693846dbe58a68a410c658699d27386323b3/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js#L966

props.onChange()はオブジェクトpropsのメソッドonChangeを呼び出す形を取るため、thisにはpropsが設定されます。つまり、onChangepropsの別のプロパティをthis経由で参照している可能性があり、props.onChangeのみを依存リストに渡してしまうと、その別のプロパティの変更を検知できないことになります。そのため、依存リストにprops全体を渡すよう警告を出しているようです。

なお、分割代入を行った場合、props.onChangeを変数onChangeに代入するため、onChange内のthisundefinedとなります。そのため、props.onChangeと異なりpropsを依存リストに渡す必要はありません。

参考