Next.jsとRecoilでブラウザがリロードされてもStateを保持する

2020/12/05に公開2

概要

Reactの状態管理ライブラリについて、Reactの状態管理はRecoilがトップに! Reduxは2位に転落!!の記事によると、トレンド上ではRecoilがReduxを追い抜いたそうです。私も手軽に導入できるRecoilが好きで、ちょくちょく触っているのですが、永続化の部分で少し詰まった部分があったので今回書いてみたいと思います。

グローバルStateの永続化について

例えばブラウザをリロードしたりする場合などで、Stateをどこかに永続化しないと消えてしまいます。もちろん、画面の入力状態などは保持する必要はないのですが、Recoilで管理するグローバルStateの情報(ログイン情報など)は保持しておきたいですよね。2020年11月時点ではまだ開発中とのことですが、RecoilではStateを永続化するAPIが用意されています。
Recoil公式ドキュメント 翻訳⑩ ガイド-Stateの永続性にて翻訳されたドキュメントがのせられています。

Next.jsではどうしたら良いか

では、Next.jsでRecoilを使った場合にどう永続化すれば良いか。まず思いつくのはsessionStorageかなと思いますが、Next.jsの場合SSRを想定しているので、useEffect内もしくはページをDynamic importにしないとstorageにアクセスできないという制約があります。Next.jsでStorageオブジェクトを使うの記事にて紹介されています。
今回はグローバルStateに関わる部分で、_app.jsで実装したいと思ってるのでsessionStorageを使うのは少ししんどそうです。というわけでNext.jsではCookieを使ったほうがベターな選択となりそうです。ただ、Cookieを使うにしてもレンダリングをSSRでやってるかクライアントサイドでやっているかで、取得できる値も変わってきてしまいます。というわけで、そんな時に役立つライブラリがncookiesNext.jsでcookieをシンプルに扱うことができるライブラリ nookies を紹介の記事で紹介されています。
Cookieを使ったStateの永続化サンプルを以下の項で紹介します。

実装サンプル

グローバルStateはログインのユーザ情報のみという前提です。

_app.js
import React from "react";
import App, { Container } from "next/app";
import { RecoilRoot } from "recoil";
import "bootstrap/dist/css/bootstrap.min.css";
import { parseCookies } from "nookies";
import { PersistenceObserver } from "./persistenceObserver";

export default class MyApp extends App {
  static async getInitialProps({ Component, router, ctx }) {
    let pageProps = {};

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }

    return { pageProps };
  }

  render() {
    const { Component, pageProps, ctx } = this.props;

    const initializeState = ({ set }) => {
      const cookie = parseCookies(ctx);
      if (cookie?.user) {
        const user = JSON.parse(cookie.user);
        if (user) {
          set({ key: "loginUser" }, user);
        }
      }
    };

    return (
      <Container>
        <RecoilRoot initializeState={initializeState}>
          <Component {...pageProps} />
          <PersistenceObserver ctx={ctx} />
        </RecoilRoot>
      </Container>
    );
  }
}
persistenceObserver.js
import { useRecoilTransactionObserver_UNSTABLE } from "recoil";
import { setCookie } from "nookies";

export function PersistenceObserver(prop) {
  useRecoilTransactionObserver_UNSTABLE(({ snapshot }) => {
    for (const modifiedAtom of snapshot.getNodes_UNSTABLE({
      isModified: true,
    })) {
      const atomLoadable = snapshot.getLoadable(modifiedAtom);
      if (atomLoadable.state === "hasValue") {
        setCookie(prop.ctx, "user", JSON.stringify(atomLoadable.contents), {
          path: "/",
        });
      }
    }
  });
  return <></>;
}

Discussion

catnosecatnose

ちょうど「なんとかできないかなー」と気になっていた部分だったので、とても参考になりました。

アプリによると思いますが「いつリフレッシュするか」みたいなところが課題になるんですかね(複数のブラウザでログインしていた場合にそれぞれで最新のステートを同期する等)。

とはいえ、グローバルステートをCookieに保持して表示速度を上げつつ、バックグランドで最新のデータに同期(revalidate)する形であれば、安全にユーザー体験を向上させられそうです。僕も時間ができたときに試してみようと思います。ありがとうございます。

なかつがわなかつがわ

コメントありがとうございます。ご参考になり良かったです。
そうですね、、複数ブラウザでの同期はバックエンドと合わせて仕組み考える必要がありそう。。