useStateHandler
Written or Updated on September 10, 2022 🖋️
import React, { useEffect, useState } from 'react'
export type StateHandler<T> = {
value: T
setValue: React.Dispatch<React.SetStateAction<T>>
}
export const useStateHandler = <T>(
initialValue: T,
storageType?: 'local' | 'session',
storageKey?: string,
debounce?: number
): StateHandler<T> => {
let provider: Storage | null = null
if (typeof window !== 'undefined' && storageType && storageKey) {
provider =
storageType === 'local'
? global?.localStorage || window?.localStorage || localStorage || null
: global?.sessionStorage || window?.sessionStorage || sessionStorage || null
}
const initialState = provider
? () => {
const s = provider!.getItem(storageKey!)
return s ? JSON.parse(s) : initialValue
}
: initialValue
const [state, setState] = useState<T>(initialState)
useEffect(() => {
if (!provider) return
if (!debounce) {
provider!.setItem(storageKey!, JSON.stringify(state))
return
}
const t = setTimeout(() => {
provider!.setItem(storageKey!, JSON.stringify(state))
}, debounce)
return () => clearTimeout(t)
}, [state])
return {
value: state,
setValue: setState,
}
}
Context
This is the React hooks that wraps useState and allows you to choose localStorage or sessionStorage as the storage destination. This can be used as normal useState if you don’t specify storageType. In that case, there seems to be no point in using this hooks, but if you declare s state with normal useState, it will be (it’s nearly “must be”) divided into state and setState, and the number of props will increase eventually, so I personally keep using this hooks instead of normal useState. That is also the reason that returns an object not an array.
Writing to localStorage is a more expensive action than the usual state update action, so it is better to set an appropriate debounce value(ms) if you can expect frequent state update like a text form.
Usage
import { useStateHandler, StateHandler } from './useStateHandler'
const Component = () => {
const inputHandler = useStateHandler<string>('', 'local', 'input')
return <Input handler={inputHandler} />
}
const Input = ({ handler }: { handler: StateHandler<string> }) => {
return (
<div>
<input
type={'text'}
value={inputHandler.value}
onChange={(e) => inputHandler.setValue(e.target.value)}
/>
</div>
)
}
Explanation
let provider: Storage | null = null
if (typeof window !== 'undefined' && storageType && storageKey) {
provider =
storageType === 'local'
? global?.localStorage || window?.localStorage || localStorage || null
: global?.sessionStorage || window?.sessionStorage || sessionStorage || null
}
The first if statement allows assigning storage to variable provider only when typeof window !== ‘undefined’. This seems an unnecessary condition but localStorage and sessionStorage only exist on the browser, and SSR frameworks such as Gatsby and Next will complain about it on build, so the condition is needed to keep them quiet and run without errors. According to storageType which was passed as props, assign localStorage or sessionStorage to provider. In case of storage doesn’t exist for some reason assign null instead. In the rest of the code, we use this provider as an if condition.
const initialState = provider
? () => {
const s = provider!.getItem(storageKey!)
return s ? JSON.parse(s) : initialValue
}
: initialValue
const [state, setState] = useState<T>(initialState)
You can use a function as an argument of setState. It is called Lazy initial state, which calculates expensive computation only on the initial render. When provider is NOT null, which means it’s told to save to storage, check that the value for the specified key already exists or not. And when provider is null, just returns initialValue as usual.
useEffect(() => {
if (!provider) return
if (!debounce) {
provider!.setItem(storageKey!, JSON.stringify(state))
return
}
const t = setTimeout(() => {
provider!.setItem(storageKey!, JSON.stringify(state))
}, debounce)
return () => clearTimeout(t)
}, [state])
This part is a bit complicated but there are only two things to do.
- ・Every time the state is updated, save it to localStorage or sessionStorage
- ・Do debounce using setTimeout when debounce is set in props
The first line is concerned provider exists or not because there’s no need to save to storage in the first place when it’s null. useEffect will be triggered and run setItem every time the value of state gets updated. When debounce was not set, just runs setItem() and that’s it. When debounce is set, keep resetting the timer of setTimeout so that setItem() will only be executed after the time of debounce value has passed.