【Next.js】localStorageの期限設定とリダイレクト処理の実装

# はじめに
Next.jsでログイン処理時にログインAPIを呼び出し、レスポンスのログイン情報を`localStorage`に保持するような処理を実装しました。
しかし、このままではログアウトをしない限り、ほぼ永続的にデータが保持されてしまいます。
そこで`localStorage`に保持したログイン情報に有効期限を設定し、期限切れの場合はログイン画面へリダイレクトする処理を実装しました。

# 前提
Next.jsプロジェクトを作成済み
ログインAPIをバックエンドで実装済み
`axios`インストール済み


# 環境

- React 17.0.1
- Next.js 10.2.3
- lscache 1.3.0
- tailwindcss 2.2.0

# 実装の流れ
1.プロジェクトに`lscache`をインストール
2.ログイン画面の実装
3.ログイン用のフックを作成
4.ホーム画面とAboutページの実装
5.`_app.jsx`の修正

## 1.プロジェクトに`lscache`をインストール

```terminal
$ yarn add lscache
```

## 2.ログイン画面の実装
Next.jsのファイルシステムルーティングの通り、下記パスに`login.jsx`ファイルを作成。
ログイン画面を実装します。

```react:src/pages/login.jsx
import Head from "next/head";
import { useState } from "react";
import { useAuth } from "src/hooks/useAuth";

const Login = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const { login } = useAuth();

  const onChangeEmail = (e) => {
    setEmail(e.target.value);
  };

  const onChangePassword = (e) => {
    setPassword(e.target.value);
  };

  const onClickLogin = () => {
    login(email, password);
  };

  return (
    <div className='bg-gray-500 flex flex-col items-center justify-center min-h-screen py-2'>
      <Head>
        <title>ログイン画面</title>
        <link rel='icon' href='/favicon.ico' />
      </Head>
      <main className='flex flex-col items-center justify-center w-full flex-1 px-20 text-center'>
        <div className='container mx-auto h-full flex flex-1 justify-center items-center'>
          <div className='w-full max-w-md'>
            <div className='leading-loose'>
              <div className='max-w-md m-4 p-10 bg-white bg-opacity-25 rounded shadow-xl'>
                <input
                  className='w-full px-5 py-1 text-gray-700 bg-gray-300 rounded focus:outline-none focus:bg-white'
                  type='email'
                  placeholder='メールアドレス'
                  required
                  value={email}
                  onChange={onChangeEmail}
                />
                <div className='mt-2'>
                  <input
                    className='w-full px-5 py-1 text-gray-700 bg-gray-300 rounded focus:outline-none focus:bg-white'
                    type='password'
                    placeholder='パスワード'
                    required
                    value={password}
                    onChange={onChangePassword}
                  />
                </div>
                <div className='mt-4 items-center flex justify-between'>
                  <button
                    className='px-4 py-1 text-white font-light tracking-wider bg-gray-900 hover:bg-gray-800 rounded'
                    onClick={onClickLogin}
                  >
                    ログイン
                  </button>
                </div>
              </div>
            </div>
          </div>
        </div>
      </main>
    </div>
  );
};

export default Login;


```
ログインボタンを押下時に後述のログイン用のフックを呼び出して、ログイン処理をしています。


## 3.ログイン用のフックを作成
`axios`でログインAPIを呼び出し、ログイン処理を行うフックを下記の通り実装します。
ここで`localStorage`に期限付きでログイン情報を保持します。

```react:src/hooks/useAuth.js
import axios from "axios";
import { useRouter } from "next/router";
import lscache from "lscache";

export const useAuth = () => {
  const router = useRouter();

  const login = (email, password) => {
    axios
      // ご自身のログインAPIのURLを設定
      .post(`http://localhost:3001/auth/sign_in`, {
        email: email,
        password: password,
      })
      .then((res) => {
        if (res.data) {
          // ポイント① lscacheを用いてlocalStorageに期限付きでログインIDを保持
          lscache.set("loginId", JSON.stringify(resData.id), 1);
          router.push("/");
        } else {
          console.log("error");
        }
      })
      .catch(() => {
        console.log("catch");
      })
      .finally(() => {
        console.log("finally");
      });
  };
  return { login };
};


```


ポイント①箇所で期限設定とログイン情報の保持を行い、ホーム画面へ遷移させています。
第１引数、第２引数でレスポンスデータのIDを`loginId`に保存し、第３引数で有効期限を設定しています。
上記のように「1」と設定すると１分間の有効期限が設定されます。


## 4.ホーム画面とAboutページの実装
動作確認しやすいように相互に行き来できるリンクを含めて実装します。

```react:src/pages/index.jsx

import Head from "next/head";
import Link from "next/link";

