Back to blog

Deep Dive: React với TypeScript Best Practices

typescriptreactfrontendbest-practicesprogramming
Deep Dive: React với TypeScript Best Practices

Chào Mừng Đến Deep Dive

Bạn đã biết TypeScript. Bạn đã biết React. Nhưng kết hợp chúng hiệu quả đòi hỏi hiểu các pattern mà cả hai đều không dạy riêng lẻ. Bài này covers các pattern đã được test trong production để xây dựng ứng dụng React type-safe — từ component props đến hooks đến context đến performance.

Yêu cầu trước: Phase 2: Frontend Development Thời gian: 2-3 giờ (làm theo các ví dụ)

Bạn Sẽ Học Được

✅ Component prop patterns (children, discriminated unions, polymorphic components)
✅ Hooks typing (useState, useRef, custom hooks với generics)
✅ Context API với full type safety
✅ Render props và compound components
✅ Higher-Order Components (HOCs) với proper types
✅ Event handlers và form typing
✅ Ref forwarding và imperative handles
✅ Performance optimization (memo, useMemo, useCallback)
✅ Error boundaries với TypeScript
✅ Suspense và lazy loading types


1. Component Prop Patterns

Basic Props với Interface

interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
  variant?: "primary" | "secondary" | "danger";
}
 
function Button({
  label,
  onClick,
  disabled = false,
  variant = "primary"
}: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {label}
    </button>
  );
}

Children Props

Có nhiều cách type children — dùng đúng cách cho nhu cầu của bạn:

import { ReactNode, PropsWithChildren } from "react";
 
// Option 1: Explicit ReactNode (phổ biến nhất)
interface CardProps {
  title: string;
  children: ReactNode;
}
 
function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}
 
// Option 2: PropsWithChildren utility
interface PanelProps {
  bordered?: boolean;
}
 
function Panel({ bordered, children }: PropsWithChildren<PanelProps>) {
  return (
    <div className={bordered ? "border" : ""}>
      {children}
    </div>
  );
}
 
// Option 3: Render prop pattern (function as child)
interface DataFetcherProps<T> {
  url: string;
  children: (data: T, loading: boolean) => ReactNode;
}
 
function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
  const { data, loading } = useFetch<T>(url);
  return <>{children(data as T, loading)}</>;
}

Discriminated Union Props

Khi props là mutually exclusive, dùng discriminated unions:

// Xấu: cho phép combinations không hợp lệ
interface BadAlertProps {
  type: "success" | "error" | "loading";
  message?: string;
  progress?: number;
}
 
// Tốt: discriminated union enforce valid combinations
type AlertProps =
  | { type: "success"; message: string }
  | { type: "error"; message: string; onRetry?: () => void }
  | { type: "loading"; progress: number };
 
function Alert(props: AlertProps) {
  switch (props.type) {
    case "success":
      return <div className="alert-success">{props.message}</div>;
    case "error":
      return (
        <div className="alert-error">
          {props.message}
          {props.onRetry && <button onClick={props.onRetry}>Retry</button>}
        </div>
      );
    case "loading":
      return <div className="alert-loading">Loading... {props.progress}%</div>;
  }
}
 
// Sử dụng
<Alert type="success" message="Đã lưu!" />
<Alert type="error" message="Thất bại" onRetry={() => retry()} />
<Alert type="loading" progress={75} />
// <Alert type="success" progress={50} /> // Lỗi! 'message' là required

Polymorphic Components (Render As)

Cho phép components render dưới dạng các HTML elements khác nhau:

import { ElementType, ComponentPropsWithoutRef } from "react";
 
type PolymorphicProps<E extends ElementType> = {
  as?: E;
} & ComponentPropsWithoutRef<E>;
 
function Box<E extends ElementType = "div">({
  as,
  children,
  ...props
}: PolymorphicProps<E> & { children?: ReactNode }) {
  const Component = as || "div";
  return <Component {...props}>{children}</Component>;
}
 
// Sử dụng
<Box>Default div</Box>
<Box as="section">Render dưới dạng section</Box>
<Box as="a" href="/about">Render dưới dạng anchor</Box>
<Box as="button" onClick={() => alert("clicked")}>Render dưới dạng button</Box>

2. Hooks Typing

useState

import { useState } from "react";
 
// Type được infer (string)
const [name, setName] = useState("");
 
// Explicit type cho complex state
interface User {
  id: string;
  name: string;
  email: string;
}
 
const [user, setUser] = useState<User | null>(null);
 
// Union type cho loading states
type Status = "idle" | "loading" | "success" | "error";
const [status, setStatus] = useState<Status>("idle");

useRef

import { useRef, useEffect } from "react";
 
// DOM element ref — ban đầu null, sẽ được assign
const inputRef = useRef<HTMLInputElement>(null);
 
