Recoilのstateをタブ間で共有する

はじめに

RecoilというReactの状態管理ライブラリが存在します。

自分はよく、/user/meから取得したユーザー情報をこのRecoilのStateに入れて保管しています。

しかしただログイン状態を入れるだけでは、複数タブで同じページを開いている場合に片方がログアウトしたという状態になっても片方はまだユーザーがRecoilに保持されていてログインしているように見えてしまうという問題があります。

なのでこの記事では、StateをBroadcastChannel APIを使用して同期してみようと思います。

Atom Effects

RecoilにはAtom Effectsという非常に便利な機能があります。これを使用することでLocal Storageと同期やはたまたサーバーと同期することが簡単にできます。

このEffectsでは、setSelfsetItemを使用することで値の取得・更新が可能です。BroadcastChannel APIを使用したEffectを作成すると以下のように作れます。

import {type AtomEffect, atom} from 'recoil';

const broadcastEffect =
  <T>(key: string): AtomEffect<T> =>
  ({setSelf, onSet}) => {
    const bc = new BroadcastChannel(key);
    bc.addEventListener('message', event => {
      const data: T = event.data;
      setSelf(data.value);
    });

    onSet(newValue => {
      bc.postMessage(newValue);
    });
  };

しかし、BroadcastChannel APIのpostMessageはすべてに対してブロードキャスト送信をします。そのため、このままだとpostMessageで「メッセージを送信→受信→送信」と無限ループになってしまいます。
これを防ぐために、タブごとに一意なIDを生成して取得したメッセージのIDが同じ(=自分が送信したもの)だったらStateを更新しないようにします。

import {type AtomEffect, atom} from 'recoil';

interface BroadcastMessage<T> {
  id: string;
  value: T;
}
const tabId = Math.random().toString(32).substring(2);
const broadcastEffect =
  <T>(key: string): AtomEffect<T> =>
  ({setSelf, onSet}) => {
    const bc = new BroadcastChannel(key);
    bc.addEventListener('message', event => {
      const data: BroadcastMessage<T> = event.data;
      if (data.id !== tabId) {
        setSelf(data.value);
      }
    });

    onSet(newValue => {
      bc.postMessage({
        id: tabId,
        value: newValue,
      } as BroadcastMessage<T>);
    });
  };

これで、タブ間共有が実現できます!ぜひ試してみてください。