Chuyển đến nội dung chính

Bài 5: Context API & State Management

Context API, provider pattern. Zustand cho global state. TanStack Query cho server state. So sánh Redux vs Zustand vs Jotai vs Recoil. State management strategies.

💻 Lập trình — Bài 5 Bài 5: Context API & State Management

React & Next.js: Từ Cơ bản đến Nâng cao

Phần 2: React Advanced & State Management

xdev.asia

1. Context API

import { createContext, useContext, useState } from 'react';

// 1. Create context with type
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | null>(null);

// 2. Custom hook (type-safe)
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// 3. Provider component
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 4. Usage
function Header() {
  const { theme, toggleTheme } = useTheme();
  return (
    <header className={theme}>
      <button onClick={toggleTheme}>Toggle: {theme}</button>
    </header>
  );
}

// App
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Main />
    </ThemeProvider>
  );
}

2. Zustand — Global State

npm install zustand
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

// Define store
interface CartStore {
  items: CartItem[];
  total: number;
  addItem: (item: CartItem) => void;
  removeItem: (id: number) => void;
  clearCart: () => void;
}

const useCartStore = create<CartStore>()(
  devtools(
    persist(
      (set, get) => ({
        items: [],
        total: 0,

        addItem: (item) => set((state) => ({
          items: [...state.items, item],
          total: state.total + item.price,
        })),

        removeItem: (id) => set((state) => {
          const item = state.items.find(i => i.id === id);
          return {
            items: state.items.filter(i => i.id !== id),
            total: state.total - (item?.price || 0),
          };
        }),

        clearCart: () => set({ items: [], total: 0 }),
      }),
      { name: 'cart-storage' } // localStorage key
    )
  )
);

// Usage — component tự động re-render khi state thay đổi
function CartBadge() {
  const itemCount = useCartStore(state => state.items.length); // Selector
  return <span>{itemCount}</span>;
}

function CartTotal() {
  const total = useCartStore(state => state.total);
  return <span>${total}</span>;
}

function AddButton({ product }: { product: Product }) {
  const addItem = useCartStore(state => state.addItem);
  return <button onClick={() => addItem(product)}>Add to Cart</button>;
}

3. TanStack Query — Server State

npm install @tanstack/react-query
import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// Setup
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 min
      gcTime: 10 * 60 * 1000,   // 10 min
      retry: 3,
      refetchOnWindowFocus: false,
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyApp />
    </QueryClientProvider>
  );
}

// Fetch data
function UserList() {
  const { data: users, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const res = await fetch('/api/users');
      if (!res.ok) throw new Error('Failed to fetch');
      return res.json() as Promise<User[]>;
    },
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {users?.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

// Mutation
function CreateUser() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (newUser: CreateUserInput) => {
      const res = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newUser),
      });
      return res.json();
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] }); // Refetch
    },
  });

  return (
    <button
      onClick={() => mutation.mutate({ name: 'New User', email: '[email protected]' })}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? 'Creating...' : 'Create User'}
    </button>
  );
}

4. So sánh State Management

LibraryTypeBundleLearningBest for
ContextBuilt-in0KBEasyTheme, auth, simple global state
ZustandClient state~1KBEasyGlobal client state, any size
Redux ToolkitClient state~11KBMediumLarge apps, complex state
JotaiAtomic~2KBEasyAtomic state, derived values
TanStack QueryServer state~12KBMediumAPI data, caching, sync

Strategy: Dùng Zustand cho client state + TanStack Query cho server state. Context chỉ cho theme/auth đơn giản.

Bài tiếp theo: React Patterns & Performance — HOC, compound components, React.memo, và performance optimization.