useEffect(() => {
  // TypeScript biết inputRef.current có thể là null
  inputRef.current?.focus();
}, []);
 
// Mutable value ref — không null, value persist
const countRef = useRef<number>(0);
countRef.current += 1; // OK, luôn được define
 
// Timer/interval ref
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

Phân biệt quan trọng:

  • useRef<T>(null) — cho DOM refs, .current là T | null
  • useRef<T>(initialValue) — cho mutable values, .current luôn là T

Custom Hooks với Generics

import { useState, useEffect } from "react";
 
// Generic data fetching hook
function useFetch<T>(url: string): {
  data: T | null;
  loading: boolean;
  error: Error | null;
} {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
 
  useEffect(() => {
    const controller = new AbortController();
 
    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const json = await response.json();
        setData(json as T);
        setError(null);
      } catch (e) {
        if (e instanceof Error && e.name !== "AbortError") {
          setError(e);
        }
      } finally {
        setLoading(false);
      }
    }
 
    fetchData();
    return () => controller.abort();
  }, [url]);
 
  return { data, loading, error };
}
 
// Sử dụng — type được infer!
interface User {
  id: string;
  name: string;
}
 
function UserProfile({ userId }: { userId: string }) {
  const { data: user, loading, error } = useFetch<User>(`/api/users/${userId}`);
 
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return null;
 
  return <div>{user.name}</div>;
}

3. Context API với TypeScript

Basic Typed Context

import { createContext, useContext, useState, ReactNode } from "react";
 
// Define context shape
interface AuthContextType {
  user: User | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  isAuthenticated: boolean;
}
 
interface User {
  id: string;
  name: string;
  email: string;
}
 
// Tạo context với undefined default
const AuthContext = createContext<AuthContextType | undefined>(undefined);
 
// Provider component
function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
 
  const login = async (email: string, password: string) => {
    const response = await fetch("/api/login", {
      method: "POST",
      body: JSON.stringify({ email, password }),
    });
    const userData = await response.json();
    setUser(userData);
  };
 
  const logout = () => setUser(null);
 
  const value: AuthContextType = {
    user,
    login,
    logout,
    isAuthenticated: user !== null,
  };
 
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
 
// Custom hook với type guard
function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error("useAuth phải được dùng trong AuthProvider");
  }
  return context;
}
 
// Sử dụng trong component
function Profile() {
  const { user, logout, isAuthenticated } = useAuth();
 
  if (!isAuthenticated) {
    return <div>Vui lòng đăng nhập</div>;
  }
 
  return (
    <div>
      <p>Xin chào, {user?.name}</p>
      <button onClick={logout}>Đăng xuất</button>
    </div>
  );
}

Generic Context Factory

Tạo context patterns có thể tái sử dụng:

import { createContext, useContext, ReactNode } from "react";
 
function createSafeContext<T>(displayName: string) {
  const Context = createContext<T | undefined>(undefined);
  Context.displayName = displayName;
 
  function useContextSafe(): T {
    const context = useContext(Context);
    if (context === undefined) {
      throw new Error(
        `use${displayName} phải được dùng trong ${displayName}Provider`
      );
    }
    return context;
  }
 
  return [Context.Provider, useContextSafe] as const;
}
 
// Sử dụng
interface ThemeContextType {
  theme: "light" | "dark";
  toggleTheme: () => void;
}
 
const [ThemeProvider, useTheme] = createSafeContext<ThemeContextType>("Theme");

4. Render Props và Compound Components

Compound Components Pattern

Tạo components làm việc cùng nhau và chia sẻ state ngầm định:

import { createContext, useContext, useState, ReactNode } from "react";
 
// Types
interface TabsContextType {
  activeTab: string;
  setActiveTab: (id: string) => void;
}
 
// Context
const TabsContext = createContext<TabsContextType | undefined>(undefined);
 
function useTabs() {
  const context = useContext(TabsContext);
  if (!context) throw new Error("Tab components phải được dùng trong Tabs");
  return context;
}
 
// Parent component
function Tabs({ defaultTab, children }: { defaultTab: string; children: ReactNode }) {
  const [activeTab, setActiveTab] = useState(defaultTab);
 
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}
 
// Tab button
function Tab({ id, children }: { id: string; children: ReactNode }) {
  const { activeTab, setActiveTab } = useTabs();
  const isActive = activeTab === id;
 
  return (
    <button
      className={isActive ? "tab active" : "tab"}
      onClick={() => setActiveTab(id)}
    >
      {children}
    </button>
  );
}
 
// Tab panel
function TabPanel({ id, children }: { id: string; children: ReactNode }) {
  const { activeTab } = useTabs();
  if (activeTab !== id) return null;
  return <div className="tab-panel">{children}</div>;
}
 
