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à requiredPolymorphic 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,.currentlàT | nulluseRef<T>(initialValue)— cho mutable values,.currentluô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
- Deep Dive: Node.js API Development → — Type-safe backends với Express/Fastify
- Deep Dive: Database & ORMs → — Prisma và Drizzle ORM patterns
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.