How to persist state after a page refresh in React using local storage


What if the browser gets Refreshed? How do we keep the state persistent?

Sometimes it is necessary to keep the state of a React component persistent even after a browser refresh. A simple way to accomplish this without having to rely on any third party library, is to use the localStorage API together with useEffect hook.

Making state persistent

Let's say we want to build a counter component which its state should persist after a page refresh. Starting out, our component would look something along these lines:

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <div>{count}</div>
      <button onClick={() => setCount((c) => c + 1)}>+</button>
      <button onClick={() => setCount((c) => c - 1)}>-</button>
    </div>
  );
}

First, we want to save the value of count to the local storage every time it changes. For that we'll use useEffect hook.

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    localStorage.setItem('count', String(count));
  }, [count]);

  return (
    <div>
      <div>{count}</div>
      <button onClick={() => setCount((c) => c + 1)}>+</button>
      <button onClick={() => setCount((c) => c - 1)}>-</button>
    </div>
  );
}

If you try to refresh you app right now, you'll notice that the value of the counter is still not persistent. However, if you inspect the local storage, you'll notice that count is being stored. Now we only need to pass the value of count to the counter component before it has been rendered for the first time. To do that, we'll pass the value in local storage as the initial state of useState.

function Counter() {
  const storedValueAsNumber = Number(localStorage.getItem('count'));
  const [count, setCount] = useState(
    Number.isInteger(storedValueAsNumber) ? storedValueAsNumber : 0
  );

  useEffect(() => {
    localStorage.setItem('count', String(count));
  }, [count]);

  return (
    <div>
      <div>{count}</div>
      <button onClick={() => setCount((c) => c + 1)}>+</button>
      <button onClick={() => setCount((c) => c - 1)}>-</button>
    </div>
  );
}

Using the Number constructor is necessary, since an item retrieved from local storage comes as a string. Since a user can change the number persisted to the local storage to any arbitrary value, it's a good idea to check if the retrieved value is actually an integer.

Considerations about this solution

By using this solution, we're actually creating shared mutable state between all instances of the Counter component. This might or might not be a problem depending on the specific situation. As a rule of thumb, if we want to reuse a component across an application, this is not a good approach. However, remember to always think things through according to your current situation. No rule of thumb is better than a well done analysis of the situation that is right in front of you.

For this example, we used the string count as key for our counter value. On a real world application, we may want to use a unique ID to make sure we are not using and/or overriding any value that is being stored at the local stored. A key such as count-mhvXdrZT4jP5T8vBxuvm75 would be appropriate.

As an alternative, if we don't want different instances of the Counter component to share state, we could receive the key for the local storage as a prop to the component.

Give it a try

Next time you're faced with having to make a React component's state persistent across a browser refresh, remember to:

  1. Analyze your current situation to check if using Local Storage together with useEffect hook is appropriate
  2. Get a unique key to store your component state in order to avoid collisions at the Local Storage
  3. Add the necessary useEffect hook to update your component's persisted state