// Attach sub-components
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
 
// Sử dụng — API clean!
function Settings() {
  return (
    <Tabs defaultTab="profile">
      <Tabs.Tab id="profile">Hồ sơ</Tabs.Tab>
      <Tabs.Tab id="security">Bảo mật</Tabs.Tab>
 
      <Tabs.Panel id="profile">
        <ProfileSettings />
      </Tabs.Panel>
      <Tabs.Panel id="security">
        <SecuritySettings />
      </Tabs.Panel>
    </Tabs>
  );
}

5. Higher-Order Components (HOCs)

HOCs ít phổ biến hơn trong React hiện đại (hooks thường thay thế chúng), nhưng vẫn hữu ích cho cross-cutting concerns.

import { ComponentType } from "react";
 
// HOC thêm loading prop
interface WithLoadingProps {
  isLoading: boolean;
}
 
function withLoading<P extends object>(
  WrappedComponent: ComponentType<P>
): ComponentType<P & WithLoadingProps> {
  return function WithLoadingComponent({
    isLoading,
    ...props
  }: P & WithLoadingProps) {
    if (isLoading) {
      return <div className="spinner">Đang tải...</div>;
    }
    return <WrappedComponent {...(props as P)} />;
  };
}
 
// Sử dụng
interface UserListProps {
  users: User[];
}
 
function UserList({ users }: UserListProps) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
 
const UserListWithLoading = withLoading(UserList);
 
// Component giờ yêu cầu isLoading prop
<UserListWithLoading users={users} isLoading={loading} />

6. Event Handlers và Forms

Event Handler Types

import {
  MouseEvent,
  KeyboardEvent,
  ChangeEvent,
  FormEvent,
} from "react";
 
function EventExamples() {
  // Click handler
  const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
    e.preventDefault();
    console.log("Clicked:", e.currentTarget.name);
  };
 
  // Change handler
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    console.log("Value:", e.target.value);
  };
 
  // Form submit
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    console.log("Form data:", Object.fromEntries(formData));
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
      <button type="submit" onClick={handleClick}>Submit</button>
    </form>
  );
}

Typed Form với useState

import { useState, ChangeEvent, FormEvent } from "react";
 
interface LoginForm {
  email: string;
  password: string;
  rememberMe: boolean;
}
 
function LoginPage() {
  const [form, setForm] = useState<LoginForm>({
    email: "",
    password: "",
    rememberMe: false,
  });
 
  const [errors, setErrors] = useState<Partial<Record<keyof LoginForm, string>>>({});
 
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const { name, value, type, checked } = e.target;
    setForm((prev) => ({
      ...prev,
      [name]: type === "checkbox" ? checked : value,
    }));
  };
 
  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    // Validate và submit...
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        name="email"
        value={form.email}
        onChange={handleChange}
      />
      {errors.email && <span className="error">{errors.email}</span>}
      {/* ... other fields */}
    </form>
  );
}

7. Ref Forwarding và Imperative Handles

forwardRef với TypeScript

import { forwardRef, useRef, useImperativeHandle } from "react";
 
interface InputProps {
  label: string;
  error?: string;
}
 
const TextInput = forwardRef<HTMLInputElement, InputProps>(
  function TextInput({ label, error }, ref) {
    return (
      <div className="form-field">
        <label>{label}</label>
        <input ref={ref} />
        {error && <span className="error">{error}</span>}
      </div>
    );
  }
);

useImperativeHandle cho Custom APIs

Expose custom API thay vì raw DOM element:

import { forwardRef, useImperativeHandle, useState } from "react";
 
// Define imperative handle type
interface ModalHandle {
  open: () => void;
  close: () => void;
  toggle: () => void;
}
 
interface ModalProps {
  title: string;
  children: ReactNode;
}
 
const Modal = forwardRef<ModalHandle, ModalProps>(
  function Modal({ title, children }, ref) {
    const [isOpen, setIsOpen] = useState(false);
 
    // Expose custom API cho parent
    useImperativeHandle(ref, () => ({
      open: () => setIsOpen(true),
      close: () => setIsOpen(false),
      toggle: () => setIsOpen((prev) => !prev),
    }));
 
    if (!isOpen) return null;
 
    return (
      <div className="modal">
        <h2>{title}</h2>
        {children}
      </div>
    );
  }
);
 
// Sử dụng
function App() {
  const modalRef = useRef<ModalHandle>(null);
 
  return (
    <div>
      <button onClick={() => modalRef.current?.open()}>Mở Modal</button>
      <Modal ref={modalRef} title="Cài đặt">
        <p>Nội dung modal</p>
      </Modal>
    </div>
  );
}

8. Performance Optimization

React.memo với TypeScript

import { memo, useMemo, useCallback } from "react";
 
