@redocly/theme
Version:
Shared UI components lib
96 lines (79 loc) • 2.19 kB
text/typescript
import { useSyncExternalStore } from 'react';
import { isBrowser } from '../utils/js-utils';
type Store<T> = {
getValue: (defaultValue: T) => T;
setValue: (next: T) => void;
subscribe: (callback: () => void) => Unsubscribe;
};
type Unsubscribe = () => void;
type CreateStoreProps<T> = {
storageKey: string;
storageType?: 'localStorage' | 'sessionStorage';
serializer?: {
parse: (value: string) => T;
serialize: (value: T) => string;
};
};
export function createStore<T>({
storageKey,
storageType = 'localStorage',
serializer = {
parse: (value: string) => JSON.parse(value) as T,
serialize: (value: T) => JSON.stringify(value),
},
}: CreateStoreProps<T>): Store<T> {
const subscribers = new Set<() => void>();
let cachedValue: T;
const shouldSerialize = (value: unknown) => typeof value !== 'string';
const getValue = (defaultValue: T): T => {
if (!isBrowser()) return defaultValue;
if (cachedValue !== undefined) return cachedValue;
const value = window[storageType].getItem(storageKey);
if (!value) {
cachedValue = defaultValue;
return cachedValue;
}
if (!shouldSerialize(defaultValue)) {
cachedValue = value as T;
return cachedValue;
}
try {
cachedValue = JSON.parse(value) as T;
} catch {
cachedValue = defaultValue;
}
return cachedValue;
};
const setValue = (next: T): void => {
if (!isBrowser()) return;
try {
window[storageType].setItem(
storageKey,
shouldSerialize(next) ? serializer.serialize(next) : (next as string),
);
cachedValue = next;
subscribers.forEach((callback) => callback());
} catch {
return;
}
};
const subscribe = (callback: () => void): Unsubscribe => {
subscribers.add(callback);
return () => {
subscribers.delete(callback);
};
};
return {
getValue,
setValue,
subscribe,
};
}
export function useStore<T>(store: Store<T>, defaultValue: T) {
const value = useSyncExternalStore(
store.subscribe,
() => store.getValue(defaultValue),
() => defaultValue,
);
return [value, store.setValue] as const;
}