Files
pos-system/microservices/.agent/skills/react-state-management/SKILL.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

8.2 KiB

name, description, compatibility, metadata
name description compatibility metadata
react-state-management State management patterns với Zustand và TanStack Query cho React/Next.js. Use for client state, server state, caching, và optimistic updates. zustand>=5, @tanstack/react-query>=5, react>=19
author version
Velik Ho 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

// 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<AuthState>()(
  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)

// ✅ 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 <Avatar src={avatar} />;
}

// ❌ BAD: Subscribes to entire store
function UserAvatar() {
  const { user } = useAuthStore(); // Re-renders on any store change
  return <Avatar src={user?.avatar} />;
}

TanStack Query Pattern

// 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

/**
 * 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

// 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 (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

Testing Patterns / Mẫu Testing

Testing Zustand Stores

// 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

// ❌ 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

// ❌ 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

// ❌ 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