Most developers write React components reactively — state changes, component re-renders, done. This works until you have 50 components, cross-component state dependencies, and performance problems you can't trace.
These patterns give your state a structure that scales.
Colocation First
Before reaching for any state management library, ask: does this state need to be shared? If not, keep it local:
// Good -- dropdown state lives where it's used
function UserMenu() {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Menu</button>
{isOpen && <DropdownItems />}
</div>
)
}
// Bad -- dropdown state in global store
const useStore = create(set => ({
isMenuOpen: false,
setMenuOpen: (v) => set({ isMenuOpen: v }),
}))
Global state is for data that is genuinely shared across distant components: auth user, theme, cart contents, notifications.
URL as State
Search filters, pagination, selected tabs — these belong in the URL:
import { useSearchParams } from 'next/navigation'
function ProductList() {
const searchParams = useSearchParams()
const page = Number(searchParams.get('page') ?? '1')
const category = searchParams.get('category') ?? 'all'
const sort = searchParams.get('sort') ?? 'newest'
// Filters persist on refresh, are shareable, and work with browser back
const products = useProducts({ page, category, sort })
// ...
}
URL state is free persistence, shareability, and browser history integration. Use it before reaching for useState.
Server State is Not Application State
Data from your API should be managed by a fetching library, not manually:
// Bad -- manual server state
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
fetchUser(id).then(setUser).catch(setError).finally(() => setLoading(false))
}, [id])
// Good -- React Query handles all of this
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
staleTime: 5 * 60 * 1000, // 5 min cache
})
React Query / SWR give you caching, deduplication, background refetching, optimistic updates, and error handling for free.
Zustand for Client State
When you do need global client state, Zustand is the leanest option:
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface CartStore {
items: CartItem[]
addItem: (item: CartItem) => void
removeItem: (id: string) => void
clear: () => void
total: () => number
}
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
addItem: (item) => set(state => ({ items: [...state.items, item] })),
removeItem: (id) => set(state => ({ items: state.items.filter(i => i.id !== id) })),
clear: () => set({ items: [] }),
total: () => get().items.reduce((sum, item) => sum + item.price, 0),
}),
{ name: 'cart-storage' } // persists to localStorage
)
)
Avoiding Re-render Cascades
When a Zustand store updates, only components that use the changed slice re-render:
// Bad -- subscribes to entire store, re-renders on any change
const store = useCartStore()
// Good -- subscribes only to items count
const itemCount = useCartStore(state => state.items.length)
// Good -- stable selector with shallow comparison
import { shallow } from 'zustand/shallow'
const { addItem, removeItem } = useCartStore(
state => ({ addItem: state.addItem, removeItem: state.removeItem }),
shallow
)
Context for Dependency Injection
Context is not a state manager — it's a dependency injection tool:
// Good use of context -- stable config values
const ThemeContext = createContext<Theme>('light')
const AuthContext = createContext<AuthUser | null>(null)
// Bad use of context -- frequently changing data causes mass re-renders
const CartContext = createContext<CartState>(...) // use Zustand instead
If the value in context changes frequently, every consumer re-renders. Use Zustand or React Query for dynamic data.
The AI SaaS Starter at whoffagents.com ships with Zustand + React Query pre-configured, correct selector patterns, and a dashboard that demonstrates these patterns at production scale. $99 one-time.
Top comments (0)