state-management
npx skills add https://github.com/supercent-io/skills-template --skill state-management
Agent 安装分布
Skill 文档
State Management
When to use this skill
- ì ì ìí íì: ì¬ë¬ ì»´í¬ëí¸ê° ê°ì ë°ì´í° ê³µì
- Props Drilling 문ì : 5ë¨ê³ ì´ì props ì ë¬
- ë³µì¡í ìí ë¡ì§: ì¸ì¦, ì¥ë°êµ¬ë, í ë§ ë±
- ìí ë기í: ìë² ë°ì´í°ì í´ë¼ì´ì¸í¸ ìí ë기í
Instructions
Step 1: ìí ë²ì ê²°ì
ë¡ì»¬ vs ì ì ìí를 구ë¶í©ëë¤.
íë¨ ê¸°ì¤:
-
ë¡ì»¬ ìí: ë¨ì¼ ì»´í¬ëí¸ììë§ ì¬ì©
- í¼ ì ë ¥ê°, í ê¸ ìí, ëë¡ë¤ì´ ì´ë¦¼/ë«í
useState,useReducerì¬ì©
-
ì ì ìí: ì¬ë¬ ì»´í¬ëí¸ìì ê³µì
- ì¬ì©ì ì¸ì¦, ì¥ë°êµ¬ë, í ë§, ì¸ì´ ì¤ì
- Context API, Redux, Zustand ì¬ì©
ìì:
// â
ë¡ì»¬ ìí (ë¨ì¼ ì»´í¬ëí¸)
function SearchBox() {
const [query, setQuery] = useState('');
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => setIsOpen(true)}
/>
{isOpen && <SearchResults query={query} />}
</div>
);
}
// â
ì ì ìí (ì¬ë¬ ì»´í¬ëí¸)
// ì¬ì©ì ì¸ì¦ ì ë³´ë Header, Profile, Settings ë±ìì ì¬ì©
const { user, logout } = useAuth(); // Context ëë Zustand
Step 2: React Context API (ê°ë¨í ì ì ìí)
ê°ë²¼ì´ ì ì ìí ê´ë¦¬ì ì í©í©ëë¤.
ìì (ì¸ì¦ Context):
// contexts/AuthContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
interface User {
id: string;
email: string;
name: string;
}
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (email: string, password: string) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
setUser(data.user);
localStorage.setItem('token', data.token);
};
const logout = () => {
setUser(null);
localStorage.removeItem('token');
};
return (
<AuthContext.Provider value={{
user,
login,
logout,
isAuthenticated: !!user
}}>
{children}
</AuthContext.Provider>
);
}
// Custom hook
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
ì¬ì©:
// App.tsx
function App() {
return (
<AuthProvider>
<Router>
<Header />
<Routes />
</Router>
</AuthProvider>
);
}
// Header.tsx
function Header() {
const { user, logout, isAuthenticated } = useAuth();
return (
<header>
{isAuthenticated ? (
<>
<span>Welcome, {user!.name}</span>
<button onClick={logout}>Logout</button>
</>
) : (
<Link to="/login">Login</Link>
)}
</header>
);
}
Step 3: Zustand (íëì ì´ê³ ê°ê²°í ìí ê´ë¦¬)
Reduxë³´ë¤ ê°ë¨íê³ ë³´ì¼ë¬íë ì´í¸ê° ì ìµëë¤.
ì¤ì¹:
npm install zustand
ìì (ì¥ë°êµ¬ë):
// stores/cartStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartStore {
items: CartItem[];
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
total: () => number;
}
export const useCartStore = create<CartStore>()(
devtools(
persist(
(set, get) => ({
items: [],
addItem: (item) => set((state) => {
const existing = state.items.find(i => i.id === item.id);
if (existing) {
return {
items: state.items.map(i =>
i.id === item.id
? { ...i, quantity: i.quantity + 1 }
: i
)
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
}),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id)
})),
updateQuantity: (id, quantity) => set((state) => ({
items: state.items.map(item =>
item.id === id ? { ...item, quantity } : item
)
})),
clearCart: () => set({ items: [] }),
total: () => {
const { items } = get();
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
}),
{ name: 'cart-storage' } // localStorage key
)
)
);
ì¬ì©:
// components/ProductCard.tsx
function ProductCard({ product }) {
const addItem = useCartStore(state => state.addItem);
return (
<div>
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addItem(product)}>
Add to Cart
</button>
</div>
);
}
// components/Cart.tsx
function Cart() {
const items = useCartStore(state => state.items);
const total = useCartStore(state => state.total());
const removeItem = useCartStore(state => state.removeItem);
return (
<div>
<h2>Cart</h2>
{items.map(item => (
<div key={item.id}>
<span>{item.name} x {item.quantity}</span>
<span>${item.price * item.quantity}</span>
<button onClick={() => removeItem(item.id)}>Remove</button>
</div>
))}
<p>Total: ${total.toFixed(2)}</p>
</div>
);
}
Step 4: Redux Toolkit (ëê·ëª¨ ì±)
ë³µì¡í ìí ë¡ì§ê³¼ 미ë¤ì¨ì´ê° íìí ê²½ì° ì¬ì©í©ëë¤.
ì¤ì¹:
npm install @reduxjs/toolkit react-redux
ìì (Todo):
// store/todosSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
interface Todo {
id: string;
text: string;
completed: boolean;
}
interface TodosState {
items: Todo[];
status: 'idle' | 'loading' | 'failed';
}
const initialState: TodosState = {
items: [],
status: 'idle'
};
// ë¹ë기 ì¡ì
export const fetchTodos = createAsyncThunk('todos/fetch', async () => {
const response = await fetch('/api/todos');
return response.json();
});
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: (state, action: PayloadAction<string>) => {
state.items.push({
id: Date.now().toString(),
text: action.payload,
completed: false
});
},
toggleTodo: (state, action: PayloadAction<string>) => {
const todo = state.items.find(t => t.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
removeTodo: (state, action: PayloadAction<string>) => {
state.items = state.items.filter(t => t.id !== action.payload);
}
},
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.status = 'idle';
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state) => {
state.status = 'failed';
});
}
});
export const { addTodo, toggleTodo, removeTodo } = todosSlice.actions;
export default todosSlice.reducer;
// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './todosSlice';
export const store = configureStore({
reducer: {
todos: todosReducer
}
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
ì¬ì©:
// App.tsx
import { Provider } from 'react-redux';
import { store } from './store';
function App() {
return (
<Provider store={store}>
<TodoApp />
</Provider>
);
}
// components/TodoList.tsx
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../store';
import { toggleTodo, removeTodo } from '../store/todosSlice';
function TodoList() {
const todos = useSelector((state: RootState) => state.todos.items);
const dispatch = useDispatch();
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch(toggleTodo(todo.id))}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => dispatch(removeTodo(todo.id))}>Delete</button>
</li>
))}
</ul>
);
}
Step 5: ìë² ìí ê´ë¦¬ (React Query / TanStack Query)
API ë°ì´í° fetching ë° ìºì±ì í¹íëì´ ììµëë¤.
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: string }) {
const queryClient = useQueryClient();
// GET: ì¬ì©ì ì ë³´ ì¡°í
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const res = await fetch(`/api/users/${userId}`);
return res.json();
},
staleTime: 5 * 60 * 1000, // 5ë¶ê° ìºì
});
// POST: ì¬ì©ì ì ë³´ ìì
const mutation = useMutation({
mutationFn: async (updatedUser: Partial<User>) => {
const res = await fetch(`/api/users/${userId}`, {
method: 'PATCH',
body: JSON.stringify(updatedUser)
});
return res.json();
},
onSuccess: () => {
// ìºì 무í¨í ë° ì¬ì¡°í
queryClient.invalidateQueries({ queryKey: ['user', userId] });
}
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<button onClick={() => mutation.mutate({ name: 'New Name' })}>
Update Name
</button>
</div>
);
}
Output format
ìí ê´ë¦¬ ë구 ì í ê°ì´ë
ìí©ë³ ì¶ì² ë구:
1. ê°ë¨í ì ì ìí (í
ë§, ì¸ì´)
â React Context API
2. ì¤ê° ë³µì¡ë (ì¥ë°êµ¬ë, ì¬ì©ì ì¤ì )
â Zustand
3. ëê·ëª¨ ì±, ë³µì¡í ë¡ì§, 미ë¤ì¨ì´ íì
â Redux Toolkit
4. ìë² ë°ì´í° fetching/caching
â React Query (TanStack Query)
5. í¼ ìí
â React Hook Form + Zod
Constraints
íì ê·ì¹ (MUST)
-
ìí ë¶ë³ì±: ìíë ì ë ì§ì ìì íì§ ìì
// â ëì ì state.items.push(newItem); // â ì¢ì ì setState({ items: [...state.items, newItem] }); -
ìµì ìí ìì¹: íì ê°ë¥í ê°ì ìíë¡ ì ì¥íì§ ìì
// â ëì ì const [items, setItems] = useState([]); const [count, setCount] = useState(0); // items.lengthë¡ ê³ì° ê°ë¥ // â ì¢ì ì const [items, setItems] = useState([]); const count = items.length; // íì ê° -
ë¨ì¼ ì§ì¤ì ìì²: ê°ì ë°ì´í°ë¥¼ ì¬ë¬ ê³³ì ì¤ë³µ ì ì¥ ê¸ì§
ê¸ì§ ì¬í (MUST NOT)
-
Props Drilling ê³¼ë¤: 5ë¨ê³ ì´ì props ì ë¬ ê¸ì§
- Context ëë ìí ê´ë¦¬ ë¼ì´ë¸ë¬ë¦¬ ì¬ì©
-
모ë ê²ì ì ì ìíë¡: ë¡ì»¬ ìíë¡ ì¶©ë¶í ê²½ì° ì ì ìí ì¬ì© ì§ì
Best practices
-
ì íì 구ë : íìí ìíë§ êµ¬ë
// â ì¢ì ì: íìí ê²ë§ const items = useCartStore(state => state.items); // â ëì ì: ì ì²´ 구ë const { items, addItem, removeItem, updateQuantity, clearCart } = useCartStore(); -
ì¡ì ì´ë¦ ëª íí:
updateâupdateUserProfile -
TypeScript ì¬ì©: íì ìì ì± íë³´
References
Metadata
ë²ì
- íì¬ ë²ì : 1.0.0
- ìµì¢ ì ë°ì´í¸: 2025-01-01
- í¸í íë«í¼: Claude, ChatGPT, Gemini
ê´ë ¨ ì¤í¬
- ui-component-patterns: ì»´í¬ëí¸ì ìí íµí©
- backend-testing: ìí ë¡ì§ í ì¤í¸
íê·¸
#state-management #React #Redux #Zustand #Context #global-state #frontend