Why not Redux
Redux added value in an era before React had stable hooks and when server state syncing was ad-hoc. Today, server-cached libraries and tiny local stores cover most needs with less boilerplate.
Modern applications have two distinct types of state:
- Server state: Data that lives on the server (fetched via API)
- UI state: Client-only state (modals, theme, form inputs)
Redux conflates these, leading to unnecessary complexity. The better approach: dedicated tools for each.
TanStack Query Patterns
Use TanStack Query (formerly React Query) for all server state. It handles caching, background refetching, optimistic updates, and request deduplication out of the box.
import { useQuery, useMutation } from '@tanstack/react-query';
// Fetching with automatic caching
function UserProfile({ userId }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 60_000, // 1 minute
});
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <Profile user={data} />;
}
// Mutations with optimistic updates
function UpdateProfile() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: updateUser,
onMutate: async (newUser) => {
await queryClient.cancelQueries(['user', newUser.id]);
const previous = queryClient.getQueryData(['user', newUser.id]);
queryClient.setQueryData(['user', newUser.id], newUser);
return { previous };
},
onError: (err, newUser, context) => {
queryClient.setQueryData(
['user', newUser.id],
context.previous
);
},
onSettled: (data) => {
queryClient.invalidateQueries(['user', data.id]);
},
});
}
Key Benefits
- Automatic background refetching
- Window focus refetching
- Retry logic with exponential backoff
- Request deduplication
- DevTools for debugging
Zustand for UI State
Zustand excels for local UI state: toggles, modal open state, or ephemeral editor content. It's tiny (1KB), serializable, and composes well with Query-driven data.
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
// Simple UI store
const useUIStore = create(
persist(
(set) => ({
theme: 'dark',
sidebarOpen: true,
setTheme: (theme) => set({ theme }),
toggleSidebar: () => set((state) => ({
sidebarOpen: !state.sidebarOpen
})),
}),
{ name: 'ui-storage' }
)
);
// Usage in component
function Sidebar() {
const { sidebarOpen, toggleSidebar } = useUIStore();
return (
<aside className={sidebarOpen ? 'open' : 'closed'}>
<button onClick={toggleSidebar}>Toggle</button>
</aside>
);
}
The Combined Approach
In practice, most apps use both:
- TanStack Query: All API calls, server state, cached data
- Zustand: UI preferences, form state, ephemeral flags
This architecture eliminated 90% of our Redux boilerplate while improving type safety and developer experience.
No more actions, reducers, or middleware—just hooks that work naturally with React's component model.