Persisting your React state in 9 lines
selbekk

selbekk @selbekk

About: I'm a full stack dev that specializes in React, English Bulldogs and beer based coding meetups

Location:
Oslo, Norway
Joined:
Mar 8, 2019

Persisting your React state in 9 lines

Publish Date: May 7 '19
160 22

I was playing around with a project from Frontend Mentor this weekend, and I was implementing this theme switcher with React hooks. It struck me that persisting which theme I had chosen between reloads would be a nice feature. So let's build a hook that provides just that!

This article will take you through the process of creating a reusable custom hook that persists our state to local storage.

Getting started

We're going to create a custom hook named usePersistedState to store our state to local storage. Our function should accept a key to store the state under, as well as the default value (in case we haven't saved anything yet). It will return the same API as useState (a tuple of the state and an updater function). Here's our hook signature:

function usePersistedState(key, defaultValue) {
  // Some magic
  return [state, setState];
}
Enter fullscreen mode Exit fullscreen mode

Even though we store our state in local storage, we're keeping a local runtime copy in a regular setState call. This is so that we can trigger re-renders, as well as improve access time slightly (accessing local storage might not be that quick). Finally, if localStorage is not available for some reason, we still have a working hook (although it won't persist the setting).

function usePersistedState(key, defaultValue) {
  const [state, setState] = React.useState(defaultValue);
  return [state, setState];
}
Enter fullscreen mode Exit fullscreen mode

Saving data in local storage

Next up, let's start reading from local storage! The localStorage API is built into your browser, and lets you access values by calling the getItem function with a string key.

function usePersistedState(key, defaultValue) {
  const [state, setState] = React.useState(
    localStorage.getItem(key) || defaultValue
  );
  return [state, setState];
}
Enter fullscreen mode Exit fullscreen mode

Here, we set the default value of our useState call to be whatever we've had stored in localStorage, or the defaultValue we passed in as an argument. Next, let's implement updating our local storage as well. We're going to use a useEffect hook for that:

function usePersistedState(key, defaultValue) {
  const [state, setState] = React.useState(
    localStorage.getItem(key) || defaultValue
  );
  useEffect(() => {
    localStorage.setItem(key, state);
  }, [key, state]);
  return [state, setState];
}
Enter fullscreen mode Exit fullscreen mode

Clever, right? Every time we update our state, we should update what's stored in our local storage. If the key changes, we'd want to store our current state under the new key as well.

What about complex values?

Although the local storage API is great, it can only store string values. This is kind of a pain - but we can get around this limitation by serializing our JavaScript objects to JSON whenever we update our state (and back again). We do this with the JSON.parse and JSON.stringify functions.

function usePersistedState(key, defaultValue) {
  const [state, setState] = React.useState(
    JSON.parse(localStorage.getItem(key)) || defaultValue
  );
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(state));
  }, [key, state]);
  return [state, setState];
}
Enter fullscreen mode Exit fullscreen mode

Now we support complex data structures too!

A last performance optimization

Our current implementation has one performance pitfall - we're reading from local storage on every render! To make matters worse - we're doing it just to get the initial value for our useState call! Luckily, there's a way around this sort of issue. By passing in a function to useState, the default value will only be run once!

Let's implement this:

function usePersistedState(key, defaultValue) {
  const [state, setState] = React.useState(
    () => JSON.parse(localStorage.getItem(key)) || defaultValue
  );
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(state));
  }, [key, state]);
  return [state, setState];
}
Enter fullscreen mode Exit fullscreen mode

Summing up!

And that's it! We've implemented a pretty neat piece of reusable code in a few lines of code. This is perfect for local settings like themes, font-sizes or whatever else UI state you'd like to persist between visits.

Here's the project I mentioned initially, complete with this very hook to save the selected theme. Try it out!

What is your favorite reusable hook?

