Next.jsでStorageオブジェクトを使う
Next.jsでブラウザの Storage オブジェクト を使う時の対応策をまとめています。
参考実装は t-yng/examples/nextjs-localstorage を参照してください。
Storage オブジェクトを使ってみる
sessionStorage
からユーザー情報を取得して画面に表示する例を考えてみます。
// hooks/useUser.ts import { useState } from "react"; import { User } from "../interfaces"; const readUser = (): User | undefined => { const user = sessionStorage.getItem("user"); return user != null ? JSON.parse(user) : undefined; }; export const useUser = () => { const [user] = useState<User | undefined>(readUser()); return { user, }; };
// pages/index.tsx import { useUser } from "../hooks/useUser"; const IndexPage = () => { const { user } = useUser(); return ( <> <h1>ユーザー情報</h1> {user == null ? ( <p>ユーザー情報が存在しません</p> ) : ( <> <p>ID: {user.id} </p> <p>ユーザー名: {user.name} </p> </> )} </> ); }; export default IndexPage;
このコードをビルドすると、undefined sessionStorage
のエラーが発生します。Next.jsはSSRが不要なページはビルド時にSSGにより Rre Rendering されます。これは、Node.js 上で実行されますが、Node.js には sessionStorage
が存在しないため下記のエラーが発生するのです。
$ yarn build Error occurred prerendering page "/". Read more: https://err.sh/next.js/prerender-error ReferenceError: sessionStorage is not defined ...
useEffectで実行する
この問題は useEffect()
内で非同期でユーザー情報を取得する事で解決できます。useEffect()
の実行は ブラウザ上でのみ限定され、SSRやSSGのタイミングでは実行されません。実行が非同期になるので、新しく loading
を状態として追加します。
// hooks/useUser.ts export const useUser = () => { const [user] = useState<User | undefined>(readUser()); const [user, setUser] = useState<User | undefined>(); const [loading, setLoading] = useState(true); useEffect(() => { setUser(readUser()); setLoading(false); }, []); return { user, loading, }; };
// pages/index.tsx const IndexPage = () => { const { user, loading } = useUser(); if (loading) { return <h1>読み込み中...</h1>; } return ( <> <h1>ユーザー情報</h1> {user == null ? ( <p>ユーザー情報が存在しません</p> ) : ( <> <p>ID: {user.id} </p> <p>ユーザー名: {user.name} </p> </> )} </> ); };
上記の対応で sessionStorage
を使えるようになりましたが、一つだけ問題が発生しました。せっかく sessionStorage
にデータをキャッシュしているのに、useEffect()
を利用する関係上、非同期で読み込む必要があるので、例えばページ遷移の度に「読み込み中...」が表示されてしまいます。
※ デモは分かりやすくするために意図的に表示を遅らせています。
Dynamic import
Next.js は Dynamic import で読み込むことで、サーバーサイドでモジュールを読み込まないようにする事ができます。これにより、フロントでのみ sessitionStorage
の参照します。
// pages/user.tsx import Link from "next/link"; import dynamic from "next/dynamic"; import { useUser } from "../hooks/useUser"; const UserPage = () => { const { user } = useUser(); return ( <> <h1>ユーザー情報</h1> {user == null ? ( <p>ユーザー情報が存在しません</p> ) : ( <> <p>ID: {user.id} </p> <p>ユーザー名: {user.name} </p> </> )} <p> <Link href="/">ホーム</Link> </p> </> ); }; const DynamicUserPage = dynamic( { loader: async () => UserPage, }, { ssr: false } ); export default DynamicUserPage;
// hooks/useUser.ts import { useState } from "react"; import { User } from "../interfaces"; const readUser = (): User | undefined => { const user = sessionStorage.getItem("user"); return user != null ? JSON.parse(user) : undefined; }; export const useUser = () => { const [user] = useState<User | undefined>(readUser()); return { user, }; };
サーバーサイドを意識する必要がなくなったので、useEffect()
で非同期に読み込む必要がなくなり、「読み込み中...」を表示する必要もなくなりました。