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