🗂️

useState と localStorage で永続化する方法

2023/02/13に公開約4,900字

はじめに

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);
};

注意点があります。setValueuseCallbackの中でラップしないと、無限レンダリングループの問題が発生する場合があります。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 について
https://beta.reactjs.org/reference/react/useCallback

react-hooks/exhaustive-depsについて
https://github.com/facebook/react/issues/14920

データ、状態を取得する

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 の デモを実行した結果になります。

参考

https://react-typescript-cheatsheet.netlify.app/docs/basic/useful-hooks#uselocalstorage

最後に

基本的な localStorage の処理を書いていきましたが、もしも Web Storage API を使用する時は、ユーザが localStorage を使えるかどうか を確認するようにするのも良いかもしれません。

もし、間違った知識や内容がありましたら、ご指摘ください

GitHubで編集を提案

Discussion

ログインするとコメントできます