google "reducer" if you're confused by the cover image
When I was building gistmarks, I needed to store users data and pass it around to various parts of the app. For this I typically use Redux combined with Redux Persist. This approach is tried and true but does involve quite a bit of boilerplate code so I wanted to try something new.
I quickly learned that useReducer
is a highly competent alternative to Redux and typing it (adding typescript types) is much more straightforward than it is with Redux. There was however one thing missing: Persistence.
For me, being able to persist a users state is crucial to my app functioning so having a way of persisting data with the useReducer
hook was essential. With Redux I would always use redux-persist however there didn't seem to be any formal way of doing it with useReducer
.
As a result I created my own hook that persists the reducers data to localStorage. Here's that hook:
Javascript Version:
import { useEffect, useReducer } from "react"
import deepEqual from "fast-deep-equal/es6"
import { usePrevious } from "./usePrevious"
export function usePersistedReducer(
reducer,
initialState,
storageKey,
) {
const [state, dispatch] = useReducer(reducer, initialState, init)
const prevState = usePrevious(state)
function init() {
const stringState = localStorage.getItem(storageKey)
if (stringState) {
try {
return JSON.parse(stringState)
} catch (error) {
return initialState
}
} else {
return initialState
}
}
useEffect(() => {
const stateEqual = deepEqual(prevState, state)
if (!stateEqual) {
const stringifiedState = JSON.stringify(state)
localStorage.setItem(storageKey, stringifiedState)
}
}, [state])
return { state, dispatch }
}
Typescript Version:
import { useEffect, useReducer } from "react"
import deepEqual from "fast-deep-equal/es6"
import { usePrevious } from "./usePrevious"
export function usePersistedReducer<State, Action>(
reducer: (state: State, action: Action) => State,
initialState: State,
storageKey: string
) {
const [state, dispatch] = useReducer(reducer, initialState, init)
const prevState = usePrevious(state)
function init(): State {
const stringState = localStorage.getItem(storageKey)
if (stringState) {
try {
return JSON.parse(stringState)
} catch (error) {
return initialState
}
} else {
return initialState
}
}
useEffect(() => {
const stateEqual = deepEqual(prevState, state)
if (!stateEqual) {
const stringifiedState = JSON.stringify(state)
localStorage.setItem(storageKey, stringifiedState)
}
}, [state])
return { state, dispatch }
}
For this hook you will also need a companion hook called usePrevious
Typescript Version:
import { useRef, useEffect } from "react"
// Given any value
// This hook will return the previous value
// Whenever the current value changes
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function usePrevious(value: any) {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
Javascript Version:
import { useRef, useEffect } from "react"
// Given any value
// This hook will return the previous value
// Whenever the current value changes
export function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
How it works
The hook manages syncing state internally. Whenever you issue a dispatch, an effect within the hook checks the previous state of the reducer and if the state changed, it will backup that state to localStorage.
How to use it
Using this hook is super easy.
const initialState = {...}
function reducer(state = initialState, action) {...}
const storageKey = 'MY_STORAGE_KEY'
const { state, dispatch } = usePersistedReducer(reducer, initialState, storageKey)
// use state and dispatch as you normally would.
Conclusion
That's pretty much it. If you think I could improve this hook leave a comment and I'll update the article. If you liked this article check out some of my other posts here
Top comments (0)