💾

Next.jsのページ遷移などでのstateのリセット防止を考える

2022/12/03に公開

この記事は、React Advent Calendar 2022 3日目の記事です。

この記事について

Next.jsではページ遷移するとpages配下で定義したstateはリセットされてしまいます。

ユーザが編集中の情報が破棄されないようにしたい場合などでこの挙動が不都合になるケースがあります。

こちらの記事ではNext.jsのページ遷移を中心に、stateのリセットを回避したいケースについて考察します。

stateを一時保存・復元する

一番手っ取り早い方法は、 state をローカルストレージに一時保存して、初期化時に復元する方法かと思います。

▼書き込み

page.tsx
// stateが更新されたらローカルストレージに保存する
useEffect(() => {
  localStorage.setItem('foo', JSON.stringify(state));
}, [state]);

stateが更新されたらローカルストレージに保存します。これにより、他のコードに手を加えることなく状態をlocal storageに保存することができます。

▼読み込み

page.tsx
// 初期化時にローカルストレージから復元する
const [state, setState] = useState<State>(localStorage.getItem('foo'));

local storage 以外の保存先を考える

localStorageの課題は以下のようなものが想定されます。

  • シリアライズ可能な値しか保存できない
  • 5MB制限(画像をBase64で保存する運用には厳しい)
  • デバイス間で共有できない

要件次第では、localStorage以外の保存先を検討する必要があることも考えられます。
データベース, ファイル(ネイティブアプリの場合), IndexedDB, etc..

個々の実装についてはこの記事では触れませんが、読み書きに非同期処理が必要な場合は、以下のような書き方になります。

▼読み込み

page.tsx
const [state, setState] = useState<State>(undefined)

useEffect(() => {
  (async () => {
    const savedState = await load('foo') as State; // loadは何かしらの状態を保存する関数
    setState(savedState);
  })();
}, []);

▼書き込み

page.tsx
useEffect(() => {
  (async () => {
    await save('foo', JSON.stringify(state)); // saveは何かしらの状態を保存する関数
  })();
}, [state]);

「ページ遷移でstateがリセットされるのを防ぐ」の別案

ここまで、ページ遷移でstateがリセットされるのを防ぐため、ストレージに永続化して読み書きする、ということを考えていました。

ページ遷移でstateがリセットされるのを防ぐには、他にも方法が考えられますので、以下に述べていきます。

  • _app.tsxでstateを定義する
  • 未保存での遷移をブロックする

_app.tsxでstateを定義する

page componentのより上位の_app.tsxでstateを定義することで、遷移してもstateがリセットされなくなります。

_app.tsx
const [state, setState] = useState<State>(initialState);

return (
  ...
    <Component key={router.route} {...pageProps} state={state} />
  ...
)

stateのライフサイクルが長くなるため、stateの管理が難しくなる傾向があります。逆に、ページ遷移でstateをリセットしたい場合は自前でリセットする必要があります。

また、リロード時にはstateがリセットされるので、別途考慮が必要となります。

stateを永続化するのと組み合わせて利用することも考えられます。例えば、stateの読み書きに時間が掛かる場合(インターネットを経由する場合など)は、stateを大きめに取っておくことで読み込みの頻度が減るのでパフォーマンス改善が期待できます。

未保存での遷移をブロックする

以下のように、 routeChangeStart イベントでthrowすると、遷移イベントが打ち切られます。

pages.tsx
import Router, {useRouter} from 'next/router';

const router = useRouter();
React.useEffect(() => {
  router.events.on('routeChangeStart', () => {
    if (unSaved) { // unSavedは未保存であることを表すフラグ
      // 警告を表示するなどの処理はここに記述する
      router.events.emit('routeChangeError');
      throw 'routeChange aborted by user.';
    }
  });
}, [unSaved]);

下記のdiscussionで様々なケースでの実装が案内されています(リロードやページクローズをブロックする方法など)。

https://github.com/vercel/next.js/discussions/32231

まとめ

この記事では、Next.jsでページ遷移する例を中心に、React stateが破棄されることを防止する方法をいくつか検討しました。

  • stateを一時保存・復元する
  • _app.tsxでstateを定義する
  • ページ遷移を防止する

要件によって最適な実装は変わってくるかと思うので、方法を知った上で実装できると良さそうです。

Discussion