React の state を key でリセットする
株式会社IVRy (アイブリー)のエンジニアの kinashi です。
普段 React の key は使っていますでしょうか?
このように配列からリストなどをレンダーするときに指定するのが一般的な使い方かと思います。
data.map(datum => (
<li key={datum.id}>{datum.name}</li>
))
この記事では key を使って特定の state をクリアする実装について考えてみようと思います。
// https://ja.react.dev/learn/preserving-and-resetting-state から引用
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
key による state のリセット
先日開発をしていて、特定の操作をトリガーにして複数のフォームをリセットしたいことがあり、調べていたら React の公式ドキュメントにたどり着きました。
(今年公式ドキュメントが新しくなり、ちゃんと読めていませんでしたが、丁寧に図が挿入されていたり、チャレンジ問題で理解を深める項目があるなど、すごく読みやすかったです)
この中に key で state をリセットするという項目があり、こう記載されています。
key はリストのためだけのものではありません! どんなコンポーネントでも React がそれを識別するために使用できるのです。
まとめにも書いてありますが、同じコンポーネントが同じ位置でレンダーされている限り state は保持されるが、異なる key を与えることでサブツリーの state をリセットすることができます。
実際に使ってみる
key を使うことで state をリセットすることができるのが分かったので、実際に簡単な実装で試してみようと思います。
先程のドキュメントでは useState を使ったコンポーネントの state をリセットしていたので、もう少し実践的に React Hook Form と Recoil が保持している state をクリアしてみます。
こんな感じで、ボタンを押したらそれぞれのフォームの値(React Hook Form) と カウント(Recoil) をクリアするものを実装してみました。
環境
Vite の react-ts
テンプレートで作った素の React 環境で試しました。
"dependencies": {
"react": "^18.3.0-canary-b8e47d988-20231023",
"react-dom": "^18.3.0-canary-b8e47d988-20231023",
"react-hook-form": "^7.47.0",
"recoil": "^0.7.7"
},
"devDependencies": {
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14",
"@vitejs/plugin-react": "^4.1.0",
"typescript": "^5.2.2",
"vite": "^4.5.0"
}
本検証とは全く関係ないですが、 use が試せるようになっていることに気付いたので、 React はカナリアバージョンを指定しています。
既に型の定義もあって、 types に canary を指定することで TS 環境でも使用できました。
"types": ["react/canary", "react-dom/canary"],
実装
Context に key と key を リセットする関数を持たせました。
Recoil は RecoilRoot に key を指定しておくことで、 state をクリアできます。
key の生成には Date.now を使っていますが、重複しない値なら何でもよいです。
function App() {
const [appKey, setAppKey] = useState(Date.now())
return (
<AppContext.Provider
value={{
appKey,
clearApp: () => setAppKey(Date.now()),
}}
>
<RecoilRoot key={appKey}>
<Home />
</RecoilRoot>
</AppContext.Provider>
)
}
フォームのリセット用にそれぞれの Form コンポーネントに key を指定しています。
クリアボタンを押したときに clearApp が呼ばれるようにしています。
(地味ですが、 use 使ってます)
const Home: FC = () => {
const { appKey, clearApp } = use(AppContext)
return (
<Container>
<Layout>
<Form1 key={appKey} />
<Form2 key={appKey} />
<Counter />
</Layout>
<ButtonLayout>
<Button type="button" data-variant="danger" onClick={clearApp}>
Clear
</Button>
</ButtonLayout>
</Container>
)
}
フォーム側はこんな感じで、とくに変わったことはしていません。
const Component: FC = () => {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useFormContext<Schema>()
const onSubmit: SubmitHandler<Schema> = async () => {
await new Promise((resolve) => setTimeout(resolve, 300))
console.log('submitted!', data)
}
return (
<Container>
<form onSubmit={handleSubmit(onSubmit)}>
<FormGroup>
<Label htmlFor="text">Text</Label>
<TextInput id="text" type="text" {...register('text')} />
<ErrorMessage>{errors.text?.message}</ErrorMessage>
</FormGroup>
<ButtonLayout>
<Button type="submit" disabled={isSubmitting}>
Submit
</Button>
</ButtonLayout>
</form>
</Container>
)
}
export const Form1: FC = () => {
const methods = useForm<Schema>({
defaultValues,
mode: 'onChange',
resolver: valibotResolver(schema),
})
return (
<FormProvider {...methods}>
<Component />
</FormProvider>
)
}
カウンターは Recoil を使って値を保持しています。
export const Counter: FC = () => {
const [count, setCount] = useRecoilState(counterState)
const increment = () => setCount((prevCount) => prevCount + 1)
return (
<Container>
<Count>{count}</Count>
<ButtonLayout>
<Button type="button" onClick={increment}>
Increment
</Button>
</ButtonLayout>
</Container>
)
}
まとめ
今回は key を使って state をリセットする方法について考えました。
key を使わずに同じ挙動を実現する方法はありますし、実際には特定の値のみクリアしたい場合の方が多いので、こんな方法もあるんだなくらいに覚えておいて、綺麗にハマるケースで使えたらなと思います。
最後に
IVRyでは一緒に働いてくれるエンジニアを募集中です!
Discussion
legend-state x React Hook Formでトライしてみました
RHFにおけるエラーメッセージのクリアは確かにProviderからのkey更新が良さそうでした
こちらのほうのデモにチャレンジしてみました