Menu

Prop drilling trong React là gì và cách tránh

Prop drilling là truyền một prop qua những component không dùng đến nó, chỉ để một component ở sâu hơn đọc được. Xem ví dụ chạy trực tiếp, vì sao nó gây khó khi ứng dụng lớn dần và các cách khắc phục theo thứ tự nên thử: composition, context, rồi thư viện quản lý state.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

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. Page và Sidebar giờ có prop user trong 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.avatarUrl hay 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 Avatar sang một nhánh khác là bạn phải luồn user qua 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.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu