# State Management This document covers state management patterns including URL state with nuqs, React Context guidelines, and synchronization strategies. ## State Categories | Category | Tool | When to Use | |----------|------|-------------| | Server State | React Query | API data, cached responses | | URL State | nuqs | Filters, pagination, selected items | | Local UI State | useState | Transient UI (modals, dropdowns) | | Shared UI State | Context | Cross-component UI state | ## URL State with nuqs ### Why URL State? - Shareable: Users can share links with specific state - Bookmarkable: Browser history navigation works - SEO-friendly: Search engines can index different states - Persistent: Survives page refreshes ### Basic Usage ```typescript import { useQueryState } from 'nuqs'; export function useOrderFilters() { const [status, setStatus] = useQueryState('status'); const [page, setPage] = useQueryState('page', { parse: (value) => parseInt(value, 10) || 1, serialize: String, }); return { status, setStatus, page, setPage }; } ``` ### With Default Values ```typescript import { useQueryState, parseAsInteger, parseAsString } from 'nuqs'; export function useProductFilters() { const [category, setCategory] = useQueryState('category', { defaultValue: 'all', parse: parseAsString, }); const [page, setPage] = useQueryState('page', { defaultValue: 1, parse: parseAsInteger, }); const [sortBy, setSortBy] = useQueryState('sort', { defaultValue: 'newest', }); return { category, setCategory, page, setPage, sortBy, setSortBy, }; } ``` ### Complex Filter Objects ```typescript import { useQueryStates, parseAsString, parseAsInteger } from 'nuqs'; const filterParsers = { search: parseAsString.withDefault(''), category: parseAsString.withDefault('all'), minPrice: parseAsInteger, maxPrice: parseAsInteger, page: parseAsInteger.withDefault(1), }; export function useAdvancedFilters() { const [filters, setFilters] = useQueryStates(filterParsers); const updateFilter = ( key: K, value: (typeof filters)[K] ) => { setFilters({ [key]: value, page: 1 }); // Reset page on filter change }; const resetFilters = () => { setFilters({ search: '', category: 'all', minPrice: null, maxPrice: null, page: 1, }); }; return { filters, updateFilter, resetFilters }; } ``` ### Shallow Routing Prevent full page reloads when updating URL state: ```typescript const [tab, setTab] = useQueryState('tab', { shallow: true, // Default is true in nuqs history: 'push', // or 'replace' }); ``` ## React Context Guidelines ### When to Use Context - Theme/appearance settings - User preferences - Feature flags - Cross-cutting concerns (toast notifications, modals) ### When NOT to Use Context - Server data (use React Query instead) - Form state (use form libraries) - Single-component state (use useState) - State that should be in URL ### Context Pattern ```typescript // context/DashboardContext.tsx import { createContext, useContext, useState, ReactNode } from 'react'; interface DashboardState { sidebarCollapsed: boolean; activeWidget: string | null; } interface DashboardContextValue extends DashboardState { toggleSidebar: () => void; setActiveWidget: (widget: string | null) => void; } const DashboardContext = createContext(null); export function DashboardProvider({ children }: { children: ReactNode }) { const [state, setState] = useState({ sidebarCollapsed: false, activeWidget: null, }); const toggleSidebar = () => { setState((prev) => ({ ...prev, sidebarCollapsed: !prev.sidebarCollapsed, })); }; const setActiveWidget = (widget: string | null) => { setState((prev) => ({ ...prev, activeWidget: widget })); }; return ( {children} ); } export function useDashboard() { const context = useContext(DashboardContext); if (!context) { throw new Error('useDashboard must be used within DashboardProvider'); } return context; } ``` ### Split Context for Performance Separate frequently-changing values to prevent unnecessary re-renders: ```typescript // Separate contexts for state and actions const DashboardStateContext = createContext(null); const DashboardActionsContext = createContext(null); export function DashboardProvider({ children }: { children: ReactNode }) { const [state, setState] = useState(initialState); // Memoize actions to prevent re-renders const actions = useMemo( () => ({ toggleSidebar: () => setState((prev) => ({ ...prev, sidebarCollapsed: !prev.sidebarCollapsed, })), setActiveWidget: (widget: string | null) => setState((prev) => ({ ...prev, activeWidget: widget })), }), [] ); return ( {children} ); } // Separate hooks for state and actions export function useDashboardState() { const context = useContext(DashboardStateContext); if (!context) throw new Error('Missing DashboardProvider'); return context; } export function useDashboardActions() { const context = useContext(DashboardActionsContext); if (!context) throw new Error('Missing DashboardProvider'); return context; } ``` ## Context and URL Synchronization When state needs to be both in context (for easy access) and URL (for shareability): ### Pattern: URL as Source of Truth ```typescript import { useQueryState } from 'nuqs'; import { createContext, useContext, ReactNode } from 'react'; interface FilterContextValue { selectedId: string | null; setSelectedId: (id: string | null) => void; view: 'grid' | 'list'; setView: (view: 'grid' | 'list') => void; } const FilterContext = createContext(null); export function FilterProvider({ children }: { children: ReactNode }) { // URL state as the single source of truth const [selectedId, setSelectedId] = useQueryState('selected'); const [view, setView] = useQueryState('view', { defaultValue: 'grid' as const, parse: (v) => (v === 'list' ? 'list' : 'grid'), }); return ( {children} ); } export function useFilters() { const context = useContext(FilterContext); if (!context) throw new Error('Missing FilterProvider'); return context; } ``` ### Pattern: Sync Context to URL When context state needs to be reflected in URL for specific scenarios: ```typescript export function useSyncToUrl() { const { selectedId } = useItemSelection(); // From context const [, setUrlSelectedId] = useQueryState('selected'); // Sync context changes to URL useEffect(() => { setUrlSelectedId(selectedId); }, [selectedId, setUrlSelectedId]); } ``` ### Pattern: Initialize Context from URL ```typescript export function SelectionProvider({ children }: { children: ReactNode }) { // Read initial value from URL const [urlSelectedId] = useQueryState('selected'); const [selectedId, setSelectedId] = useState( urlSelectedId // Initialize from URL ); // Keep context in sync with URL changes useEffect(() => { setSelectedId(urlSelectedId); }, [urlSelectedId]); return ( {children} ); } ``` ## State Debugging ### React Query DevTools ```typescript import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; function App() { return ( <> ); } ``` ### Context Debug Component ```typescript function DebugContext() { const state = useDashboardState(); if (process.env.NODE_ENV !== 'development') return null; return (
      {JSON.stringify(state, null, 2)}
    
); } ``` ## Best Practices 1. **URL First**: Default to URL state for shareable data 2. **Minimal Context**: Keep context small and focused 3. **Separate Concerns**: Don't mix server state with UI state 4. **Type Everything**: Use TypeScript for all state types 5. **Default Values**: Always provide sensible defaults 6. **Single Source**: Avoid duplicating state across systems ## Anti-Patterns - Storing server data in context (use React Query) - Using context for form state (use form libraries) - Deep nesting of providers - Not memoizing context actions - Duplicating URL state in useState