EngineeringSenior DeveloperTechnical Sessions

Session 07: React Advanced Patterns & Performance

Mục tiêu: Master React hooks, render optimization, state management, và advanced component patterns cho enterprise apps.


1. Hooks Deep Dive

useCallback & useMemo - Khi Nào Thực Sự Cần

tsx
// ❌ Over-optimization: useMemo/useCallback KHÔNG CẦN ở đây
const Component = ({ items }: { items: Item[] }) => {
  // ❌ items.length tính nhanh, không cần useMemo
  const count = useMemo(() => items.length, [items]);

  // ❌ simple handler, không truyền xuống child memoized
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return <div onClick={handleClick}>{count}</div>;
};

// ✅ CẦN useMemo: expensive computation
const Analytics = ({ orders }: { orders: Order[] }) => {
  const stats = useMemo(() => ({
    total: orders.reduce((sum, o) => sum + o.total, 0),
    avgValue: orders.reduce((sum, o) => sum + o.total, 0) / orders.length,
    byCategory: Object.groupBy(orders, o => o.category),
    topProducts: computeTopProducts(orders), // O(n log n) sorting
  }), [orders]);

  return <StatsPanel stats={stats} />;
};

// ✅ CẦN useCallback: truyền handler xuống memoized child
const OrderList = ({ orders }: { orders: Order[] }) => {
  const handleStatusChange = useCallback((id: string, status: OrderStatus) => {
    // API call to update status
    updateOrderStatus(id, status);
  }, []); // Stable reference để OrderRow không re-render

  return (
    <div>
      {orders.map(order => (
        <MemoizedOrderRow
          key={order.id}
          order={order}
          onStatusChange={handleStatusChange}
        />
      ))}
    </div>
  );
};

const MemoizedOrderRow = React.memo(OrderRow);

useRef - Beyond DOM References

tsx
// 1. Previous value tracking
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T | undefined>(undefined);
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

// 2. Interval management
function useInterval(callback: () => void, delay: number | null) {
  const savedCallback = useRef(callback);

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    if (delay === null) return;
    const id = setInterval(() => savedCallback.current(), delay);
    return () => clearInterval(id);
  }, [delay]);
}

// 3. Mutable value không trigger re-render
function useRenderCount() {
  const renderCount = useRef(0);
  renderCount.current++;
  return renderCount.current;
}

useTransition & useDeferredValue (React 18+)

tsx
// useTransition: mark state update as non-urgent
const ProductSearch = () => {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleSearch = (e: ChangeEvent<HTMLInputElement>) => {
    // Urgent: update input immediately
    setQuery(e.target.value);

    // Non-urgent: defer heavy filtering
    startTransition(() => {
      setFilteredProducts(filterProducts(e.target.value));
    });
  };

  return (
    <div>
      <input value={query} onChange={handleSearch} />
      {isPending ? <Spinner /> : <ProductGrid products={filteredProducts} />}
    </div>
  );
};

// useDeferredValue: defer expensive rendering
const SearchResults = ({ query }: { query: string }) => {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  const results = useMemo(
    () => heavySearch(deferredQuery),
    [deferredQuery]
  );

  return (
    <div style={{ opacity: isStale ? 0.7 : 1 }}>
      {results.map(r => <ResultCard key={r.id} result={r} />)}
    </div>
  );
};

2. Custom Hooks cho Enterprise Apps

tsx
// 1. useApi: generic data fetching hook
interface UseApiOptions<T> {
  initialData?: T;
  enabled?: boolean;
}

function useApi<T>(
  url: string,
  options: UseApiOptions<T> = {}
): {
  data: T | undefined;
  error: Error | null;
  isLoading: boolean;
  refetch: () => Promise<void>;
} {
  const [data, setData] = useState<T | undefined>(options.initialData);
  const [error, setError] = useState<Error | null>(null);
  const [isLoading, setIsLoading] = useState(false);

  const fetchData = useCallback(async () => {
    setIsLoading(true);
    setError(null);
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const json = await response.json();
      setData(json);
    } catch (e) {
      setError(e instanceof Error ? e : new Error(String(e)));
    } finally {
      setIsLoading(false);
    }
  }, [url]);

  useEffect(() => {
    if (options.enabled !== false) fetchData();
  }, [fetchData, options.enabled]);

  return { data, error, isLoading, refetch: fetchData };
}

