--- name: react-state-management description: State management patterns với Zustand và TanStack Query cho React/Next.js. Use for client state, server state, caching, và optimistic updates. compatibility: "zustand>=5, @tanstack/react-query>=5, react>=19" metadata: author: Velik Ho version: "1.0" --- # React State Management / Quản Lý State React ## When to Use This Skill / Khi Nào Sử Dụng Use this skill when: - Creating Zustand stores / Tạo Zustand stores - Implementing TanStack Query / Triển khai TanStack Query - Managing client vs server state / Quản lý client vs server state - Implementing optimistic updates / Triển khai optimistic updates - Testing stores and queries / Testing stores và queries ## Core Concepts / Khái Niệm Cốt Lõi ### State Categories | Category | Solution | Use Case | |----------|----------|----------| | **UI State** | Zustand | Theme, modals, local preferences | | **Server State** | TanStack Query | API data, caching, sync | | **URL State** | Next.js Router | Filters, pagination, search | | **Form State** | React Hook Form | Form inputs, validation | ## Key Patterns / Mẫu Chính ### Zustand Store Pattern ```typescript // stores/auth-store.ts import { create } from 'zustand'; import { devtools, persist } from 'zustand/middleware'; /** * EN: Auth store interface * VI: Interface cho auth store */ interface AuthState { user: User | null; isAuthenticated: boolean; // Actions login: (user: User) => void; logout: () => void; } /** * EN: Create store with middleware * VI: Tạo store với middleware */ export const useAuthStore = create()( devtools( persist( (set) => ({ user: null, isAuthenticated: false, login: (user) => set({ user, isAuthenticated: true }, false, 'auth/login'), logout: () => set({ user: null, isAuthenticated: false }, false, 'auth/logout'), }), { name: 'auth-storage' } ), { name: 'AuthStore' } ) ); ``` ### Selector Pattern (Performance) ```typescript // ✅ GOOD: Use selectors to prevent unnecessary re-renders // VI: Sử dụng selectors để tránh re-render không cần thiết function UserAvatar() { // Only re-renders when user.avatar changes const avatar = useAuthStore((state) => state.user?.avatar); return ; } // ❌ BAD: Subscribes to entire store function UserAvatar() { const { user } = useAuthStore(); // Re-renders on any store change return ; } ``` ### TanStack Query Pattern ```typescript // services/api.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; /** * EN: Query keys factory for type-safe keys * VI: Factory cho query keys đảm bảo type-safe */ export const chatKeys = { all: ['chats'] as const, lists: () => [...chatKeys.all, 'list'] as const, list: (filters: ChatFilters) => [...chatKeys.lists(), filters] as const, details: () => [...chatKeys.all, 'detail'] as const, detail: (id: string) => [...chatKeys.details(), id] as const, }; /** * EN: Custom hook for fetching chats * VI: Custom hook để fetch chats */ export function useChats(filters: ChatFilters) { return useQuery({ queryKey: chatKeys.list(filters), queryFn: () => fetchChats(filters), staleTime: 5 * 60 * 1000, // 5 minutes }); } /** * EN: Mutation with cache invalidation * VI: Mutation với invalidation cache */ export function useCreateChat() { const queryClient = useQueryClient(); return useMutation({ mutationFn: createChat, onSuccess: () => { // Invalidate all chat lists queryClient.invalidateQueries({ queryKey: chatKeys.lists() }); }, }); } ``` ### Optimistic Updates ```typescript /** * EN: Optimistic update for better UX * VI: Optimistic update cho UX tốt hơn */ export function useSendMessage() { const queryClient = useQueryClient(); return useMutation({ mutationFn: sendMessage, onMutate: async (newMessage) => { // Cancel outgoing refetches await queryClient.cancelQueries({ queryKey: chatKeys.detail(newMessage.chatId) }); // Snapshot previous value const previous = queryClient.getQueryData(chatKeys.detail(newMessage.chatId)); // Optimistically update queryClient.setQueryData(chatKeys.detail(newMessage.chatId), (old: Chat) => ({ ...old, messages: [...old.messages, { ...newMessage, status: 'sending' }], })); return { previous }; }, onError: (err, newMessage, context) => { // Rollback on error queryClient.setQueryData( chatKeys.detail(newMessage.chatId), context?.previous ); }, onSettled: (_, __, { chatId }) => { // Always refetch after error or success queryClient.invalidateQueries({ queryKey: chatKeys.detail(chatId) }); }, }); } ``` ### Provider Setup ```tsx // providers/query-provider.tsx 'use client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { useState } from 'react'; /** * EN: Query provider with client-side only instantiation * VI: Query provider với khởi tạo chỉ phía client */ export function QueryProvider({ children }: { children: React.ReactNode }) { const [queryClient] = useState( () => new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, // 1 minute refetchOnWindowFocus: false, }, }, }) ); return ( {children} ); } ``` ## Testing Patterns / Mẫu Testing ### Testing Zustand Stores ```typescript // stores/__tests__/auth-store.test.ts import { describe, it, expect, beforeEach } from 'vitest'; import { useAuthStore } from '../auth-store'; describe('AuthStore', () => { beforeEach(() => { // Reset store before each test useAuthStore.setState({ user: null, isAuthenticated: false }); }); it('should login user', () => { const user = { id: '1', name: 'Test' }; useAuthStore.getState().login(user); expect(useAuthStore.getState().user).toEqual(user); expect(useAuthStore.getState().isAuthenticated).toBe(true); }); it('should logout user', () => { useAuthStore.setState({ user: { id: '1' }, isAuthenticated: true }); useAuthStore.getState().logout(); expect(useAuthStore.getState().user).toBeNull(); expect(useAuthStore.getState().isAuthenticated).toBe(false); }); }); ``` ## Common Mistakes / Lỗi Thường Gặp ### 1. Subscribing to Entire Store ```typescript // ❌ BAD: Component re-renders on any store change const store = useAuthStore(); // ✅ GOOD: Select only needed data const userName = useAuthStore((s) => s.user?.name); ``` ### 2. Missing Query Key Invalidation ```typescript // ❌ BAD: Cache becomes stale after mutation useMutation({ mutationFn: updateUser }); // ✅ GOOD: Invalidate related queries useMutation({ mutationFn: updateUser, onSuccess: () => queryClient.invalidateQueries({ queryKey: userKeys.all }), }); ``` ### 3. Creating QueryClient in Component Body ```tsx // ❌ BAD: New client on every render function App() { const queryClient = new QueryClient(); // Recreated every render! } // ✅ GOOD: Use useState or module scope function App() { const [queryClient] = useState(() => new QueryClient()); } ``` ## Quick Reference / Tham Chiếu Nhanh | Pattern | When to Use | |---------|-------------| | `create()` | Basic Zustand store | | `devtools()` | Enable Redux DevTools | | `persist()` | Persist to localStorage | | `useQuery()` | Fetch & cache server data | | `useMutation()` | Create/Update/Delete operations | | `queryClient.invalidateQueries()` | Refresh cache after mutation | | `queryClient.setQueryData()` | Optimistic updates | ## Resources / Tài Nguyên - [React Enterprise Architect](../react-enterprise-architect/SKILL.md) - Architecture patterns - [React Testing Patterns](../react-testing-patterns/SKILL.md) - Testing strategies - [Zustand Docs](https://zustand-demo.pmnd.rs/) - [TanStack Query Docs](https://tanstack.com/query/latest)