From aai-dev-frontend
Provides patterns and code examples for managing local UI, shared, server (TanStack Query/SWR), URL (Next.js), and global (Zustand/Redux) state in React apps.
How this skill is triggered — by the user, by Claude, or both
Slash command
/aai-dev-frontend:state-managementThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Patterns for choosing and implementing state management strategies in frontend applications.
Patterns for choosing and implementing state management strategies in frontend applications.
State that only affects a single component.
// Form input values
const [value, setValue] = useState('');
// Toggle states
const [isOpen, setIsOpen] = useState(false);
// Loading/error states
const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle');
State shared between components (lift state up or use context).
// Theme context
const ThemeContext = createContext<Theme>('light');
function ThemeProvider({ children }) {
const [theme, setTheme] = useState<Theme>('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
Data from external sources (use TanStack Query, SWR, or similar).
// TanStack Query
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 5 * 60 * 1000, // 5 minutes
});
// Mutations
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
State that should be shareable/bookmarkable.
// Next.js App Router
import { useSearchParams, useRouter } from 'next/navigation';
function FilteredList() {
const searchParams = useSearchParams();
const router = useRouter();
const filter = searchParams.get('filter') || 'all';
const setFilter = (value: string) => {
const params = new URLSearchParams(searchParams);
params.set('filter', value);
router.push(`?${params.toString()}`);
};
return <Filters value={filter} onChange={setFilter} />;
}
| Use Case | Solution |
|---|---|
| Simple local state | useState |
| Complex local state | useReducer |
| Cross-component state | Context API |
| Global app state | Zustand, Redux Toolkit |
| Server state | TanStack Query, SWR |
| Form state | React Hook Form, Formik |
| URL state | Router params/search params |
import { create } from 'zustand';
interface AppState {
user: User | null;
setUser: (user: User | null) => void;
notifications: Notification[];
addNotification: (notification: Notification) => void;
removeNotification: (id: string) => void;
}
const useAppStore = create<AppState>((set) => ({
user: null,
setUser: (user) => set({ user }),
notifications: [],
addNotification: (notification) =>
set((state) => ({
notifications: [...state.notifications, notification],
})),
removeNotification: (id) =>
set((state) => ({
notifications: state.notifications.filter((n) => n.id !== id),
})),
}));
// Query configuration
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
gcTime: 5 * 60 * 1000, // 5 minutes
retry: 3,
refetchOnWindowFocus: false,
},
},
});
// Custom hook for domain logic
function useUsers(filters: UserFilters) {
return useQuery({
queryKey: ['users', filters],
queryFn: () => fetchUsers(filters),
select: (data) => data.users,
});
}
Compute values instead of storing them.
// Bad: Synced state
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, item) => sum + item.price, 0));
}, [items]);
// Good: Derived state
const [items, setItems] = useState([]);
const total = useMemo(
() => items.reduce((sum, item) => sum + item.price, 0),
[items]
);
For complex state transitions.
type Status = 'idle' | 'loading' | 'success' | 'error';
function useAsync<T>() {
const [state, setState] = useState<{
status: Status;
data: T | null;
error: Error | null;
}>({
status: 'idle',
data: null,
error: null,
});
const execute = async (promise: Promise<T>) => {
setState({ status: 'loading', data: null, error: null });
try {
const data = await promise;
setState({ status: 'success', data, error: null });
} catch (error) {
setState({ status: 'error', data: null, error: error as Error });
}
};
return { ...state, execute };
}
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot previous value
const previousTodos = queryClient.getQueryData(['todos']);
// Optimistically update
queryClient.setQueryData(['todos'], (old) =>
old.map((t) => (t.id === newTodo.id ? newTodo : t))
);
return { previousTodos };
},
onError: (err, newTodo, context) => {
// Rollback on error
queryClient.setQueryData(['todos'], context.previousTodos);
},
onSettled: () => {
// Always refetch after error or success
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
Is the state...
1. Used by only this component?
→ useState/useReducer
2. Shared by a few nearby components?
→ Lift state up to common ancestor
3. Used across many unrelated components?
→ Context or global store
4. From an external API?
→ TanStack Query/SWR
5. Should be in the URL?
→ Router state (params/search)
6. Complex with many transitions?
→ State machine (XState, useReducer)
Used by:
frontend-developer agentnpx claudepluginhub bradtaylorsf/alphaagent-teamProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.