Việc truyền props qua nhiều tầng component không liên quan là một công việc cực kỳ tẻ nhạt. Hôm nay bạn đang triển khai một tính năng, nhưng ngày mai bạn lại phải chỉnh sửa sáu tệp tin chỉ để đổi tên một prop duy nhất. Đó chính là định nghĩa ngắn gọn về prop drilling: một component cha có dữ liệu, một component con ở sâu bên dưới cần nó, và mọi component nằm giữa chúng đều trở thành những "người đưa thư". Ứng dụng vẫn chạy, nhưng mã nguồn trở nên mong manh. Chỉ cần xóa một lớp trung gian, một nửa cây component sẽ sụp đổ. Thay đổi một kiểu dữ liệu (type), và TypeScript sẽ báo lỗi tràn lan qua ba thư mục khác nhau. React Context API ra đời để loại bỏ hoàn toàn những thành phần trung gian đó.
Prop Drilling Thực Sự Trông Như Thế Nào
Hãy tưởng tượng một khung ứng dụng tiêu chuẩn. Bạn có một component App thực hiện fetch thông tin người dùng hiện tại. Bên trong App là một Layout, bên trong Layout là một Sidebar, bên trong Sidebar lồng một Navigation, và cuối cùng bên trong Navigation bạn tìm thấy UserAvatar — thành phần thực sự cần đối tượng người dùng (user object).
Mã nguồn của bạn sẽ trông như thế này:
function App() {
const user = { name: 'Aarav', role: 'admin' };
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <Navigation user={user} />;
}
function Navigation({ user }) {
return <UserAvatar user={user} />;
}
Layout, Sidebar, và Navigation không làm gì với đối tượng user đó ngoại trừ việc truyền nó xuống dưới. Chúng tích tụ những props mà chúng không hề sở hữu, các interface trở nên cồng kềnh, và việc kiểm thử (testing) chúng đòi hỏi phải giả lập (mocking) những dữ liệu mà chúng thậm chí không bao giờ chạm tới. Vấn đề thực sự nằm ở chỗ nó lan rộng nhanh đến mức nào. Chỉ cần thêm một cờ isLoggedIn, một chuỗi locale, hoặc một giá trị theme, và cùng một kịch bản đó sẽ lặp lại.
Context API Thay Đổi Cuộc Chơi Như Thế Nào
Hãy coi Context API giống như một bộ định tuyến WiFi. Thay vì phải chạy dây cáp dài qua mọi căn phòng để kết nối tới từng thiết bị, bộ định tuyến sẽ gửi tín hiệu qua không trung. Bất kỳ thiết bị nào trong phạm vi phủ sóng đều có thể kết nối trực tiếp. Theo thuật ngữ của React, bộ định tuyến là Provider, tín hiệu là state hoặc dữ liệu của bạn, và thiết bị là bất kỳ component lồng nhau nào gọi useContext.
Việc thiết lập bao gồm ba thành phần chính:
React.createContext()xây dựng kênh truyền dữ liệu.- Provider bao bọc một phần cây component của bạn và truyền đi một giá trị.
- Hook
useContextcho phép các component con nhận giá trị đó mà không cần chạm vào các props trung gian.
Bạn vẫn có một cấu trúc cây, nhưng các nhánh giữa gốc (root) và lá (leaf) không còn cần phải ký kết "hợp đồng vận chuyển" nữa.
Xây Dựng Một Context Từ Đầu
Hãy cùng xây dựng một ví dụ cụ thể với các thiết lập giao diện (theme), vì hầu hết các ứng dụng đều cần chế độ sáng hoặc tối vào một thời điểm nào đó.
Đầu tiên, tạo đối tượng context. Đây chính là đường ống dẫn:
import { createContext, useState, useMemo } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export default ThemeContext;
Sau đó, bao bọc ứng dụng của bạn trong provider. Thông thường việc này diễn ra gần gốc (root):
import { ThemeProvider } from './ThemeContext';
function App() {
return (
<ThemeProvider>
<Layout />
</ThemeProvider>
);
}
Giờ đây, bất kỳ component con nào cũng có thể trực tiếp bắt lấy tín hiệu. Đây là một nút chuyển đổi (toggle button) nằm sâu bên trong giao diện:
import { useContext } from 'react';
import ThemeContext from './ThemeContext';
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
onClick={() => setTheme(prev => prev === 'light' ? 'dark' : 'light')}
>
Current theme: {theme}
</button>
);
}
Lưu ý rằng Layout, Sidebar, và Navigation không bao giờ nhìn thấy prop theme. Chúng render như bình thường, và ThemeToggle lấy những gì nó cần trực tiếp từ context. Sự kết nối này là vô hình từ bên ngoài, và đó chính xác là mục đích của nó.
Khi Nào Nên Sử Dụng Context
Context hoạt động tốt nhất cho những dữ liệu mà nhiều component ở xa cùng chia sẻ nhưng không có một component cha duy nhất nào quản lý một cách gọn gàng. Các ứng cử viên sáng giá bao gồm:
- Thiết lập giao diện (Theme settings) như chế độ sáng/tối, màu nhấn (accent colors), hoặc tỉ lệ phông chữ.
- Trạng thái xác thực (Authentication state) như đối tượng người dùng hiện tại, trạng thái đăng nhập, hoặc thời gian hết hạn phiên làm việc.
- Ngôn ngữ và khu vực (Language and locale) cho việc đa ngôn ngữ hóa.
- Dữ liệu giỏ hàng (Shopping cart data) cần phải được đồng bộ hóa giữa badge trên header, menu thả xuống của giỏ hàng mini và trang thanh toán.
Hãy kiềm chế ham muốn đổ mọi mảnh state cục bộ vào Context. Một ô nhập liệu (form input) ở hai tầng bên dưới không cần một sự phát sóng toàn cầu. Hãy giữ Context cho các vấn đề mang tính xuyên suốt (cross-cutting concerns) thực sự, và để phần còn lại là các props thông thường.
Bẫy Hiệu Suất Và Cách Tránh
Context không phải là miễn phí. Khi một giá trị context thay đổi, mọi component đang kết nối với context đó đều sẽ re-render, ngay cả khi phần giá trị mà chúng quan tâm không hề thay đổi. Sai lầm kinh điển là ném một object literal mới vào Provider trong mỗi lần component cha re-render.
Trong ví dụ về theme của chúng ta, mỗi khi ThemeProvider re-render vì component cha của chính nó cập nhật, biểu thức { theme, setTheme } sẽ tạo ra một đối tượng hoàn toàn mới. React nhận thấy một tham chiếu (reference) mới, và mọi consumer đều bị cập nhật. Nếu theme của bạn hiếm khi thay đổi nhưng state của ứng dụng lại thay đổi thường xuyên, bạn đang phải trả giá bằng những lần re-render không cần thiết.
Cách khắc phục gồm hai bước:
Chia nhỏ các context theo tần suất cập nhật. Một UserContext chỉ thay đổi một lần khi đăng nhập không nên dùng chung một provider với NotificationContext vốn cập nhật vài giây một lần. Hãy giữ chúng tách biệt để dữ liệu tĩnh không phải "đi nhờ" trên cùng một chuyến tàu re-render với dữ liệu biến động.
Bọc giá trị trong useMemo khi giá trị đó là một object hoặc array. Hãy cung cấp cho React một tham chiếu ổn định:
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
Now the object identity only shifts when theme actually changes. Descendant components that care about the context but are shielded by React.memo further down will skip the work.
Mistakes That Waste Your Time
The two errors that still show up in production code are easy to prevent.
First, forgetting to export the context itself. If you only export the Provider wrapper and keep the context object private, a developer writing a new feature cannot call useContext without refactoring your module. Export the context so consumers can import both the provider and the consumer hook cleanly.
Second, calling useContext outside the corresponding Provider. If ThemeToggle renders in a branch of the tree that is not wrapped in ThemeProvider, the hook returns the default value passed to createContext, or undefined if you passed none. This leads to silent failures like cannot read property of undefined. You can guard against this by assigning a sensible default or throwing a clear error early in the hook call.
Context vs Redux: Keep It Simple
You do not always need Redux. For medium-sized projects, Context paired with useState or useReducer covers the bulk of state sharing. Redux shines when you need time-travel debugging, complex middleware, or global transactions that must roll back in sequence. If your entire state story is a user object, a theme string, and a cart array, a store library adds boilerplate you will never leverage.
That said, Context is not a full state management system on its own. It does not give you a single global snapshot, and it does not batch updates across unrelated contexts. Use it as a replacement for prop drilling, not as an operating system for your entire data layer.
The Real Takeaway
Stop threading props through components that do not care about them. Create focused contexts for the data that actually spans your tree, wrap providers high enough to cover the consumers, and always stabilize the value object when you are passing collections or functions. Context API keeps yourReact code direct: props stay local, global data travels wirelessly, and your component boundaries stay clean.
