useWebStorage
The useWebStorage
hook provides a way to synchronize state with the browser’s localStorage
or sessionStorage
. It allows you to easily get, set, and remove items from storage while keeping the state in sync across different tabs or windows.
Add the utility
npx @ivnatsr/ezreact add use-web-storage
import { useSyncExternalStore, useEffect, useCallback, useRef } from 'react'
const subscribe = (callback) => { const listener = (event) => { callback(event.newValue) } window.addEventListener('storage', listener)
return () => { window.removeEventListener('storage', listener) }}
const getStorageValue = ({ storageKey, storage }) => { return window[storage].getItem(storageKey)}
const setValueToStorage = ({ storageKey, storage, value }) => { const stringyfiedValue = JSON.stringify(value) window[storage].setItem(storageKey, stringyfiedValue)
window.dispatchEvent( new StorageEvent('storage', { key: storageKey, newValue: stringyfiedValue }) )}
const removeValueFromStorage = ({ storageKey, storage }) => { window[storage].removeItem(storageKey) window.dispatchEvent(new StorageEvent('storage', { key: storageKey }))}
const valueIsFunction = (value) => typeof value === 'function'
export function useWebStorage({ storageKey, initialState, storage = 'localStorage' }) { const store = useSyncExternalStore( subscribe, () => getStorageValue({ storageKey, storage }), () => JSON.stringify(initialState ?? null) ) const storeRef = useRef(store) const initialStateRef = useRef(initialState) const isFirstRender = useRef(true)
useEffect(() => { if ( getStorageValue({ storageKey, storage }) === null && initialStateRef.current !== undefined && isFirstRender.current ) { setValueToStorage({ storageKey, storage, value: initialStateRef.current }) } isFirstRender.current = false }, [storageKey, storage])
useEffect(() => { storeRef.current = store }, [store])
const setItemValue = useCallback( (value) => { if (value == null) { throw new TypeError( `Cannot set the following storage key's value to null or undefined: ${storageKey}. If you want to remove the item from storage, use the removeItem method instead` ) }
let newValue = null
if (valueIsFunction(value)) { if (storeRef.current === null && initialStateRef.current === undefined) { throw new TypeError( `Cannot call ${value.name} as the store's value is null and no initial state was provided` ) } newValue = storeRef.current !== null ? value(JSON.parse(storeRef.current)) : value(initialStateRef.current) } else { newValue = value }
setValueToStorage({ storageKey, storage, value: newValue }) }, [storage, storageKey] )
const removeItem = useCallback(() => { removeValueFromStorage({ storageKey, storage }) }, [storage, storageKey])
return { state: JSON.parse(store ?? 'null'), setItemValue, removeItem }}
import { useSyncExternalStore, useEffect, useCallback, useRef } from 'react'
export type Storage = 'localStorage' | 'sessionStorage'
const subscribe = (callback: (newValue: string | null) => void) => { const listener = (event: StorageEvent) => { callback(event.newValue) } window.addEventListener('storage', listener)
return () => { window.removeEventListener('storage', listener) }}
const getStorageValue = ({ storageKey, storage}: { storageKey: string; storage: Storage }) => { return window[storage].getItem(storageKey)}
const setValueToStorage = <ValueType>({ storageKey, storage, value}: { storageKey: string; storage: Storage; value: ValueType }) => { const stringyfiedValue = JSON.stringify(value) window[storage].setItem(storageKey, stringyfiedValue)
window.dispatchEvent( new StorageEvent('storage', { key: storageKey, newValue: stringyfiedValue }) )}
const removeValueFromStorage = ({ storageKey, storage}: { storageKey: string; storage: Storage }) => { window[storage].removeItem(storageKey) window.dispatchEvent(new StorageEvent('storage', { key: storageKey }))}
export type FunctionAsValue<ValueType> = (value: ValueType) => ValueType
const valueIsFunction = <T>(value: T | FunctionAsValue<T>): value is FunctionAsValue<T> => typeof value === 'function'
export function useWebStorage<StateType>({ storageKey, initialState, storage = 'localStorage'}: { storageKey: string initialState?: StateType storage?: Storage}) { const store = useSyncExternalStore( subscribe, () => getStorageValue({ storageKey, storage }), () => JSON.stringify(initialState ?? null) ) const storeRef = useRef(store) const initialStateRef = useRef(initialState) const isFirstRender = useRef(true)
useEffect(() => { if ( getStorageValue({ storageKey, storage }) === null && initialStateRef.current !== undefined && isFirstRender.current ) { setValueToStorage({ storageKey, storage, value: initialStateRef.current }) } isFirstRender.current = false }, [storageKey, storage])
useEffect(() => { storeRef.current = store }, [store])
const setItemValue = useCallback( (value: StateType | FunctionAsValue<StateType>) => { if (value == null) { throw new TypeError( `Cannot set the following storage key's value to null or undefined: ${storageKey}. If you want to remove the item from storage, use the removeItem method instead` ) }
let newValue = null
if (valueIsFunction(value)) { if (storeRef.current === null && initialStateRef.current === undefined) { throw new TypeError( `Cannot call ${value.name} as the store's value is null and no initial state was provided` ) } newValue = storeRef.current !== null ? value(JSON.parse(storeRef.current)) : value(initialStateRef.current as StateType) } else { newValue = value }
setValueToStorage({ storageKey, storage, value: newValue }) }, [storage, storageKey] )
const removeItem = useCallback(() => { removeValueFromStorage({ storageKey, storage }) }, [storage, storageKey])
return { state: JSON.parse(store ?? 'null') as StateType | null, setItemValue, removeItem }}
Parameters
storageKey
: A string that represents the key under which the value is stored in the web storage.initialState
(optional): The initial state value to set if the storage is empty.storage
(optional): A string that specifies which storage to use, either'localStorage'
or'sessionStorage'
. Defaults to'localStorage'
.
Return
This hook returns an object containing:
state
: The current value stored in the specified storage, parsed as the appropriate type ornull
if not found.setItemValue
: A function to set a new value in the storage. It can accept a value or a function that returns a new value based on the current state.removeItem
: A function to remove the item from storage.
Example
import { useWebStorage } from './path/to/use-web-storage'
const WebStorageExample = () => { const { state, setItemValue, removeItem } = useWebStorage({ storageKey: 'exampleKey', initialState: 'Hello, World!' })
return ( <div> <h1>Stored Value: {state}</h1> <button onClick={() => setItemValue('New Value')}>Set New Value</button> <button onClick={removeItem}>Remove Item</button> </div> )}
export default WebStorageExample