Skip to content

localStorage and Persistence

useEffect lets you save state to localStorage. Initializing state from localStorage on first render gives you persistence — data that survives page reloads without a database or server.

const [incomeSources, setIncomeSources] = useState([]);
useEffect(() => {
localStorage.setItem('zb-income', JSON.stringify(incomeSources));
}, [incomeSources]);

Every time incomeSources changes, this effect saves the new value. JSON.stringify converts the array to a string — localStorage only stores strings.

State initialization runs once on first render. Pass a function to useState to compute the initial value lazily:

const [incomeSources, setIncomeSources] = useState(() => {
try {
const stored = localStorage.getItem('zb-income');
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
});

The function form of useState is called lazy initialization — React calls the function once and uses the return value as the initial state. Without the function form, localStorage.getItem would run on every render (not just the first), which is wasteful.

The try/catch handles cases where localStorage contains invalid JSON — old data, a browser extension writing garbage, or a user manually editing storage.

This pattern repeats for every piece of state you want to persist. Extract it into a custom hook:

import { useState } from 'react';
export function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error('useLocalStorage write error:', error);
}
};
return [storedValue, setValue];
}

value instanceof Function handles the functional update form: setValue(prev => prev + 1).

Now use it exactly like useState:

const [incomeSources, setIncomeSources] = useLocalStorage('zb-income', []);

Every set triggers both a React state update and a localStorage write. No useEffect needed.

ZeroBudget stores each month independently. The key includes the year and month:

const key = `zb-${year}-${String(month + 1).padStart(2, '0')}`;
const [monthData, setMonthData] = useLocalStorage(key, emptyMonth());

When the user navigates to a different month, the key changes. useLocalStorage initializes from the new key’s stored value (or the default if nothing is stored yet). The previous month’s data stays in localStorage untouched.

  1. Create src/hooks/useLocalStorage.js with the useLocalStorage hook above.
  2. Replace the incomeSources useState in App.jsx with useLocalStorage('zb-income', []).
  3. Add a few income sources, reload the page, and confirm they persist.
  4. Check the Application → Local Storage section in DevTools to see the stored JSON.
  • Save state to localStorage with a useEffect that runs when the value changes.
  • Initialize state from localStorage with the lazy function form of useState.
  • Wrap both in a useLocalStorage custom hook so the pattern is reusable with a clean API.
  • Key localStorage entries by month/year so each month stores independently.