While implementing features in the Indent dashboard, we heavily use the localStorage browser API. A convenient way to use the localStorage in React is the useLocalStorage
hook from usehooks.com. Below is a simplified example of the pattern that often occurs in our code while using the useLocalStorage
hook:
const Component1 = () => {const [value, setValue] = useLocalStorage('key1')return <div>Component1 {value}</div>;};const Component2 = () => {const [value, setValue] = useLocalStorage('key1')return <div>Component2 {value}</div>;};const Page = () => (<><Component1 /><Component2 /></>;);
In the above example we have two independent components utilizing the useLocalStorage
hook with the same key. The original implementation of the useLocalStorage
has a few drawbacks when used in this manner:
useLocalStorage
creates a dedicated state variable with useState
. Each of these states is completely separate from one another. If all of these states need to be updated to a new value, one re-render is going to occur for each of the updates.useLocalStorage
creates a separate setValue
function that updates only the piece of state that it is associated with. This means that if the setValue
is called within one of these components the other component wouldn't reflect the correct value of the localStorage entry until the page is refreshed.useLocalStorage
independently reads from the localStorage when the component using useLocalStorage
is mounted. This means that different components on the same page could potentially read different values from the localStorage. The result could be an inconsistent UI.Each read from the localStorage happens on the component mount. Each useLocalStorage reads from the localStorage separately.
Initially I was using the swr
JavaScript library (named after the stale-while-revalidate pattern) only for HTTP caching, and then I came to the realisation that swr
is so much more cool and general purpose to only be a library for caching HTTP requests. So I got the idea to write a useLocalStorage
implementation which has a more optimal behaviour using the swr
library and here's the result:
import { useCallback, useMemo } from 'react'import useSWR, { cache } from 'swr'const localStorageFetcher = async (_fetcherName: string, key: string) => {const item = window.localStorage.getItem(key)// An Error occurring in the fetcher is gracefully handled by SWRif (item == undefined) throw new Error()return JSON.parse(item)}export function useLocalStorage<T>(key: string,initialValue?: T | (() => T)): any[] {const swrKey = useMemo(() => ['localStorageFetcher', key], [key])const initialData = useMemo(() => (initialValue instanceof Function ? initialValue() : initialValue),[initialValue])const { data: storedValue, mutate } = useSWR(swrKey, localStorageFetcher, {initialData,// Read from the localStorage only if the value is not in the cache already.revalidateOnMount: !cache.has(swrKey),// Disable refetching from the localStorage. After the initialization from// the localStorage, the value is not re-read until the page is re-loaded.revalidateOnFocus: false,revalidateOnReconnect: false,refreshWhenHidden: false,refreshWhenOffline: false,shouldRetryOnError: false,refreshInterval: 0})const setStoredValue = useCallback((value: T | ((c: T) => T)) => {mutate((currentStoredValue: any) => {const valueToStore =value instanceof Function ? value(currentStoredValue) : valueif (valueToStore === currentStoredValue) return currentStoredValuewindow.localStorage.setItem(key, JSON.stringify(valueToStore))return valueToStore},false // don't refetch from the localStorage, just update the state)},[mutate, key])return [storedValue, setStoredValue]}
The new useLocalStorage
has the same API, but:
useLocalStorage
hook enter the page with the same key, they don't read from the localStorage again, but use the swr
cached value insteadsetValue
function causes the immediate change in all other components using useLocalStorage
with that key. No page refresh is necessary.All of this means that the performance and the robustness of the UI is going to be improved.
Instead of having a separate piece of state for every invocation of the useLocalStorage hook, we have a single piece of state that all of the useLocalStorage invocations refer to. Only the first component that mounts reads from the localStorage. Every subsequent component reads from the SWR Cache.
After implementing this version of the useLocalStorage
hook I wasn't surprised to discover that others have attempted to do the same. Most notably the zydalabs/swr-internal-state library contains a slightly different implementation of useLocalStorage
using swr
.