I needed a simple way to use the sessionStorage
and localStorage
with React, and one of the requirements was to use TypeScript. The idea from Robin Wieruch's original article is pretty simple. It encapsulates the browser sessionStorage
or localStorage
with the useState
hook. The hooks actually return a stateful value, and a function to update it. All I had to do was to put everything together into a TypeScript module.
You can also customize the STORAGE_KEYS_PREFIX
, which can be useful to avoid conflicts between storage keys from different applications you might be developing.
import { useState, useEffect } from 'react' | |
// Check if the code is running in the browser (Next.js support) | |
const isClient = typeof window !== 'undefined' | |
type StorageType = 'localStorage' | 'sessionStorage' | |
/** | |
* A prefix to identify session and local storage keys saved using | |
* the storage hooks in this application. | |
*/ | |
export const STORAGE_KEYS_PREFIX = 'my-app_' | |
/** | |
* Interface for a JSON converter which provides methods to serialize | |
* and deserialize values to and from JSON strings. | |
*/ | |
export interface JsonConverter<AppType, ParsedType> { | |
/** | |
* Serializes a value to a JSON-compatible object. | |
* @param value The value to be serialized. | |
* @returns The JSON-compatible object representation of the value. | |
*/ | |
toJson: (value: AppType) => ParsedType | |
/** | |
* Deserializes a JSON-compatible object to a value. | |
* @param storedValue The JSON-compatible object to be deserialized. | |
* @returns The deserialized value. | |
*/ | |
fromJson: (storedValue: ParsedType) => AppType | |
} | |
const storageFactory = | |
(storageType: StorageType, keyPrefix: string) => | |
<AppType, ParsedType = AppType>( | |
storageKey: string, | |
fallbackState: AppType, | |
converter?: JsonConverter<AppType, ParsedType> | |
): [AppType, React.Dispatch<React.SetStateAction<AppType>>] => { | |
const storage: Storage | null = isClient ? window[storageType] : null | |
if (!storageKey) throw new Error(`"storageKey" must be a nonempty string, but "${storageKey}" was passed.`) | |
const [value, setValue] = useState<AppType>(() => { | |
if (!storage) return fallbackState | |
const storedString = storage.getItem(keyPrefix + storageKey) | |
if (storedString && converter) { | |
const parsedValue = JSON.parse(storedString) | |
return converter.fromJson(parsedValue) | |
} else if (storedString) { | |
return JSON.parse(storedString) // If no converter provided, return the parsed JSON directly | |
} else { | |
return fallbackState | |
} | |
}) | |
useEffect(() => { | |
if (!storage) return | |
if (converter) { | |
const jsonValue = converter.toJson(value) | |
storage.setItem(keyPrefix + storageKey, JSON.stringify(jsonValue)) | |
} else { | |
storage.setItem(keyPrefix + storageKey, JSON.stringify(value)) | |
} | |
}, [value, storageKey, converter]) | |
return [value, setValue] | |
} | |
/** | |
* Saves data in local storage. | |
* @param storageKey A string to identify the value being cached. | |
* @param fallbackState The default value when no value has been stored yet. | |
* @returns A stateful value, and a function to update it. | |
* @example | |
* const [collapsed, setCollapsed] = useLocalStorage('isSidebarCollapsed', false); | |
*/ | |
const useLocalStorage = storageFactory('localStorage', STORAGE_KEYS_PREFIX) | |
/** | |
* Saves data in session storage. | |
* @param storageKey A string to identify the value being cached. | |
* @param fallbackState The default value when no value has been stored yet. | |
* @returns A stateful value, and a function to update it. | |
* @example | |
* const [collapsed, setCollapsed] = useSessionStorage('isSidebarCollapsed', false); | |
*/ | |
const useSessionStorage = storageFactory('sessionStorage', STORAGE_KEYS_PREFIX) | |
export { useLocalStorage, useSessionStorage } |
If you like this post, you'll also like:
Top comments (3)
This is a cool approach for keeping data synced with storage. I faced a similar problem a while ago and came up with a not too dissimilar solution though it's more of a wrapper for storage rather than a direct line in and out of it.
React: Custom hook for accessing storage
Andrew Bone ・ Apr 21 '21 ・ 8 min read
To be honest the idea is not mine I just refined the validation a little bit and wrote it using TypeScript. I didn't credit the original author because it was late and I was tired, but as soon as I find it I'll edit the post. 😅
Edit: I have updated the post with a link to the original article.
Great article.
Some comments have been hidden by the post's author - find out more