// 2. useDebounce
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// 3. useLocalStorage - persist state
function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue] as const;
}

// 4. useMediaQuery
function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(
    () => window.matchMedia(query).matches
  );

  useEffect(() => {
    const mql = window.matchMedia(query);
    const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
    mql.addEventListener('change', handler);
    return () => mql.removeEventListener('change', handler);
  }, [query]);

  return matches;
}

3. Render Optimization

React.memo - Shallow Comparison

tsx
// React.memo: skip re-render nếu props không đổi
const OrderRow = React.memo(({ order, onEdit }: OrderRowProps) => {
  console.log(`Rendering order ${order.id}`);
  return (
    <tr>
      <td>{order.id}</td>
      <td>{order.total}</td>
      <td><button onClick={() => onEdit(order.id)}>Edit</button></td>
    </tr>
  );
});

// Custom comparison function
const ExpensiveChart = React.memo(
  ({ data, config }: ChartProps) => {
    return <HighchartsReact options={buildChartOptions(data, config)} />;
  },
  (prev, next) => {
    // Only re-render khi data thực sự thay đổi
    return prev.data.length === next.data.length &&
           prev.data.every((d, i) => d.value === next.data[i].value);
  }
);

React.lazy & Suspense - Code Splitting

tsx
// Lazy load heavy components
const AdminDashboard = React.lazy(() => import('./pages/AdminDashboard'));
const ReportViewer = React.lazy(() => import('./pages/ReportViewer'));

// Route-based splitting
function App() {
  return (
    <Suspense fallback={<FullPageSpinner />}>
      <Routes>
        <Route path="/admin" element={<AdminDashboard />} />
        <Route path="/reports" element={<ReportViewer />} />
      </Routes>
    </Suspense>
  );
}

// Preload on hover (smart prefetching)
const AdminLink = () => {
  const handleMouseEnter = () => {
    // Start loading chunk khi user hover
    import('./pages/AdminDashboard');
  };

  return (
    <Link to="/admin" onMouseEnter={handleMouseEnter}>
      Admin
    </Link>
  );
};

4. State Management Comparison

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

interface OrderStore {
  orders: Order[];
  selectedOrderId: string | null;
  isLoading: boolean;
  // Actions
  fetchOrders: () => Promise<void>;
  selectOrder: (id: string) => void;
  updateOrderStatus: (id: string, status: OrderStatus) => Promise<void>;
}

const useOrderStore = create<OrderStore>()(
  devtools(
    persist(
      (set, get) => ({
        orders: [],
        selectedOrderId: null,
        isLoading: false,

        fetchOrders: async () => {
          set({ isLoading: true });
          const orders = await api.getOrders();
          set({ orders, isLoading: false });
        },

        selectOrder: (id) => set({ selectedOrderId: id }),

        updateOrderStatus: async (id, status) => {
          await api.updateOrder(id, { status });
          set({
            orders: get().orders.map(o =>
              o.id === id ? { ...o, status } : o
            ),
          });
        },
      }),
      { name: 'order-store' } // persist to localStorage
    )
  )
);

// Usage - chỉ subscribe fields cần thiết (auto selector optimization)
const OrderList = () => {
  const orders = useOrderStore(s => s.orders);
  const isLoading = useOrderStore(s => s.isLoading);
  const fetchOrders = useOrderStore(s => s.fetchOrders);

  useEffect(() => { fetchOrders(); }, [fetchOrders]);

  if (isLoading) return <Spinner />;
  return orders.map(o => <OrderCard key={o.id} order={o} />);
};

TanStack Query (React Query) - Server State Management

tsx
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// Fetching
const useOrders = (filters: OrderFilters) => {
  return useQuery({
    queryKey: ['orders', filters],
    queryFn: () => api.getOrders(filters),
    staleTime: 5 * 60 * 1000,      // 5 min: data considered fresh
    gcTime: 30 * 60 * 1000,        // 30 min: cache garbage collection
    retry: 3,
    refetchOnWindowFocus: true,
  });
};

