🪝
webStorage.hook をつくってみた
ReactでもwebStorageを積極的に使いたいのですが、ちょっと面倒です。いちいち取得したりセットしたり、useEffectでrenderを起こしたり…
いろんなサイトを参考しながら、hookを作成してみました🙏
こちらが作ってみたコードです
webStorage.hook.ts
import { SetStateAction, useSyncExternalStore } from 'react';
import { LocalStorageKeyValues, WebStorageType } from './types';
const selectWebStorage = (storageType?: WebStorageType) => {
switch (storageType) {
case 'session':
return window.sessionStorage;
case 'local':
default:
return window.localStorage;
}
};
const subscribe = (callback: () => void) => {
window.addEventListener('storage', callback);
return () => window.removeEventListener('storage', callback);
};
export const useSyncWebStorage = <K extends keyof LocalStorageKeyValues>(
key: K,
initialValue: LocalStorageKeyValues[K],
storageType?: WebStorageType,
): [
LocalStorageKeyValues[K],
(value: SetStateAction<LocalStorageKeyValues[K]>) => void,
] => {
const storage = selectWebStorage(storageType);
const getSnapshot = () => {
return storage.getItem(key);
};
const localStorageValue = useSyncExternalStore(subscribe, getSnapshot);
const parsedValue = localStorageValue
? (JSON.parse(localStorageValue) as LocalStorageKeyValues[K])
: initialValue;
const setValue = (action: SetStateAction<LocalStorageKeyValues[K]>) => {
try {
const value = action instanceof Function ? action(parsedValue) : action;
storage.setItem(key, JSON.stringify(value));
window.dispatchEvent(
new StorageEvent('storage', { key, newValue: JSON.stringify(value) }),
);
} catch (error) {
console.error(error);
}
};
return [parsedValue, setValue];
};
types.ts
import { Team } from 'services/teams/types';
export type WebStorageType = 'local' | 'session';
export type LocalStorageKeyValues = {
foo: Record<bar, boolean>;
};
工夫したポイント
(参考URL)
keyに対して対応したvalueの型を推論させたここの部分
export const useSyncWebStorage = <K extends keyof LocalStorageKeyValues>(
key: K,
initialValue: LocalStorageKeyValues[K],
storageType?: WebStorageType,
): [
(参考URL)
useStateと似たようなインターフェースにしたここの部分
const setValue = (action: SetStateAction<LocalStorageKeyValues[K]>) => {
try {
const value = action instanceof Function ? action(parsedValue) : action;
useState
の setHoge((prev) => [...prev, hoge])
みたいなことがしたかった
useSyncExternalStoreを使って、webStorageの変更とレンダリングを同期させた
react@18から登場したhookです
sessionもlocalもインターフェースがほぼ同じなので選べるようにした
※ただし、将来的に仕様が変えればhookごと分けます
const selectWebStorage = (storageType?: WebStorageType) => {
switch (storageType) {
case 'session':
return window.sessionStorage;
case 'local':
default:
return window.localStorage;
}
};
ハマったこと
変更を行なったのと同じページでは、storage イベントは発火しない
メモ: これは変更を行ったのと同じページでは動作しません。本来、これは同じ保存領域を使用している同じドメインの他のページが更新を同期するための仕組みです。他のドメインのページは、同じ保存領域オブジェクトにはアクセスできません。
https://developer.mozilla.org/ja/docs/Web/API/Window/storage_event
こちらの記事、コードが参考になりました🙏
useSyncExternalStoreが無限ループする
reactのドキュメント、トラブルシューティングに載ってます。
describe
hook内部にあると毎度呼び出されます。外部に定義するか useCallbabck
の利用を検討しましょう。
getSnapshot
変更がない時は常に同じ値を返さないと無限ループします。今回の場合、 getSnapshot
内で JSON.parse
すると object
になる可能性があります。こちらは、内部的にはObject.isで比較されるため、中身が同じでも参照が違えば false
になります。そのため、parseをせずに getSnapshot
を実行し、そのあと工程でparseを行います。
参考リンク集
- https://stackoverflow.com/questions/49285864/is-there-a-valueof-similar-to-keyof-in-typescript
- https://usehooks-ts.com/react-hook/use-local-storage
- https://ja.react.dev/reference/react/useSyncExternalStore
- https://developer.mozilla.org/ja/docs/Web/API/Window/storage_event
- https://oakhtar147.medium.com/sync-local-storage-state-across-tabs-in-react-using-usesyncexternalstore-613d2c22819e
- https://ja.react.dev/reference/react/useSyncExternalStore#my-subscribe-function-gets-called-after-every-re-render
- https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/is
- https://ja.react.dev/reference/react/useSyncExternalStore#im-getting-an-error-the-result-of-getsnapshot-should-be-cached
Discussion