Comments 22 total

  • Maxim
    MaximJan 7, 2020

    Really nice, thank you Kristofer! This is a very pragmatic approach. I kind of like Redux, and you inspired me to add a construct React.useReducer on top of this. It might be just the perfect substitute for a full-blown react-redux + redux-persist setup.

    • fredericocotrim
      fredericocotrimFeb 11, 2020

      Could you share your approuch using usePersistedState on useReducer?

      • Cristobal Aguirre
        Cristobal AguirreApr 13, 2020

        I adapted both this post and this blog post by Swizec Teller to get what I wanted using useReducer. I'm making an ecommerce site and wanted to persist the cart items. So I did:

        const initialState = {
          //other state
          cartItems: JSON.parse(localStorage.getItem('cart_items')) || [],
        }
        
        function reducer(state, action) {
          switch (action.type) {
            case 'UPDATE_CART': {
              const { variantId, quantity } = action.payload;
              const updatedCartItems = state.cartItems.filter(
                i => i.variantId !== variantId
              );
              const cartItems = [...updatedCartItems, { variantId, quantity }];
              // write to localStorage <-- key part
              if (typeof window !== 'undefined') {
                localStorage.setItem('cart_items', JSON.stringify(cartItems));
              }
              return {
                ...state,
                cartItems,
              };
            }
        

        and then plugged that into context. If you need more context, I'm using gatsby and followed this guide to set up the state mgt. logic.

        Hope it helps!

    • islami00
      islami00Feb 22, 2022

      Ikr! I was also considering learning redux to persist the state left over from react query.
      The main thing I was running away from was typing reducers because I use typescript. I think I'll try this with a custom json serialise function for any special objects by prepping them as classes. I'll update the design I had in mind initially with useReducer.
      (Taking notes here now, haha) The reducer is a bit like redux in the sense that there's one global reducer that has a root and nodes of different ui components in the state tree. It would be generic enough to have typings and allow other components to have their own custom branch of this node.
      I guess the localstate update would need to be more complex in that case to avoid chucking it all in one key. Hmm, I wonder what the limit for that is really.

  • Ron Arts
    Ron ArtsMay 4, 2020

    What if an Async Storage solution is being used, like AsyncStorage on react native?

  • yinonhever
    yinonheverJul 2, 2020

    Hey, can I use this code in a class-based component? Or can it only be used in a functional component whose state is managed with Hooks?

    • selbekk
      selbekkJul 2, 2020

      Hi! Hooks unfortunately require function components to work, but you could recreate the same functionality with either a HOC or just directly in your class component. In your componentDidUpdate, add a line that serializes and saves your state to local storage.

      componentDidUpdate() {
        window.localStorage.setItem(
          my state key,
          JSON.stringify(this.state)
        );
      }
      

      When you initialize the state, you need to do the reverse:

      constructor(props) {
        this.state = JSON.parse(
          window.localStorage.getItem(my state key)
        ) || { fallback: value };
      }
      

      Hope that helps!

  • yinonhever
    yinonheverJul 16, 2020

    Great custom hook, I already used it in several projects. Are you gonna make it an NPM package?

    • selbekk
      selbekkJul 18, 2020

      For 9 lines of code? I’d rather just copy it. But feel free to make a package out of it yourself if you want to 🥳

  • selbekk
    selbekkAug 23, 2020

    That happens when you try to render a regular object as children. I’d have to see more of the related code to be of any assistance

  • Jad
    JadSep 25, 2020

    This was very helpful ☺️ thanks for sharing 💜

  • Yuhan Xiao
    Yuhan XiaoOct 15, 2020

    Hi @selbekk , nice article! Can you explain a little about the last section(about optimization)? How would

    () => JSON.parse(localStorage.getItem(key))

    inside useState() work? Wouldn't it not work, since it is declared, not invoked inside useState()?

    • selbekk
      selbekkOct 15, 2020

      Thanks!

      When you pass a function as an argument to useState instead of a "regular" value, it is only run on the initial render. It’s a way React offers to do initialization work only once, instead of on each render.

  • Ivo Silva
    Ivo SilvaDec 9, 2020

    In case you're storing falsy values, you might want to initialize the state like this:

    const [value, setValue] = useState(() => {
        const storedValue = localStorage.getItem(key);
        return storedValue !== null ? JSON.parse(storedValue) : defaultValue;
    });
    
    Enter fullscreen mode Exit fullscreen mode
  • АнонимApr 7, 2021

    [deleted]

  • Stephanie Raymos
    Stephanie RaymosApr 22, 2021

    Thanks for this! Can you explain how we would actually use this within the application?

    • selbekk
      selbekkApr 25, 2021

      Hi Stephanie!

      You would copy the source code into your project, and import the usePersistentState hook where you would want to use it

  • Chavez Harris
    Chavez HarrisNov 7, 2021

    This is awesome!

  • Taranveer Singh
    Taranveer SinghJan 22, 2025

    Thanks a lot for this brilliant code!

Add comment