specwright/templates/skills/dev-team/frontend/state-management/SKILL.md
# State Management > Skill Template: Frontend Development > Category: Application State & Data Flow > Version: 1.0.0 > Created: 2026-01-09 ## Purpose Implement scalable state management patterns for frontend applications using modern state management libraries, reactive programming, and data flow architectures. ## When to Activate Activate this skill when: - Designing application state architecture - Implementing global state management - Managing complex data flows - Synchronizing client a
npx skillsauth add michsindlinger/specwright specwright/templates/skills/dev-team/frontend/state-managementInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Skill Template: Frontend Development Category: Application State & Data Flow Version: 1.0.0 Created: 2026-01-09
Implement scalable state management patterns for frontend applications using modern state management libraries, reactive programming, and data flow architectures.
Activate this skill when:
import { useState, useReducer, useCallback } from 'react';
// Simple state with useState
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(c => c + 1);
return <button onClick={increment}>Count: {count}</button>;
};
// Complex state with useReducer
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.payload, done: false }];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.payload ? { ...todo, done: !todo.done } : todo
);
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.payload);
default:
return state;
}
};
const TodoList = () => {
const [todos, dispatch] = useReducer(todoReducer, []);
const addTodo = useCallback((text) => {
dispatch({ type: 'ADD_TODO', payload: text });
}, []);
const toggleTodo = useCallback((id) => {
dispatch({ type: 'TOGGLE_TODO', payload: id });
}, []);
return (
<div>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
/>
))}
</div>
);
};
import { createContext, useContext, useReducer } from 'react';
// Create context
const AppStateContext = createContext();
// State reducer
const appReducer = (state, action) => {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'SET_THEME':
return { ...state, theme: action.payload };
case 'TOGGLE_SIDEBAR':
return { ...state, sidebarOpen: !state.sidebarOpen };
default:
return state;
}
};
// Provider component
export const AppStateProvider = ({ children }) => {
const [state, dispatch] = useReducer(appReducer, {
user: null,
theme: 'light',
sidebarOpen: false
});
return (
<AppStateContext.Provider value={{ state, dispatch }}>
{children}
</AppStateContext.Provider>
);
};
// Custom hook for consuming state
export const useAppState = () => {
const context = useContext(AppStateContext);
if (!context) {
throw new Error('useAppState must be used within AppStateProvider');
}
return context;
};
// Usage in components
const Header = () => {
const { state, dispatch } = useAppState();
const toggleTheme = () => {
dispatch({
type: 'SET_THEME',
payload: state.theme === 'light' ? 'dark' : 'light'
});
};
return (
<header className={`header header-${state.theme}`}>
<button onClick={toggleTheme}>Toggle Theme</button>
</header>
);
};
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
// Simple store
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}));
// Store with middleware (devtools + persist)
const useUserStore = create(
devtools(
persist(
(set, get) => ({
user: null,
token: null,
login: (user, token) => set({ user, token }),
logout: () => set({ user: null, token: null }),
updateProfile: (updates) =>
set({ user: { ...get().user, ...updates } })
}),
{ name: 'user-storage' }
)
)
);
// Computed/derived state
const useCartStore = create((set, get) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id)
})),
// Computed values
get totalPrice() {
return get().items.reduce((sum, item) => sum + item.price, 0);
},
get itemCount() {
return get().items.length;
}
}));
// Usage in components
const Cart = () => {
const { items, totalPrice, addItem, removeItem } = useCartStore();
return (
<div>
<h2>Cart ({items.length} items)</h2>
{items.map(item => (
<CartItem key={item.id} item={item} onRemove={removeItem} />
))}
<div>Total: ${totalPrice.toFixed(2)}</div>
</div>
);
};
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Fetch data with caching
const useUsers = () => {
return useQuery({
queryKey: ['users'],
queryFn: async () => {
const response = await fetch('/api/users');
return response.json();
},
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000 // 10 minutes
});
};
// Mutation with optimistic updates
const useUpdateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, updates }) => {
const response = await fetch(`/api/users/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
return response.json();
},
// Optimistic update
onMutate: async ({ id, updates }) => {
await queryClient.cancelQueries({ queryKey: ['users'] });
const previousUsers = queryClient.getQueryData(['users']);
queryClient.setQueryData(['users'], (old) =>
old.map(user => user.id === id ? { ...user, ...updates } : user)
);
return { previousUsers };
},
// Rollback on error
onError: (err, variables, context) => {
queryClient.setQueryData(['users'], context.previousUsers);
},
// Refetch after success
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
}
});
};
// Usage in component
const UserList = () => {
const { data: users, isLoading, error } = useUsers();
const updateUser = useUpdateUser();
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
const handleUpdate = (id, updates) => {
updateUser.mutate({ id, updates });
};
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} onUpdate={handleUpdate} />
))}
</div>
);
};
import { defineStore } from 'pinia';
// Options API style
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Counter'
}),
getters: {
doubleCount: (state) => state.count * 2,
countPlusOne() {
return this.count + 1;
}
},
actions: {
increment() {
this.count++;
},
async fetchCount() {
const response = await fetch('/api/count');
const data = await response.json();
this.count = data.count;
}
}
});
// Composition API style
export const useUserStore = defineStore('user', () => {
const user = ref(null);
const token = ref(null);
const isAuthenticated = computed(() => !!token.value);
const login = async (credentials) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
});
const data = await response.json();
user.value = data.user;
token.value = data.token;
};
const logout = () => {
user.value = null;
token.value = null;
};
return { user, token, isAuthenticated, login, logout };
});
// Usage in components
<script setup>
import { useCounterStore } from '@/stores/counter';
const counter = useCounterStore();
</script>
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">Increment</button>
</div>
</template>
// useToggle composable
export function useToggle(initialValue = false) {
const value = ref(initialValue);
const toggle = () => {
value.value = !value.value;
};
const setTrue = () => {
value.value = true;
};
const setFalse = () => {
value.value = false;
};
return { value, toggle, setTrue, setFalse };
}
// useAsync composable for data fetching
export function useAsync(asyncFunction) {
const data = ref(null);
const error = ref(null);
const loading = ref(false);
const execute = async (...params) => {
loading.value = true;
error.value = null;
try {
data.value = await asyncFunction(...params);
} catch (e) {
error.value = e;
} finally {
loading.value = false;
}
};
return { data, error, loading, execute };
}
// Usage
<script setup>
const { value: isOpen, toggle } = useToggle(false);
const fetchUsers = async () => {
const response = await fetch('/api/users');
return response.json();
};
const { data: users, loading, execute: loadUsers } = useAsync(fetchUsers);
onMounted(() => {
loadUsers();
});
</script>
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class CounterService {
private countSubject = new BehaviorSubject<number>(0);
public count$: Observable<number> = this.countSubject.asObservable();
increment(): void {
this.countSubject.next(this.countSubject.value + 1);
}
decrement(): void {
this.countSubject.next(this.countSubject.value - 1);
}
reset(): void {
this.countSubject.next(0);
}
}
// Usage in component
@Component({
selector: 'app-counter',
template: `
<div>
<p>Count: {{ count$ | async }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
</div>
`
})
export class CounterComponent {
count$ = this.counterService.count$;
constructor(private counterService: CounterService) {}
increment(): void {
this.counterService.increment();
}
decrement(): void {
this.counterService.decrement();
}
}
// Actions
import { createAction, props } from '@ngrx/store';
export const loadUsers = createAction('[User List] Load Users');
export const loadUsersSuccess = createAction(
'[User API] Load Users Success',
props<{ users: User[] }>()
);
export const loadUsersFailure = createAction(
'[User API] Load Users Failure',
props<{ error: string }>()
);
// Reducer
import { createReducer, on } from '@ngrx/store';
export interface UserState {
users: User[];
loading: boolean;
error: string | null;
}
const initialState: UserState = {
users: [],
loading: false,
error: null
};
export const userReducer = createReducer(
initialState,
on(loadUsers, state => ({ ...state, loading: true })),
on(loadUsersSuccess, (state, { users }) => ({
...state,
users,
loading: false,
error: null
})),
on(loadUsersFailure, (state, { error }) => ({
...state,
loading: false,
error
}))
);
// Selectors
import { createFeatureSelector, createSelector } from '@ngrx/store';
export const selectUserState = createFeatureSelector<UserState>('users');
export const selectAllUsers = createSelector(
selectUserState,
(state) => state.users
);
export const selectUsersLoading = createSelector(
selectUserState,
(state) => state.loading
);
// Usage in component
@Component({
selector: 'app-user-list',
template: `
<div *ngIf="loading$ | async">Loading...</div>
<div *ngFor="let user of users$ | async">
{{ user.name }}
</div>
`
})
export class UserListComponent implements OnInit {
users$ = this.store.select(selectAllUsers);
loading$ = this.store.select(selectUsersLoading);
constructor(private store: Store) {}
ngOnInit(): void {
this.store.dispatch(loadUsers());
}
}
[MCP_TOOLS]
<!-- Populated during skill creation based on: 1. User's installed MCP servers 2. User's selection for this skill Recommended for this skill (examples): - State management debugging tools - Performance monitoring platforms - Data flow visualization tools Note: Skills work without MCP servers, but functionality may be limited -->Keep state as close to where it's used as possible:
Normalize nested/relational data:
// Bad: Nested structure
{
posts: [
{ id: 1, title: 'Post 1', author: { id: 1, name: 'John' } },
{ id: 2, title: 'Post 2', author: { id: 1, name: 'John' } }
]
}
// Good: Normalized structure
{
posts: {
byId: {
1: { id: 1, title: 'Post 1', authorId: 1 },
2: { id: 2, title: 'Post 2', authorId: 1 }
},
allIds: [1, 2]
},
authors: {
byId: {
1: { id: 1, name: 'John' }
},
allIds: [1]
}
}
Compute derived values instead of storing them:
// Bad: Storing derived state
const [items, setItems] = useState([]);
const [itemCount, setItemCount] = useState(0);
const [totalPrice, setTotalPrice] = useState(0);
// Good: Computing derived state
const [items, setItems] = useState([]);
const itemCount = items.length;
const totalPrice = items.reduce((sum, item) => sum + item.price, 0);
Always create new objects/arrays for state updates:
// Bad: Mutating state
state.items.push(newItem);
// Good: Immutable update
setState({ items: [...state.items, newItem] });
Remember: Good state management is about choosing the right tool for the right job. Not all state needs global management, and not all data fetching needs caching.
tools
Session Handoff: Erstellt eine vollständige Zusammenfassung der aktuellen Session für einen sauberen Kontextwechsel. NUR bei explizitem Aufruf (/session-handoff). NICHT automatisch auslösen. Geeignet wenn der User die Session resetten will, den Kontext aufräumen will, oder bei ~120k Tokens angelangt ist.
development
Pre-Mortem Risk Analysis: Strukturierte Prospective-Hindsight-Übung um launch-blocking Risiken vor Commitment aufzudecken. Team stellt sich vor, das Produkt sei 14 Tage nach Launch gefloppt, und arbeitet rückwärts. Klassifiziert Risiken in Tigers (echt), Paper Tigers (hypothetisch), Elephants (unausgesprochen). Nutze diesen Skill vor Build-Commitment, bei zu hoher Stakeholder-Confidence, vor Major-Releases, oder wenn das Team vage Sorgen nicht artikulieren kann. Trigger: /pre-mortem, 'pre-mortem', 'risk analysis', 'was könnte schiefgehen', 'risiken vor launch'.
testing
Six-Sigma Atomicity Validator for create-spec stories
tools
UX pattern definition guidance for navigation, user flows, interactions, and accessibility