"Stop guessing. State management and local storage aren't the same thing. Here's why it matters and how to pick the right solution for your app."
Happy Coding! — Ahmed Fahmy
Picture this: you've got a component that needs to save data. So you think "let me just throw this in local storage." You write the code, it compiles... but it doesn't work. The page doesn't update. The user clicks the button a million times. Nothing happens.
Why? Because local storage isn't state management. It's something completely different.
Here's the thing: most developers mix them up. They think local storage solves state management problems. It doesn't. Each one solves a different problem.
State management tells React "the data changed, re-render." Local storage tells the browser "save this for later."
One is about reactivity. The other is about persistence.
// ❌ This is wrong - local storage without state
function Modal() {
const isOpen = localStorage.getItem('isOpen') === 'true'
const handleToggle = () => {
localStorage.setItem('isOpen', !isOpen)
}
return isOpen ? <div>...</div> : null
}
See what's happening? You're saving to local storage. But React doesn't know the data changed. The page doesn't update. The user sees the same thing.
// ✅ This is right - state with local storage
function Modal() {
const [isOpen, setIsOpen] = useState(() => {
return localStorage.getItem('isOpen') === 'true'
})
const handleToggle = () => {
const newValue = !isOpen
setIsOpen(newValue)
localStorage.setItem('isOpen', newValue)
}
return isOpen ? <div>...</div> : null
}
The difference? We used useState. That tells React "the data changed." Then we saved to local storage. Both together.
Local State → Lifted State → Context → Global Store → Persistence
↓ ↓ ↓ ↓ ↓
useState Props pass Context Zustand localStorage
Each one has its place. Each one has its use.
When to use: Your data only lives in one component. No other component needs to know about it.
function Dropdown() {
const [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(!isOpen)}>
Click me
</button>
{isOpen && <ul>...</ul>}
</>
)
}
Benefits:
When not to use: When another component needs the same data.
When to use: Two or more components need the same data.
function Parent() {
const [count, setCount] = useState(0)
return (
<>
<Counter count={count} setCount={setCount} />
<Display count={count} />
</>
)
}
Benefits:
Drawbacks:
When not to use: When you have a deep component tree.
When to use: You need to pass data to many components without prop drilling.
const ThemeContext = createContext()
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
function Button() {
const { theme } = useContext(ThemeContext)
return <button className={theme}>Click</button>
}
Benefits:
Drawbacks:
When to use it: Theme, auth, locale - things that don't change often.
When to use: You need truly global state. You can even use it outside React.
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
function Counter() {
const { count, increment } = useStore()
return <button onClick={increment}>{count}</button>
}
Benefits:
When to use it: Shopping cart, user auth, global UI state.
When to use: You need data to stick around after the user closes the browser.
The important truth: Local storage alone isn't enough. You always need to pair it with state.
// ❌ This is wrong
function Component() {
const data = localStorage.getItem('data')
return <div>{data}</div>
}
// ✅ This is right
function Component() {
const [data, setData] = useState(() => {
return localStorage.getItem('data') || ''
})
const handleChange = (newData) => {
setData(newData)
localStorage.setItem('data', newData)
}
return <input value={data} onChange={(e) => handleChange(e.target.value)} />
}
When to use it:
When not to use it:
| Need | Local State | Lifted State | Context | Zustand | + localStorage |
|---|---|---|---|---|---|
| Single component | ✅ | ❌ | ❌ | ❌ | ❌ |
| Sibling components | ❌ | ✅ | ✅ | ✅ | ✅ |
| Deep tree | ❌ | ❌ | ✅ | ✅ | ✅ |
| Truly global | ❌ | ❌ | ❌ | ✅ | ✅ |
| Outside React | ❌ | ❌ | ❌ | ✅ | ✅ |
| Persistence | ❌ | ❌ | ❌ | ✅ | ✅ |
| Selective updates | ❌ | ❌ | ❌ | ✅ | ✅ |
You've got:
// Theme - Context
const ThemeContext = createContext()
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
// Auth & Cart - Zustand
const useStore = create((set) => ({
user: null,
cart: [],
setUser: (user) => set({ user }),
addToCart: (item) => set((state) => ({
cart: [...state.cart, item]
})),
}))
// Persist cart to localStorage
const usePersistedCart = create(
persist(
(set) => ({
cart: [],
addToCart: (item) => set((state) => ({
cart: [...state.cart, item]
})),
}),
{ name: 'cart-storage' }
)
)
// Modal - Local state
function ProductModal() {
const [isOpen, setIsOpen] = useState(false)
return isOpen ? <Modal onClose={() => setIsOpen(false)} /> : null
}
// Form - Local state
function CheckoutForm() {
const [email, setEmail] = useState('')
const [address, setAddress] = useState('')
return (
<form>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<input value={address} onChange={(e) => setAddress(e.target.value)} />
</form>
)
}
Pick the simplest solution that solves your problem. Don't use Zustand if local state works. Don't use local storage without state.
Sources: