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.
Saving state to localStorage
Section titled “Saving state to localStorage”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.
Initializing from localStorage
Section titled “Initializing from localStorage”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.
A reusable useLocalStorage hook
Section titled “A reusable useLocalStorage hook”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.
Month-keyed storage in ZeroBudget
Section titled “Month-keyed storage in ZeroBudget”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.
Exercise
Section titled “Exercise”- Create
src/hooks/useLocalStorage.jswith theuseLocalStoragehook above. - Replace the
incomeSourcesuseStateinApp.jsxwithuseLocalStorage('zb-income', []). - Add a few income sources, reload the page, and confirm they persist.
- Check the Application → Local Storage section in DevTools to see the stored JSON.
- Save state to localStorage with a
useEffectthat runs when the value changes. - Initialize state from localStorage with the lazy function form of
useState. - Wrap both in a
useLocalStoragecustom hook so the pattern is reusable with a clean API. - Key localStorage entries by month/year so each month stores independently.