ReactRouter のページ遷移で値が切り替わるように Formik の入力値を localStorage に保存する
Formik のフォームの値を localStorage
に保存しておくライブラリとして formik-persist というものがありますが、ReactRouter でページ遷移した際に値がうまく切り替わらなかったので自前で Hook を組んで何とか実装してみます。
経緯としては以下の通りです。
- formik-persist を使ったけどページ遷移しても遷移前のフォームの入力値が残り続ける
- せっかくなので
localStorage
を使って自前で処理を書いてみよう - react-use の
useLocalStorage
を使ってみたけどkey
が変更されても値が切り替わらないので結局同じ問題が発生する - 好みの挙動になるように自前で
useLocalStorage
Hook を実装しよう (˘꒳˘) 意識高い
もしかしたら key
を適切に与えれば解消するような、react-router でよくある感じのものなのかもしれません。
動作するコードは以下のリポジトリに置いてあります。
useLocalStorage を定義する
useLocalStorage
のコードは https://usehooks.com/useLocalStorage/ を始め色々見つかりますので参考にしつつ書いていきます。
import { useState, useEffect, useCallback } from 'react' function getPersistedValue<V>(key: string, defaultValue: V) { const v = window.localStorage.getItem(key) if (!v) return defaultValue try { return JSON.parse(v) as V } catch (err) { console.warn(err) return defaultValue } } function persistValue<V>(key: string, value: V) { try { const v = JSON.stringify(value) window.localStorage.setItem(key, v) } catch (err) { console.warn(err) } } // Hook export default function useLocalStorage<V>(key: string, initialValue: V) { const [value, setValue] = useState(() => getPersistedValue(key, initialValue)) // `key` が変わったら値を再取得する (*˘꒳˘*) useEffect(() => { setValue(getPersistedValue(key, initialValue)) }, [key, setValue, initialValue]) // 値を更新しつつ `localStorage` に保存する (*˘꒳˘*) const setValuePersistently = useCallback((v: V) => { persistValue(key, v) setValue(v) }, [key, setValue]) // ちょっと色気を出してクリア用のメソッドも提供してみる (*˘꒳˘*) const clearValue = useCallback(() => { setValuePersistently(initialValue) }, [setValuePersistently, initialValue]) return [value, setValuePersistently, clearValue] as const }
今回の肝は key
の変更に合わせて対応する localStorage
の値で value
を更新することと、関連するハンドラも忘れずに更新されるようにしておくことです。
Formik と合わせる
Formik と組み合わせて以下の動作を実現します。
initialValues
が変更されたらフォームの内容を新しいinitialValues
でリセットするonChange
でフォームの値をlocalStorage
に保存するonSubmit
でフォームの値を初期化する- 送信後コメントフォームを空にするというケースを想定
interface FormValue { title: string body: string } const createInitialValue = () => ({ title: '', body: '' }) const FormView: React.FC<RouteComponentProps<{ id: string }>> = props => { const { id } = props.match.params const formKey = `FormView:${id}` const initialValues = useMemo(createInitialValue, []) const [values, persistValues, clearValues] = useLocalStorage( formKey, initialValues ) // `Formik` には `onChange` がないので `validate` で代用する (*˘꒳˘*) きたない const fakelyValidateToHandleChange = useCallback( (v: FormValue) => { persistValues(v) return Promise.resolve() }, [persistValues] ) return ( <Formik initialValues={values} onSubmit={clearValues} validate={fakelyValidateToHandleChange} validateOnChange enableReinitialize > {() => ( <Form> <Field name="title" component="input" /> <Field name="body" component="textarea" /> <button type="submit">Submit</button> </Form> )} </Formik> ) }
ポイントとしては以下の通りです。
enableReinitialize
を指定してinitialValues
の変更時にフォームの値を新しいinitialValues
の値でリセットする- ページ遷移時に
initialValues
が変わってフォームの内容が遷移後のページのものに更新されるように
- ページ遷移時に
validate
をonChange
として転用- 本当はこんなことやりたくないのですが...
残念なポイントとしては、上記のダーティハックはもちろんですが、せっかく Formik が State を管理してくれているのに、それをわざわざ親コンポーネントに引っ張り上げて二重管理をしてしまっている所です。
initialValues
が実質 Controlled Component の value
みたいな扱いになってしまっているので、ちょっと気持ち悪いです。
React でコンポーネントを組むときに、どうしようもない場合によく現れるパターンな気がしますが、スッキリしないですね。
とりあえず、一応動いたので今回は良しとします。気が向いたら formik-persist のコードも見ながら他にいい方法がないか調べてみます。
追記
以下のように Formik
と Persist
双方に key
を設定すればページ遷移しても、適切に入力値が保存されたものに切り替わるようになりました。
const FormView: React.FC<RouteComponentProps<{ id: string }>> = props => { const { id } = props.match.params const formKey = `FormView:${id}` const initialValues = useMemo(createInitialValue, []) return ( <Formik initialValues={initialValues} onSubmit={console.log} enableReinitialize key={formKey} > {() => ( <Form> <Field name="title" component="input" /> <Field name="body" component="textarea" /> <button type="submit">Submit</button> <Persist name={formKey} key={formKey} /> </Form> )} </Formik> ) }
これで一件落着したような気がします。react-persist は connect
を使ってこんな風に Formik
にアクセスできるんだなっていう良い例ですね。インスピレーションの幅が広がりそうです。