DEV Community

Cover image for Use LocalStorage Hook in React with TypeScript
Radzion Chachura
Radzion Chachura

Posted on • Originally published at radzion.com

Use LocalStorage Hook in React with TypeScript

🚨 Watch on YouTube

I have a productivity app, and usually, I store things that can work without a back-end in the local storage. Here we have a hook that provides a state for a widget that plays focus sounds. It stores everything in the local storage, and I call it usePersistentStorage. It receives a key for local storage and an optional initial value.

const focusSoundsInitialState: FocusSoundsState = {
  isEnabled: false,
  mode: "compilation",
  compilationConfiguration: defaultFocusSoundsComilationConfiguration,
  configuration: {
    fire: 0.8,
    night: 0.2,
    seaside: 0.2,
  },
}

// inside of the component
const [state, setState] = usePersistentStorageValue(
  "focus-sounds",
  focusSoundsInitialState
)
Enter fullscreen mode Exit fullscreen mode

In the hook, we have useState with initializer function and useEffectthat listens for the state change and updates localStorage. In the initializer, we start with taking value from local storage. Then we check if it's an object and return either value from storage or the initial value.

import { useEffect, useState } from "react"

import { persistentStorage } from "./persistentStorage"

export function usePersistentStorageValue<T>(key: string, initialValue?: T) {
  const [value, setValue] = useState<T>(() => {
    const valueFromStorage = persistentStorage.getItem(key)

    if (
      typeof initialValue === "object" &&
      !Array.isArray(initialValue) &&
      initialValue !== null
    ) {
      return {
        ...initialValue,
        ...valueFromStorage,
      }
    }

    return valueFromStorage || initialValue
  })

  useEffect(() => {
    persistentStorage.setItem(key, value)
  }, [key, value])

  return [value, setValue] as const
}
Enter fullscreen mode Exit fullscreen mode

There might be a situation when we want to use something different than localStorage, so we have an abstraction of persistent storage. It has two methods, one to get a value and another to set.

interface PersistentStorage {
  getItem(key: string): string | null
  setItem(key: string, value: any): void
}

class LocalStorage implements PersistentStorage {
  getItem(key: string) {
    const item = localStorage.getItem(key)

    if (item === null) return undefined

    if (item === "null") return null
    if (item === "undefined") return undefined

    try {
      return JSON.parse(item)
    } catch {}

    return item
  }
  setItem(key: string, value: any) {
    if (value === undefined) {
      localStorage.removeItem(key)
    } else {
      localStorage.setItem(key, JSON.stringify(value))
    }
  }
}

class MockStorage implements PersistentStorage {
  getItem() {
    return null
  }
  setItem() {}
}

export const persistentStorage = window?.localStorage
  ? new LocalStorage()
  : new MockStorage()
Enter fullscreen mode Exit fullscreen mode

If there's no local storage in the window, we can provide a fallback, but I don't worry about that. In the getItem, we have fancy checks. Sometimes we need to distinguish null from undefined. In the end, we return a parsed result. There could be something wrong with the format, so we wrap it with try-catch. If we want to change the format of the stored value, we can migrate by changing the key. One approach would be to update a date postfix of the key every time we want to migrate.

Top comments (0)