8.2 KiB
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 |
|
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
- React Enterprise Architect - Architecture patterns
- React Testing Patterns - Testing strategies
- Zustand Docs
- TanStack Query Docs