// Mutation with optimistic update
const useUpdateOrder = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: { id: string; status: OrderStatus }) =>
      api.updateOrder(data.id, data),

    // Optimistic update
    onMutate: async (newData) => {
      await queryClient.cancelQueries({ queryKey: ['orders'] });
      const previousOrders = queryClient.getQueryData(['orders']);

      queryClient.setQueryData(['orders'], (old: Order[]) =>
        old.map(o => o.id === newData.id ? { ...o, ...newData } : o)
      );

      return { previousOrders };
    },

    onError: (_err, _vars, context) => {
      // Rollback on error
      queryClient.setQueryData(['orders'], context?.previousOrders);
    },

    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['orders'] });
    },
  });
};

// Sử dụng
const OrderPage = () => {
  const { data: orders, isLoading, error } = useOrders({ status: 'active' });
  const updateOrder = useUpdateOrder();

  if (isLoading) return <Skeleton />;
  if (error) return <ErrorMessage error={error} />;

  return orders.map(order => (
    <OrderCard
      key={order.id}
      order={order}
      onStatusChange={(status) =>
        updateOrder.mutate({ id: order.id, status })
      }
    />
  ));
};

Khi Nào Chọn Gì?

Use caseZustandTanStack QueryRedux Toolkit
Server state (API data)⚠️ Manual✅ Best⚠️ RTK Query
Client state (UI)✅ Best❌✅ OK
Complex state logic✅ Middleware❌✅ Reducers
BoilerplateÍtRất ítNhiều
Learning curveThấpThấpCao
DevTools✅✅✅
🎯 Recommendation cho .NET + React projects:
- Server state → TanStack Query (caching, sync, optimistic updates)
- Client state → Zustand (UI state, user preferences)
- Dùng cả 2 cùng lúc, chúng complement nhau hoàn hảo

5. Advanced Component Patterns

Compound Components

tsx
// Pattern: components hoạt động cùng nhau qua Context
interface TabsContextType {
  activeTab: string;
  setActiveTab: (tab: string) => void;
}

const TabsContext = createContext<TabsContextType | null>(null);

const Tabs = ({ children, defaultTab }: { children: ReactNode; defaultTab: string }) => {
  const [activeTab, setActiveTab] = useState(defaultTab);
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
};

Tabs.List = ({ children }: { children: ReactNode }) => (
  <div role="tablist" className="tabs-list">{children}</div>
);

Tabs.Trigger = ({ value, children }: { value: string; children: ReactNode }) => {
  const ctx = useContext(TabsContext)!;
  return (
    <button
      role="tab"
      aria-selected={ctx.activeTab === value}
      onClick={() => ctx.setActiveTab(value)}
      className={ctx.activeTab === value ? 'active' : ''}
    >
      {children}
    </button>
  );
};

Tabs.Content = ({ value, children }: { value: string; children: ReactNode }) => {
  const ctx = useContext(TabsContext)!;
  if (ctx.activeTab !== value) return null;
  return <div role="tabpanel">{children}</div>;
};

// Usage - clean, readable API
<Tabs defaultTab="orders">
  <Tabs.List>
    <Tabs.Trigger value="orders">Orders</Tabs.Trigger>
    <Tabs.Trigger value="products">Products</Tabs.Trigger>
    <Tabs.Trigger value="analytics">Analytics</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="orders"><OrderList /></Tabs.Content>
  <Tabs.Content value="products"><ProductList /></Tabs.Content>
  <Tabs.Content value="analytics"><AnalyticsDashboard /></Tabs.Content>
</Tabs>

Error Boundaries

tsx
class ErrorBoundary extends Component<
  { children: ReactNode; fallback: ReactNode },
  { hasError: boolean; error?: Error }
> {
  state = { hasError: false, error: undefined };

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    // Log to monitoring service
    logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

// Usage
<ErrorBoundary fallback={<ErrorPage />}>
  <OrderDashboard />
</ErrorBoundary>

Tổng Kết Session 07

ConceptKey Takeaway
useMemo/useCallbackChỉ dùng khi truyền xuống memoized child hoặc expensive computation
useTransitionDefer non-urgent state updates, giữ UI responsive
Custom HooksExtract shared logic, compose small hooks
ZustandClient state: simple, minimal boilerplate
TanStack QueryServer state: caching, sync, optimistic updates
Compound ComponentsClean API cho complex UI components

Bài tập: Refactor 1 component nặng: apply React.memo, useCallback, và useMemo. Đo re-render count trước/sau bằng React DevTools Profiler.