docs: add Trellis planning and project specs

This commit is contained in:
2026-07-01 06:27:55 -07:00
parent 7f227c5f2a
commit aafd9caaac
349 changed files with 55801 additions and 0 deletions

View File

@@ -0,0 +1,372 @@
# 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 = <K extends keyof typeof filters>(
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<DashboardContextValue | null>(null);
export function DashboardProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<DashboardState>({
sidebarCollapsed: false,
activeWidget: null,
});
const toggleSidebar = () => {
setState((prev) => ({
...prev,
sidebarCollapsed: !prev.sidebarCollapsed,
}));
};
const setActiveWidget = (widget: string | null) => {
setState((prev) => ({ ...prev, activeWidget: widget }));
};
return (
<DashboardContext.Provider
value={{ ...state, toggleSidebar, setActiveWidget }}
>
{children}
</DashboardContext.Provider>
);
}
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<DashboardState | null>(null);
const DashboardActionsContext = createContext<DashboardActions | null>(null);
export function DashboardProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<DashboardState>(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 (
<DashboardStateContext.Provider value={state}>
<DashboardActionsContext.Provider value={actions}>
{children}
</DashboardActionsContext.Provider>
</DashboardStateContext.Provider>
);
}
// 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<FilterContextValue | null>(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 (
<FilterContext.Provider
value={{
selectedId,
setSelectedId,
view,
setView,
}}
>
{children}
</FilterContext.Provider>
);
}
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<string | null>(
urlSelectedId // Initialize from URL
);
// Keep context in sync with URL changes
useEffect(() => {
setSelectedId(urlSelectedId);
}, [urlSelectedId]);
return (
<SelectionContext.Provider value={{ selectedId, setSelectedId }}>
{children}
</SelectionContext.Provider>
);
}
```
## State Debugging
### React Query DevTools
```typescript
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<>
<AppContent />
<ReactQueryDevtools initialIsOpen={false} />
</>
);
}
```
### Context Debug Component
```typescript
function DebugContext() {
const state = useDashboardState();
if (process.env.NODE_ENV !== 'development') return null;
return (
<pre className="fixed bottom-4 right-4 p-2 bg-black/80 text-white text-xs">
{JSON.stringify(state, null, 2)}
</pre>
);
}
```
## 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