Prop drilling là việc truyền một prop qua những component không dùng đến nó, chỉ để một component nằm sâu hơn trong cây đọc được. Ở đây user đi từ App qua Page và Sidebar để đến được Avatar, component duy nhất đọc nó.
Bốn tầng, và chỉ tầng cuối cùng quan tâm đến user. Console cho thấy Page và Sidebar chạy vì một giá trị mà chúng không hề hiển thị. Nhấn nút và chúng lại ghi log.
Vì sao nó gây khó
Hai hay ba tầng thì không sao. Prop drilling thành vấn đề khi cây lớn dần:
- Các component ở giữa mang những prop chúng không dùng.
PagevàSidebargiờ có propusertrong chữ ký hàm, nên đọc chúng là bạn phải tự hỏi prop đó để làm gì. - Mỗi thay đổi đều chạm đến mọi tầng. Thêm
user.avatarUrlhay một giá trị thứ hai nhưonLogout, bạn phải sửa từng component trên đường đi, không chỉ hai đầu. - Di chuyển một component làm đứt chuỗi. Chuyển
Avatarsang một nhánh khác là bạn phải luồnuserqua một đường mới. - Rất dễ đánh rơi một prop. Quên chuyển tiếp ở một tầng là component nằm sâu nhận
undefined, mà tầng làm rơi prop chẳng báo lỗi gì.
Thử xem: thêm một hàm onLogout trong App mà Avatar cần gọi. Bạn sẽ phải sửa cả bốn component.
Cách 1: composition với children
Đây là cách mọi người hay bỏ qua. Thay vì truyền dữ liệu xuống để một component nằm sâu render với nó, hãy để chính component sở hữu dữ liệu render component nằm sâu đó, rồi truyền element đã hoàn chỉnh xuống qua children (hoặc bất kỳ prop nào khác). Các tầng ở giữa chỉ render một chỗ trống và không bao giờ thấy user.
Page và Sidebar không còn biết có user tồn tại. App tạo Avatar ngay tại nơi user nằm và chuyền nó xuống như một element dựng sẵn. Những component bố cục như thế này trở nên dùng lại được, và thêm onLogout giờ chỉ là thay đổi trong App và Avatar. Cả hai vẫn ghi log sau mỗi cú nhấn, vì mỗi lần render App lại truyền cho chúng một element mới: composition làm code gọn hơn, không làm giảm số lần render. Trang children trình bày thêm các pattern dùng slot.
Composition hiệu quả khi component sở hữu dữ liệu cũng có thể quyết định phần nằm sâu trông thế nào. Nó không giúp được khi component nằm sâu thuộc một thư viện hoặc một cấu trúc mà component sở hữu không render.
Cách 2: context
Khi nhiều component ở các độ sâu khác nhau cần cùng một giá trị (theme, người dùng đang đăng nhập, ngôn ngữ hiện tại), hãy đặt nó vào context. Bất kỳ component nào nằm dưới provider đều đọc được bằng useContext, và không có gì ở giữa phải thay đổi.
Avatar và Greeting đọc user từ hai độ sâu khác nhau, còn Page và Sidebar không nhận prop nào. Thêm một component đọc thứ ba ở bất kỳ đâu dưới Page cũng chạy mà không phải động đến phần còn lại. Cái giá phải trả: luồng dữ liệu không còn hiện rõ qua props, và mọi component đọc đều render lại khi giá trị thay đổi. Trang useContext trình bày về provider, giá trị mặc định và những lần render lại đó.
Cách 3: thư viện quản lý state
Với state lớn của ứng dụng mà nhiều component đọc và thay đổi thường xuyên (giỏ hàng, trình soạn thảo tài liệu, dữ liệu trực tiếp), một thư viện quản lý state như Redux Toolkit, Zustand hay Jotai cho phép mỗi component chỉ subscribe phần dữ liệu nó dùng. Cách này tránh được cả prop drilling lẫn cái giá "mọi component đọc đều render lại" của một context lớn. Nhưng đó cũng là thêm một dependency và thêm một loạt khái niệm, nên chỉ tìm đến nó khi hai cách đầu không còn đủ, chứ không phải để xử lý một prop đi qua ba tầng.
// Zustand, for comparison (not available in the editor here)
const useCart = create((set) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
}));
function CartCount() {
const count = useCart((s) => s.items.length); // renders only when the count changes
return <span>{count}</span>;
}
Khi nào prop drilling vẫn ổn
Truyền props là cách dữ liệu chảy trong React, và là pattern dễ đọc nhất: bạn có thể lần theo một giá trị từ trên xuống dưới chỉ bằng cách nhìn JSX. Một prop đi qua một hai component có dùng một phần của nó, hoặc qua một chuỗi ngắn các component liên quan chặt chẽ với nhau, không phải là vấn đề cần giải quyết. Hãy lift state lên component cha chung gần nhất, truyền nó xuống, và chỉ đổi cách làm khi các tầng ở giữa bắt đầu mang những prop chúng không dùng đến.
Câu hỏi thường gặp
Prop drilling trong React là gì?
Là việc truyền một prop xuống qua nhiều tầng component không dùng đến nó, chỉ để một component nằm sâu trong cây đọc được. Mỗi component ở giữa nhận prop rồi chuyển tiếp xuống dưới.
Prop drilling có xấu không?
Tự bản thân thì không. Truyền một giá trị xuống hai hay ba tầng rất rõ ràng và dễ theo dõi. Nó chỉ thành vấn đề khi nhiều tầng mang những prop chúng không bao giờ dùng, khiến mỗi lần đổi tên hay thêm trường đều phải sửa những tệp chẳng liên quan gì.
Làm sao để tránh prop drilling?
Hãy thử composition trước: để component cha dựng sẵn component nằm sâu và truyền nó xuống qua children hoặc một prop khác, nhờ đó các tầng ở giữa không bao giờ thấy dữ liệu. Nếu nhiều component ở các độ sâu khác nhau cần giá trị đó, hãy dùng context. Với state lớn và thay đổi thường xuyên, hãy cân nhắc một thư viện quản lý state.
Context có phải cách duy nhất để khắc phục prop drilling?
Không, và thường cũng không phải cách tốt nhất. Composition với children loại bỏ prop drilling mà không cần thêm context, đồng thời giữ cho luồng dữ liệu hiện rõ trong code.