DEV Community

Cover image for Persisting useReducer with a custom React Hook
Sunny Golovine
Sunny Golovine

Posted on

Persisting useReducer with a custom React Hook

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 }
}

Enter fullscreen mode Exit fullscreen mode

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 }
}

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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)