useState と localStorage で永続化する方法
はじめに
useState
は React の基本的な Hook API の一つではありますが、useState
では状態を永続的に保持することはできません。ユーザーがページをリロードすると、その状態は消えてしまいます。では、React でデータや状態を永続的に保持するにはどうすればいいのでしょうか。状態を永続化するカスタムフックを書けば解決できそうです。
想定読者
- React, localStorage の基礎知識を保有している方
- useState と localStorage で状態管理を知りたい方
- Recoil, Jotai など状態管理ライブラリの使用を控えたい方
結論
- 実際のコードはこちら
import { useCallback, useState } from "react";
type Props<T> = {
key: string;
initialValue: T;
};
type Result<T> = readonly [T, (v: T) => void];
export const usePersistState = <T>({ key, initialValue}: Props<T>): Result<T> => {
const getItemFromStorage = <T>(key: string, defaultValue?: T) => {
try {
const val = JSON.parse(localStorage.getItem(key) + "");
if (val !== null) {
return val;
}
return localStorage.setItem(key, JSON.stringify(defaultValue));
} catch {
return defaultValue;
}
};
const [state, setState] = useState<T>(getItemFromStorage<T>(key, initialValue));
const setValue = useCallback(
(value: T) => {
localStorage.setItem(key, JSON.stringify(value));
setState(value);
},
[key]
);
return [state, setValue] as const;
};
usePersistState
の使用例
// プリミティブの場合
const [value, setValue] = usePersistState<string>({
key: "key1",
initialValue: "Hello World"
});
// 配列の場合
const [fruits, setFruits] = usePersistState<string[]>({
key: "key2",
initialValue: ["apple", "banana", "orange"]
});
// オブジェクトの場合
const initialValue = {
name: "kenji",
content: "Hello, using usePersistState"
}
const [user, setUser] = usePersistState<typeof initialValue>({
key: "key3",
initialValue: initialValue
});
以降は実装の解説になります。
解説
React や TypeScript に慣れていない方が見ると、混乱してしまうかもしれません。
この usePersistState()
はデータ、状態を localStorage に保存し、必要なときにデータ、状態を渡します。
理解を深めるために順を追って解説していきます。
データ、状態を保存する
localStorage にデータ、状態を保存するために、key
をつけなければなりません。他にも useState
と同様に、initialValue
(初期値) を渡したい場合があります。useState
のときと同じように、保存するデータ、状態の型も渡したいです。これを実現するには、T
(ジェネリックス)を使いましょう。
type Props<T> = {
key: string;
initialValue: T;
}
export const usePersistStore = ({ key, initialValue }: Props<T>) => {
const setValue = (value: T) => {};
};
useState を追加
import { useState } from "react";
type Props<T> = {
key: string;
initialValue: T;
}
export const usePersistStore = ({ key, initialValue }: Props<T>) => {
const setValue = (value: T) => {};
const [state, setState] = useState<T>(initialValue);
}
データ、状態を保存するsetValue
を追加します。localStorage やuseState
にもデータ、状態を保存しておきましょう。そうすれば、usePersistState
の戻り値としてvalue
を渡すために localStorage からデータ、状態を取得する必要がなくなります。
const setValue = (value: T) => {
localStorage.setItem(name, JSON.stringify(value));
setState(value);
};
注意点があります。setValue
はuseCallback
の中でラップしないと、無限レンダリングループの問題が発生する場合があります。React はsetValue
が変更されるかどうかを知りません。useEffect
の内部でこの関数を使用する場合、依存関係の配列への追加を省略できるかもしれませんが、eslintのreact-hooks/exhaustive-deps
はエラーを表示させるでしょう。useCallback
でラップして、key
を変更しないことがわかっている場合でも、key
の dependencies(依存)を渡しています。
const setValue = useCallback(
(value: T) => {
localStorage.setItem(key, JSON.stringify(value));
setState(value);
},
[key]
);
useCallback について
react-hooks/exhaustive-deps
について
データ、状態を取得する
const getItemFromStorage = () => {
try {
const val = JSON.parse(localStorage.getItem(key) + "");
if (val !== null) {
return val;
}
return localStorage.setItem(key, JSON.stringify(initialValue));
} catch {
return initialValue;
}
};
基本的には、localStorage からデータ、状態を取得しようとしています。データ、状態が存在しない場合は、localStorage に保存します。このコードは、データが parse できない場合に備えて、try-catch ブロックの中でラップされています。
完成
引数や戻り値の型を追加して、React が useState で行っているように、value と set の関数を戻り値に渡します。
return [state, setValue] as const;
import { useCallback, useState } from "react";
type Props<T> = {
key: string;
initialValue: T;
};
type Result<T> = readonly [T, (v: T) => void];
export const usePersistState = <T>({ key, initialValue}: Props<T>): Result<T> => {
// 省略
const [state, setState] = useState<T>(getItemFromStorage<T>(key, initialValue));
// 省略
return [state, setValue] as const;
};
Develop Tool で正常に動作しているか、確認して完了です。
下記は、CodeSandbox の デモを実行した結果になります。
参考
最後に
基本的な localStorage の処理を書いていきましたが、もしも Web Storage API を使用する時は、ユーザが localStorage を使えるかどうか を確認するようにするのも良いかもしれません。
もし、間違った知識や内容がありましたら、ご指摘ください
Discussion