かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JavaScript ABテスト localStorage 期限付きの値を保存したい

AB テストを行うための状態を保存したい。AB テストなので一定時間経過したら A / B の状態をリセットしたい。
同じブラウザなら一定時間同じ状態を表示させたいくらいの要件だったので手っ取り早く localStorage に値を保存したい。期限付きで。

localStorage に期限のオプションはない

localStorage に Cookie や JWT みたいな有効期限のオプションは存在しない。
ABテスト用の state と一緒に有効期限を保存してしまえばよい

const LOCALSTORAGE_KEY = 'keyOfLocalStorage' as const;
const EXPIRED_TIME = 1000 * 60 * 60 * 12; // 12h

type ABTestType = 'A' | 'B';

interface StorageValue {
  state: ABTestType;
  timestamp: number;
  expiresIn: number;
}

export const setLocalStorage = (state: ABTestType) => {
  const timestamp = Date.now();
  const saveData: StorageValue = {
    state,
    timestamp,
    expiresIn: timestamp + EXPIRED_TIME;
  };
 
  localStorage.setItem(
    LOCALSTORAGE_KEY,
    JSON.stringify(saveData)
  );
}

LocalStorage に値が存在しない 又は 値が期限切れの時、新しく localStorage に保存すればOK

localStorage からデータを取得する関数

export const getLocalStorage = (): {
  state?: ABTestType;
  isExpired?: boolean;
} => {
  const data = getLocalStorageValue();
  if (!data) {
    return { state: undefined, isExpired: undefined };
  }

  const { state, expiresIn } = data;
  const isExpired = Date.now() > expiresIn;
  return { state, isExpired };  
}

const getLocalStorageValue = () => {
  const data = localStorage.getItem(LOCALSTORAGE_KEY);
  // localStorage にデータが存在しない
  if (data === null) { return undefined; }

  try {
    return JSON.parse(data) as StorageValue; // ※ざっくり実装なので as で型を指定してしまう    
  } catch (error) {
    // localStorage に保存されているデータの形式が不正
     return undefined;
  }
}

localStorage に AB テスト用の data が存在しない 又は 期限切れの場合は新しく data を保存する

import { FC, useEffect, useState } from "react";
impoet { getLocalStorage, setLocalStorage } from './storage';

const App: FC = () => {
  const { state, isExpired } = getLocalStorage();
  const [ABState, setABState] = useState<undefined | ABTestType>(state);

  useEffect(() => {
    if (isExpired === false) {
      return;
    }
    
    const newState = (['A', 'B'] as const)[Math.floor(Math.random() * 2)];
    setLocalStorage(newState);
    setABState(newState);
  }, [isExpired]);

  return (
    <div>
      {/* ABState が undefined の時は `A` として扱う */}
      { ABState === 'B' ? <ComponentB /> : <ComponentA /> }
    </div>
  );
};

Sample

なんとなくいい感じに動いてるからヨシ!
ここまでやっておいて何だけど、有効期限付きなら Cookie の方が手っ取り早かったかもしれない…

おわり ₍ ᐢ•ᴗ•ᐢ ₎


[参考]