Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

propsが変更されたときにすべてのstateをリセットするHOCを作る

react.devの「そのエフェクトは不要かも」のprops が変更されたときにすべての state をリセットするに書かれている解決策が微妙に感じたので、改善案を提案します。

そもそも: 公式の解決策がなぜ微妙に感じるのか?

props が変更されたときにすべての state をリセットするでは、propが変更されたときに必ずstateをリセットする制約を作るために、子コンポーネントへkeyを設定するだけのラッパーコンポーネントを作ろう と提案してます。

↓(修正前) userId propが更新されたとき、useEffectを使って comment stateをリセットしているコンポーネント

export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');

  // 🔴 Avoid: Resetting state on prop change in an Effect
  useEffect(() => {
    setComment('');
  }, [userId]);
  // ...
}

↓(修正後) userId propが更新されたとき、keyを使って comment stateをリセットしているコンポーネント

export default function ProfilePage({ userId }) {
  return (
    <Profile
      userId={userId}
      key={userId}
    />
  );
}

function Profile({ userId }) {
  // ✅ This and any other state below will reset on key change automatically
  const [comment, setComment] = useState('');
  // ...
}

…まあ言いたいことは理解できるんですが、 コンポーネントを分けなきゃいけないところが微妙に感じます。

  • Profileを単体でみたとき「userIdが変わったらcommentがリセットされる」という事情が読み取れない
  • 分割後の子コンポーネントの名前の付け方が難しい

このように 公式の解決策には問題があります。 なので、代わりにHOCを作ればいいんじゃないか?というのが本記事の提言です。

別の解決策: withResetOnPropChangeというHOCを作る

要はkeyを指定するだけのラッパーを作れたらよいのですから、それをする高階コンポーネント(Higher-Order Component)を作ればよさそうです。こんな感じです↓

const withResetOnPropChange =
  <Props extends Record<string, unknown>>(Component: FC<Props>, key: string) =>
  (props: Props) => <Component key={props[key] as string} {...props} />;

これを使うと上のProfilePageはこのように書けます。

export default withResetOnPropChange(function ProfilePage({ userId }) {
  // ✅ This and any other state below will reset on key change automatically
  const [comment, setComment] = useState('');
  // ...
}, 'userId');

コンポーネントの分割もなくなり、コードの意図もシンプルに読み解けるようになりました。個人的にはかなり良いんじゃないかなと思ってます。