Skip to content

Instantly share code, notes, and snippets.

@olibooty
Last active June 17, 2024 08:09
Show Gist options
  • Select an option

  • Save olibooty/c1e36fd51d4a0e6ad471985c6ce9ba06 to your computer and use it in GitHub Desktop.

Select an option

Save olibooty/c1e36fd51d4a0e6ad471985c6ce9ba06 to your computer and use it in GitHub Desktop.
import { renderHook } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { useLocalStorage } from './useLocalStorage';
describe('useLocalStorage', () => {
const key = 'test-key';
const value = 'test-value';
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
jest.clearAllMocks();
const keys = Object.keys(localStorage);
keys.forEach((key) => {
localStorage.removeItem(key);
});
});
it('should set the local storage state correctly', () => {
const { result } = renderHook(() => useLocalStorage(key));
const [state, setState] = result.current;
expect(state).toBeNull();
act(() => {
setState(value);
});
const [newState] = result.current;
expect(newState).toBe(value);
});
it('should remove the local storage state correctly', () => {
const { result } = renderHook(() => useLocalStorage(key));
const [state, setState, removeState] = result.current;
expect(state).toBeNull();
act(() => {
setState(value);
});
const [newState] = result.current;
expect(newState).toBe(value);
act(() => {
removeState();
});
const [removedState] = result.current;
expect(removedState).toBeNull();
});
it('persists storage between hooks', () => {
const { result: result1 } = renderHook(() => useLocalStorage(key));
const { result: result2 } = renderHook(() => useLocalStorage(key));
const [state1, setState1] = result1.current;
const [state2, setState2] = result2.current;
expect(state1).toBeNull();
expect(state2).toBeNull();
act(() => {
setState1(value);
});
const [newState1] = result1.current;
const [newState2] = result2.current;
expect(newState1).toBe(value);
expect(newState2).toBe(value);
act(() => {
setState2('new-value');
});
const [updatedState1] = result1.current;
const [updatedState2] = result2.current;
expect(updatedState1).toBe('new-value');
expect(updatedState2).toBe('new-value');
});
});
import { useSyncExternalStore } from 'react';
function setLocalStorageState<TValue>(key: string, value: TValue) {
const stringifiedValue = JSON.stringify(value);
window.localStorage.setItem(key, stringifiedValue);
window.dispatchEvent(
new StorageEvent('storage', { key, newValue: stringifiedValue }),
);
}
function removeLocalStorageState(key: string) {
window.localStorage.removeItem(key);
window.dispatchEvent(new StorageEvent('storage', { key }));
}
function subscribe(listener: (event: StorageEvent) => any) {
window.addEventListener('storage', listener);
return () => void window.removeEventListener('storage', listener);
}
function getSnapshot<TValue>(key: string): TValue | null {
const storedValue = window.localStorage.getItem(key);
if (storedValue) {
return JSON.parse(storedValue);
}
return null;
}
type UseLocalStorageReturn<T> = readonly [
T | null,
(value: T) => void,
() => void,
];
export const useLocalStorage = <T>(key: string): UseLocalStorageReturn<T> => {
const state = useSyncExternalStore<T | null>(subscribe, () =>
getSnapshot(key),
);
return [
state,
(value: T) => setLocalStorageState(key, value),
() => removeLocalStorageState(key),
] as const;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment