Tutorial: useState() + localStorage in React with custom hooks

Last updated on May 2020

Sometimes you want to have some basic way to persist data between tab reloads in your React app. One way to do it is with localStorage. This tutorial explains a way to automatically set and get from local storage so you can persist data between reloads.

For a simple react app, you can implement this yourself, with custom hooks.

I've done it here with a couple - one to getLocalStorageValue that takes in a key, and initial value. If it finds a value in localstorage for the key, then it returns it. Otherwise returns the initial value.

This is called in useStorageState() which uses useState(). When the value is updated, it updates state as well as localStorage value for the key.

This could be expanded to also include support for passing in a function as initial value, so it would only be computed if there was no cached value.

import {useEffect, useState} from 'react'
/*
 for a key, return the cached value (from local storage).
 If none exists, return the initial value instead.
 */
function getLocalStorageValue(key, initialValue) {
    try {
        const cached = window.localStorage.getItem(key) ?? '';
        if (cached) return JSON.parse(cached);
    } catch {
        // clear existing data - probably a JSON.parse() error
        window.localStorage.setItem(key, '');
    }

    /*
       note: You might want to support initialValue being an function,
       and return initialValue() if it is. But that is not in this demo
      */
    return initialValue;
}

/**
 * React hook to useState, but initially load from localStorage
 * and also set any changes to localStorage
 */
function useStorageState(key, initialValue) {
    // pass in a function to useState so it
    // only gets called the first time
    const [value, setValue] = useState(() =>
        getLocalStorageValue(key, initialValue)
    );

    // return 2 items, similar to useState()
    return [
        value,

        // wrapper around setValue, and also set local storage
        (newValue) => {
            setValue(newValue);
            window.localStorage.setItem(key, JSON.stringify(newValue));
        },
    ];
}

function YourComponent() {
    const [name, setName] = useStorageState('name', 'fred');
    const [age, setAge] = useStorageState('age', 5);

    return (
        <>
            <input
                type="text"
                value={name}
                onChange={(e) => setName(e.currentTarget.value)}
            />
            <input
                type="text"
                value={age}
                onChange={(e) => setAge(e.currentTarget.value)}
            />
            <h1>
                Hello {name}, you are {age}
            </h1>
        </>
    );
}

Demo of this component in use

Note: normally when useState() is used you can pass in a function that receives an argument of the current state. The implementation above does not support that.

If you are using this on a SSR (server side rendered) site you will face some issues:

  • The server has no access to your local storage
  • If you resolve that (by putting the window.localStorage call in useEffect for example), your users will sometimes see a flash of the SSR content, then the content loaded from your local storage value. There are ways to work around it but its out of scope of this basic tutorial.

Making it reactive

The version above is not really reactive. If another part of your application (or another tab) updates localStorage, your effects will not re-run. If you want to make it reactive, then you can use BroadcastChannel to send a message (every time you update your localstorage property), and listen for changes. That is out of the scope of this article, but it is quite simple to setup.

© 2019-2023 a5h.dev.
All Rights Reserved. Use information found on my site at your own risk.