State Management (Zustand)
State Architecture
Section titled “State Architecture”SparkFeed separates state into two categories:
| Category | Tool | Examples |
|---|---|---|
| Server state | TanStack Query | Feed list, articles, database data |
| Client state | Zustand | Sidebar open/closed, selected article, UI theme |
Server state (data that comes from the database) is handled by TanStack Query. Client state (ephemeral UI state) is handled by Zustand.
Why Zustand?
Section titled “Why Zustand?”Zustand is a minimal state management library for React:
- Tiny: Under 1KB gzipped
- No boilerplate: No actions, reducers, or middleware required
- React-friendly: Works natively with hooks
- TypeScript-first: Full type inference
Compared to Redux, Zustand needs ~90% less code for the same functionality.
The UI Store
Section titled “The UI Store”SparkFeed’s main UI store:
// src/client/stores/ui.store.tsimport { create } from 'zustand';
interface UIStore { // Sidebar sidebarOpen: boolean; toggleSidebar: () => void; setSidebarOpen: (open: boolean) => void;
// Selected article selectedArticleId: number | null; setSelectedArticle: (id: number | null) => void;
// Reading pane readingPaneVisible: boolean; setReadingPaneVisible: (visible: boolean) => void;}
export const useUIStore = create<UIStore>((set) => ({ sidebarOpen: true, toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })), setSidebarOpen: (open) => set({ sidebarOpen: open }),
selectedArticleId: null, setSelectedArticle: (id) => set({ selectedArticleId: id }),
readingPaneVisible: false, setReadingPaneVisible: (visible) => set({ readingPaneVisible: visible }),}));Using the Store in Components
Section titled “Using the Store in Components”// In a componentimport { useUIStore } from '@/stores/ui.store';
export function SidebarToggle() { const { sidebarOpen, toggleSidebar } = useUIStore();
return ( <button onClick={toggleSidebar} aria-label="Toggle sidebar"> {sidebarOpen ? <PanelLeftClose /> : <PanelLeftOpen />} </button> );}Zustand is selector-based, so components only re-render when the specific state they use changes.
Combining Server and Client State
Section titled “Combining Server and Client State”TanStack Query manages server state with caching and invalidation:
// ArticleList.tsx: combining both storesimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';import { useUIStore } from '@/stores/ui.store';
export function ArticleList({ feedId }: { feedId: number }) { const { selectedArticleId, setSelectedArticle } = useUIStore();
const { data, isLoading } = useQuery({ queryKey: ['articles', feedId], queryFn: () => fetchArticles(feedId), });
return ( <ul> {data?.articles.map((article) => ( <li key={article.id} onClick={() => setSelectedArticle(article.id)} data-selected={article.id === selectedArticleId} > {article.title} </li> ))} </ul> );}