本文へスキップ
ブラウザに保存する

Part 9 · state を共有する

52 / 64

ブラウザに保存する

リロードしても、閉じても残る状態

前の章で、URL に状態を置きました。 ですが URL に出したくないものもあります。

ダークモードの設定、閉じたお知らせ、入力途中の下書き。その人にだけ残ってほしいものです。

そういうときは、ブラウザの保存領域を使います。

やはり形は同じ

// いつもの
const [name, setName] = useState("");

// 置き場所をブラウザの保存領域にしたもの
const [name, setName] = useLocalStorageState("key-name", { defaultValue: "" });

この Part で 3 つめですが、また同じ形です。 これは偶然ではありません。あとでまとめて扱います。

ブラウザに保存される入力

入力してからリロードしてみる

まだ入力されていません

入力してからページを再読み込みしてみてください

入力してからページを再読み込みしてください。入力が残っています。タブを閉じて開き直しても残ります。

local-storage.tsx

"use client";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import useLocalStorageState from "use-local-storage-state";

export function LocalStorageDemo() {
  // これも形は useState と同じ。置き場所がブラウザの保存領域になっただけ
  const [name, setName] = useLocalStorageState("react-lesson-demo-name", {
    defaultValue: "",
  });

  return (
    <div className="flex flex-col gap-4">
      <Input
        placeholder="名前を入力してみる"
        aria-label="名前を入力してみる"
        value={name}
        onChange={(event) => setName(event.target.value)}
      />

      <div className="rounded-md border p-3">
        {name === "" ? (
          <span className="text-muted-foreground">まだ入力されていません</span>
        ) : (
          <span>こんにちは、{name} さん</span>
        )}
      </div>

      <Button size="sm" variant="outline" onClick={() => setName("")}>
        消す
      </Button>

      <p className="text-muted-foreground">
        入力してからページを再読み込みしてみてください
      </p>
    </div>
  );
}

素で書くと、意外と面倒

ブラウザの保存領域は localStorage という名前で、 React がなくても使えます。ただ、React から使うと問題が出ます。

// 素朴に書くとこうなるが、これは動かない
const [name, setName] = useState(localStorage.getItem("name") ?? "");

面倒なのは次の 3 点です。

  • サーバー側では動かない Next.js は画面をサーバーでも組み立てます。そこに localStorage は存在しないのでエラーになります
  • 保存し忘れる 更新のたびに書き込む処理を自分で呼ぶ必要があります
  • 文字列しか入らない オブジェクトを入れるには毎回変換が要ります

デモで使っている use-local-storage-state は、 これを全部引き受けて useState と同じ形にしてくれます。

local-storage.tsx

"use client";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import useLocalStorageState from "use-local-storage-state";

export function LocalStorageDemo() {
  // これも形は useState と同じ。置き場所がブラウザの保存領域になっただけ
  const [name, setName] = useLocalStorageState("react-lesson-demo-name", {
    defaultValue: "",
  });

  return (
    <div className="flex flex-col gap-4">
      <Input
        placeholder="名前を入力してみる"
        aria-label="名前を入力してみる"
        value={name}
        onChange={(event) => setName(event.target.value)}
      />

      <div className="rounded-md border p-3">
        {name === "" ? (
          <span className="text-muted-foreground">まだ入力されていません</span>
        ) : (
          <span>こんにちは、{name} さん</span>
        )}
      </div>

      <Button size="sm" variant="outline" onClick={() => setName("")}>
        消す
      </Button>

      <p className="text-muted-foreground">
        入力してからページを再読み込みしてみてください
      </p>
    </div>
  );
}

入れてはいけないもの

そのほか、次のような制約があります。

  • 容量が小さい(おおよそ 5MB 程度)。 大量のデータは入りません
  • その端末のそのブラウザにしか残らない。 スマホで見たら別の状態です
  • ユーザーがいつでも消せる

local-storage.tsx

"use client";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import useLocalStorageState from "use-local-storage-state";

export function LocalStorageDemo() {
  // これも形は useState と同じ。置き場所がブラウザの保存領域になっただけ
  const [name, setName] = useLocalStorageState("react-lesson-demo-name", {
    defaultValue: "",
  });

  return (
    <div className="flex flex-col gap-4">
      <Input
        placeholder="名前を入力してみる"
        aria-label="名前を入力してみる"
        value={name}
        onChange={(event) => setName(event.target.value)}
      />

      <div className="rounded-md border p-3">
        {name === "" ? (
          <span className="text-muted-foreground">まだ入力されていません</span>
        ) : (
          <span>こんにちは、{name} さん</span>
        )}
      </div>

      <Button size="sm" variant="outline" onClick={() => setName("")}>
        消す
      </Button>

      <p className="text-muted-foreground">
        入力してからページを再読み込みしてみてください
      </p>
    </div>
  );
}

理解できたか確かめる

確認クイズ

ログイン用のトークンを localStorage に保存してよい?

確認クイズ

localStorage を useState の初期値に直接書くと何が起きる?

local-storage.tsx

"use client";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import useLocalStorageState from "use-local-storage-state";

export function LocalStorageDemo() {
  // これも形は useState と同じ。置き場所がブラウザの保存領域になっただけ
  const [name, setName] = useLocalStorageState("react-lesson-demo-name", {
    defaultValue: "",
  });

  return (
    <div className="flex flex-col gap-4">
      <Input
        placeholder="名前を入力してみる"
        aria-label="名前を入力してみる"
        value={name}
        onChange={(event) => setName(event.target.value)}
      />

      <div className="rounded-md border p-3">
        {name === "" ? (
          <span className="text-muted-foreground">まだ入力されていません</span>
        ) : (
          <span>こんにちは、{name} さん</span>
        )}
      </div>

      <Button size="sm" variant="outline" onClick={() => setName("")}>
        消す
      </Button>

      <p className="text-muted-foreground">
        入力してからページを再読み込みしてみてください
      </p>
    </div>
  );
}

この章のまとめ

  • ブラウザの保存領域に置くと、リロードしても、閉じても残る
  • 素で書くと、サーバー側で動かない・保存し忘れる・型変換が要る
  • 秘密の情報は入れない。開発者ツールから丸見え
  • 向いているのは消えても致命的でない、その人だけの設定

local-storage.tsx

"use client";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import useLocalStorageState from "use-local-storage-state";

export function LocalStorageDemo() {
  // これも形は useState と同じ。置き場所がブラウザの保存領域になっただけ
  const [name, setName] = useLocalStorageState("react-lesson-demo-name", {
    defaultValue: "",
  });

  return (
    <div className="flex flex-col gap-4">
      <Input
        placeholder="名前を入力してみる"
        aria-label="名前を入力してみる"
        value={name}
        onChange={(event) => setName(event.target.value)}
      />

      <div className="rounded-md border p-3">
        {name === "" ? (
          <span className="text-muted-foreground">まだ入力されていません</span>
        ) : (
          <span>こんにちは、{name} さん</span>
        )}
      </div>

      <Button size="sm" variant="outline" onClick={() => setName("")}>
        消す
      </Button>

      <p className="text-muted-foreground">
        入力してからページを再読み込みしてみてください
      </p>
    </div>
  );
}