useReducerWithLocalStorage — Persisting Reducer State to Local Storage

Published: Mon Feb 28 2022

The built-in useReducer hook is a great tool for managing multiple pieces of component state that need to be updated together. In a recent use case, I was managing a user's preferances/settings withing an app with useReducer and needed to sync those settings with local storage.

On the component level, the state was initially managed as follows:

// reducer
const calculationParamsReducer = (
  state: UserCustomisableParams,
  action: ActionType,
) => {
  switch (action.type) {
    case 'updatePreference1':
      return { ...state, preferance1: action.payload }
    case 'updatePreference2':
      return { ...state, preferance2: action.payload }
  }
}

// initial state
const defaultUserPreferences = {
  preferance1: true,
  preference2: false,
}

// usage
const [userCustomisedParams, dispatch] = useReducer(
  calculationParamsReducer,
  defaultUserPreferences,
)

I'd been using a custom hook to sync component state to local storage with a variant of the useLocalStorage hook. Its API looks like this:

// the hook takes in a key and a default value
const [name, setName] = useLocalStorage<string>(
  'name',
  'Bob',
)

The goal was to compose a new custom hook from this useLocalStorage hook that worked with useReducer.

Composing useLocalStorage with useReducer

Fortunately, I stumbled upon useReducerWithLocalStorage by Mattia Richetto, which shows how to compose useLocalStorage with useReducer.

The hook was a fit for my use case except that it wasn't written in TypeScript. I modified it slightly by making the passing of arguments resemble useReducer and rewrote it in TypeScript:

import * as React from 'react'
// useLocalStorage from useHooks
import { useLocalStorage } from './useLocalStorage'

export const useReducerWithLocalStorage = <S, A>(
  reducer: React.Reducer<S, A>,
  initializerArg: S,
  key: string,
) => {
  const [localStorageState, setLocalStorageState] =
    useLocalStorage(key, initializerArg)

  return React.useReducer(
    (state: S, action: A) => {
      const newState = reducer(state, action)
      setLocalStorageState(newState)
      return newState
    },
    { ...localStorageState },
  )
}

export default useReducerWithLocalStorage

Now I had a flexible hook that provided me with the required type information.

Applying the hook to the problem

I could now take this new hook to use to manage user preferances in the app like so:

// reducer
const calculationParamsReducer = (
  state: UserCustomisableParams,
  action: ActionType,
) => {
  switch (action.type) {
    case 'updatePreference1':
      return { ...state, preferance1: action.payload }
    case 'updatePreference2':
      return { ...state, preferance2: action.payload }
  }
}

// initial state
const defaultUserPreferences = {
  preferance1: true,
  preference2: false,
}

// usage
const [userCustomisedParams, dispatch] =
  useReducerWithLocalStorage(
    calculationParamsReducer,
    defaultUserPreferences,
    'user-preferences',
  )

Here's a link to the a gist of this hook.