const Home = () => {
  return (
    <div>
      <Head>
        <title>ホーム</title>
        <link rel='icon' href='/favicon.ico' />
      </Head>

      <div className='content'>
        <div className='flex items-center justify-between w-full my-4 pl-4 sm:pr-4'>
          <div className='mr-6'>
            <h2 className='text-3xl md:text-4xl font-semibold tracking-tight leading-7 md:leading-10 mb-1 truncate'>
              ホーム画面
            </h2>
            <div className='text-teal-600 '>
              <Link href='/about'>
                <a>Aboutページへ</a>
              </Link>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default Home;
```


```react:src/pages/about.jsx
import Head from "next/head";
import Link from "next/link";

const About = () => {
  return (
    <div>
      <Head>
        <title>About</title>
        <link rel='icon' href='/favicon.ico' />
      </Head>

      <div className='content'>
        <div className='flex items-center justify-between w-full my-4 pl-4 sm:pr-4'>
          <div className='mr-6'>
            <h2 className='text-3xl md:text-4xl font-semibold tracking-tight leading-7 md:leading-10 mb-1 truncate'>
              Aboutページ
            </h2>
            <div className='text-teal-600 '>
              <Link href='/'>
                <a>HOMEへ</a>
              </Link>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default About;

```


## 5.`_app.jsx`の修正
`_app.jsx`ではログイン時に保持した`loginId`の有効期限が切れていた場合に削除処理を行い、ログイン画面へリダイレクトする処理を実装しています。
`useEffect`を用いて、画面遷移や画面リロードの際に上記の処理が動作するように実装しています。

```react:src/pages/_app.jsx
import "tailwindcss/tailwind.css";
import { useEffect } from "react";
import lscache from "lscache";

const MyApp = ({ Component, pageProps, router }) => {
  // ポイント② useEffectを使用
  useEffect(() => {
    // ポイント③ ログインページへのアクセスの場合は処理をしない
    if (router.pathname === "/login") return;
    
    // ポイント④ 期限切れのデータを削除
    lscache.flushExpired();

    // ポイント⑤ loginIdが取得できない場合、ログインページへリダイレクト
    if (!lscache.get("loginId")) {
      router.push("/login");
    }
  }, [Component]);
  return (
    <>
      <Component {...pageProps} />
    </>
  );
};

export default MyApp;

```
ポイント②で`useEffect`の第２引数に`Component`を設定して、画面遷移や画面リロードなどの際に処理が走るようにしています。
ポイント③でログインページへのアクセスの際は処理を行わないようにしています。
ポイント④で有効期限の切れたストレージのデータを削除しています。
ポイント⑤`loginId`のデータが取得できなかった場合、ログインページにリダイレクトするように実装しています。

# 動作確認
ログイン情報の状態をChromeのデベロッパーツールで確認しながら動作確認をしていきます。

## 1.next devでサーバを起動。

```
next dev
```

## 2.ログイン画面にアクセス
Chromeで 「http://localhost:3000/login」 にアクセスします。
デベロッパーツールを開き、「Application」パネルのLocal Storageの中身を表示します。
現在は空っぽの状態です。


![スクリーンショット 2021-06-30 16.57.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1433539/f740dc75-ed22-7499-b66c-83b95ef5212c.png)


## 3.ログインと画面遷移
ログインすると画像のように`lscache-loginId`にデータが保持されています。
ホーム画面とAboutページを行き来しても`lscache-loginId`は削除されず、画面遷移ができています。

![スクリーンショット 2021-06-30 16.50.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1433539/97a1f8ad-38e4-5749-0ff2-8aa6b82bb3e3.png)


![スクリーンショット 2021-06-30 16.50.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1433539/a3ff0236-28f2-6407-640e-acf5fae2b283.png)


## 4.１分後に画面遷移
１分後、画面遷移をするとログイン画面にリダイレクトし、
`lscache-loginId`が削除されていることが確認できます。


![スクリーンショット 2021-06-30 16.51.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1433539/a72e9746-81da-c50a-8aa2-76e4d98ad068.png)

以上で動作確認完了です。


# 最後に
`localStorage`に期限を設定し、期限が切れている場合はログイン画面にリダイレクトする機能を実装していきました。
認証周りは将来的にはライブラリや認証サービスを使用するように修正するかもしれませんが、上記の技術での実装方法を記録として残します。
不備等ございましたら、ご指摘とご教授のほどお願いいたします。
ありがとうございました。


# 参考文献
- [ローカルストレージを有効期限付きのキャッシュとして使えるJSライブラリ「lscache」](https://co.bsnws.net/article/234)
- [lscache](https://github.com/pamelafox/lscache)
- [Next.jsでページ共通の処理をする（useEffectを使う例）](https://zenn.dev/catnose99/articles/2169dae14b58b6)