interface ExpensiveListProps {
  items: Item[];
  onItemClick: (id: string) => void;
  filter: string;
}
 
// memo ngăn re-render nếu props không đổi
const ExpensiveList = memo(function ExpensiveList({
  items,
  onItemClick,
  filter,
}: ExpensiveListProps) {
  const filteredItems = useMemo(
    () => items.filter((item) => item.name.includes(filter)),
    [items, filter]
  );
 
  return (
    <ul>
      {filteredItems.map((item) => (
        <li key={item.id} onClick={() => onItemClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
});

useMemo và useCallback

import { useMemo, useCallback, useState } from "react";
 
function ProductList({ products }: { products: Product[] }) {
  const [filter, setFilter] = useState("");
 
  // Memoize expensive computation
  const filteredProducts = useMemo(() => {
    return products.filter((p) =>
      p.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [products, filter]);
 
  // Memoize callback để ngăn child re-renders
  const handleProductClick = useCallback((id: string) => {
    console.log("Clicked:", id);
  }, []);
 
  return (
    <div>
      <input value={filter} onChange={(e) => setFilter(e.target.value)} />
      <ProductGrid
        products={filteredProducts}
        onProductClick={handleProductClick}
      />
    </div>
  );
}

9. Error Boundaries

Error boundaries bắt JavaScript errors trong child components. Chúng phải là class components trong React.

import { Component, ReactNode, ErrorInfo } from "react";
 
interface ErrorBoundaryProps {
  children: ReactNode;
  fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
}
 
interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
}
 
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false, error: null };
  }
 
  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }
 
  componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
    console.error("Error caught:", error, errorInfo);
  }
 
  reset = () => {
    this.setState({ hasError: false, error: null });
  };
 
  render() {
    if (this.state.hasError && this.state.error) {
      const { fallback } = this.props;
 
      if (typeof fallback === "function") {
        return fallback(this.state.error, this.reset);
      }
 
      return fallback || <div>Đã xảy ra lỗi</div>;
    }
 
    return this.props.children;
  }
}
 
// Sử dụng
<ErrorBoundary
  fallback={(error, reset) => (
    <div>
      <h1>Oops! Đã xảy ra lỗi</h1>
      <p>{error.message}</p>
      <button onClick={reset}>Thử lại</button>
    </div>
  )}
>
  <MainContent />
</ErrorBoundary>

10. Suspense và Lazy Loading

React.lazy với TypeScript

import { lazy, Suspense } from "react";
 
// Basic lazy loading
const Dashboard = lazy(() => import("./Dashboard"));
const Settings = lazy(() => import("./Settings"));
 
function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

Named Exports với Lazy

// Cho named exports, cần wrapper
const LazyChart = lazy(() =>
  import("./Charts").then((module) => ({
    default: module.BarChart,
  }))
);

Cheat Sheet Tổng Hợp

Props Types

// Basic props
interface Props {
  required: string;
  optional?: number;
}
 
// Children
import { ReactNode, PropsWithChildren } from "react";
interface PropsWithKids { children: ReactNode; }
 
// Extending HTML elements
import { ComponentPropsWithoutRef } from "react";
interface ButtonProps extends ComponentPropsWithoutRef<"button"> {
  variant: "primary" | "secondary";
}

Hook Types

// useState
const [user, setUser] = useState<User | null>(null);
 
// useRef - DOM
const inputRef = useRef<HTMLInputElement>(null);
 
// useRef - mutable
const countRef = useRef<number>(0);

Event Types

import { MouseEvent, ChangeEvent, FormEvent } from "react";
 
const onClick = (e: MouseEvent<HTMLButtonElement>) => {};
const onChange = (e: ChangeEvent<HTMLInputElement>) => {};
const onSubmit = (e: FormEvent<HTMLFormElement>) => {};

Tóm Tắt và Key Takeaways

✅ Props patterns: Dùng discriminated unions cho mutually exclusive props
✅ Children typing: ReactNode cho any renderable content
✅ Hooks: Explicit generics cho useState<T>, phân biệt DOM refs và mutable refs
✅ Context: Luôn throw trong custom hooks khi context undefined
✅ Forms: Generic form hooks tiết kiệm typing lặp lại
✅ forwardRef: Dùng useImperativeHandle để expose custom APIs
✅ Performance: memo, useMemo, useCallback cần stable references
✅ Error boundaries: Vẫn yêu cầu class components
✅ Lazy loading: Wrap với Suspense, handle named exports explicitly

Tiếp Theo

Tài Liệu Bổ Sung


Series: TypeScript Full-Stack Roadmap Trước: Deep Dive: Advanced TypeScript Types Tiếp: Deep Dive: Node.js API Development

Happy coding! 🚀

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.