Truyền Dữ Liệu Sâu Với Context
Thông thường, bạn sẽ truyền thông tin từ component cha tới component con thông qua props. Nhưng việc truyền props có thể trở nên dài dòng và bất tiện nếu bạn phải truyền chúng qua nhiều component ở giữa, hoặc nếu nhiều component trong ứng dụng của bạn cần cùng thông tin đó. Context cho phép component cha cung cấp một số thông tin cho bất kỳ component nào trong cây bên dưới nó—bất kể sâu đến đâu—mà không cần truyền một cách rõ ràng thông qua props.
Bạn sẽ được học
- ”Prop drilling” là gì
- Cách thay thế việc truyền props lặp đi lặp lại bằng context
- Các trường hợp sử dụng phổ biến cho context
- Các phương án thay thế phổ biến cho context
Vấn đề với việc truyền props
Truyền props là một cách tuyệt vời để truyền dữ liệu một cách rõ ràng qua cây UI của bạn tới những component sử dụng nó.
Nhưng việc truyền props có thể trở nên dài dòng và bất tiện khi bạn cần truyền một prop sâu qua cây, hoặc nếu nhiều component cần cùng một prop. Tổ tiên chung gần nhất có thể ở xa những component cần dữ liệu, và nâng state lên cao như vậy có thể dẫn đến tình huống được gọi là “prop drilling”.
Nâng state lên


Prop drilling


Sẽ thật tuyệt nếu có cách “dịch chuyển” dữ liệu tới những component trong cây cần nó mà không cần truyền props? Với tính năng context của React, điều đó hoàn toàn có thể!
Context: một phương án thay thế cho việc truyền props
Context cho phép component cha cung cấp dữ liệu cho toàn bộ cây bên dưới nó. Có nhiều cách sử dụng cho context. Đây là một ví dụ. Hãy xem xét component Heading
này chấp nhận một level
cho kích thước của nó:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading level={1}>Title</Heading> <Heading level={2}>Heading</Heading> <Heading level={3}>Sub-heading</Heading> <Heading level={4}>Sub-sub-heading</Heading> <Heading level={5}>Sub-sub-sub-heading</Heading> <Heading level={6}>Sub-sub-sub-sub-heading</Heading> </Section> ); }
Giả sử bạn muốn nhiều heading trong cùng một Section
luôn có cùng kích thước:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading level={1}>Title</Heading> <Section> <Heading level={2}>Heading</Heading> <Heading level={2}>Heading</Heading> <Heading level={2}>Heading</Heading> <Section> <Heading level={3}>Sub-heading</Heading> <Heading level={3}>Sub-heading</Heading> <Heading level={3}>Sub-heading</Heading> <Section> <Heading level={4}>Sub-sub-heading</Heading> <Heading level={4}>Sub-sub-heading</Heading> <Heading level={4}>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
Hiện tại, bạn truyền prop level
tới từng <Heading>
riêng biệt:
<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>
Sẽ thật tuyệt nếu bạn có thể truyền prop level
tới component <Section>
thay vì vào <Heading>
. Cách này bạn có thể đảm bảo rằng tất cả các heading trong cùng một section có cùng kích thước:
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>
Nhưng làm thế nào component <Heading>
có thể biết level của <Section>
gần nhất? Điều đó cần có cách nào đó để component con “hỏi” dữ liệu từ đâu đó ở trên trong cây.
Bạn không thể làm được điều đó chỉ với props. Đây là lúc context xuất hiện. Bạn sẽ làm điều đó trong ba bước:
- Tạo một context. (Bạn có thể gọi nó là
LevelContext
, vì nó dành cho heading level.) - Sử dụng context đó từ component cần dữ liệu. (
Heading
sẽ sử dụngLevelContext
.) - Cung cấp context đó từ component chỉ định dữ liệu. (
Section
sẽ cung cấpLevelContext
.)
Context cho phép component cha—thậm chí là component rất xa!—cung cấp một số dữ liệu cho toàn bộ cây bên trong nó.
Sử dụng context trong những component con gần


Sử dụng context trong những component con xa


Bước 1: Tạo context
Đầu tiên, bạn cần tạo context. Bạn sẽ cần export nó từ một file để các component của bạn có thể sử dụng nó:
import { createContext } from 'react'; export const LevelContext = createContext(1);
Tham số duy nhất của createContext
là giá trị mặc định. Ở đây, 1
tham chiếu tới level heading lớn nhất, nhưng bạn có thể truyền bất kỳ loại giá trị nào (thậm chí là một object). Bạn sẽ thấy ý nghĩa của giá trị mặc định trong bước tiếp theo.
Bước 2: Sử dụng context
Import Hook useContext
từ React và context của bạn:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
Hiện tại, component Heading
đọc level
từ props:
export default function Heading({ level, children }) {
// ...
}
Thay vào đó, hãy xóa prop level
và đọc giá trị từ context bạn vừa import, LevelContext
:
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext
là một Hook. Giống như useState
và useReducer
, bạn chỉ có thể gọi Hook ngay bên trong component React (không bên trong vòng lặp hoặc điều kiện). useContext
thông báo cho React rằng component Heading
muốn đọc LevelContext
.
Bây giờ component Heading
không có prop level
, bạn không cần truyền prop level tới Heading
trong JSX của bạn như thế này nữa:
<Section>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
</Section>
Cập nhật JSX để Section
nhận nó thay vào đó:
<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
Như một lời nhắc, đây là markup mà bạn đang cố gắng làm cho hoạt động:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section level={1}> <Heading>Title</Heading> <Section level={2}> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section level={3}> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section level={4}> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
Chú ý ví dụ này vẫn chưa hoạt động hoàn toàn! Tất cả các heading có cùng kích thước bởi vì mặc dù bạn đang sử dụng context, bạn vẫn chưa cung cấp nó. React không biết lấy nó từ đâu!
Nếu bạn không cung cấp context, React sẽ sử dụng giá trị mặc định mà bạn đã chỉ định trong bước trước. Trong ví dụ này, bạn đã chỉ định 1
làm tham số cho createContext
, vì vậy useContext(LevelContext)
trả về 1
, thiết lập tất cả những heading đó thành <h1>
. Hãy khắc phục vấn đề này bằng cách để mỗi Section
cung cấp context riêng của nó.
Bước 3: Cung cấp context
Component Section
hiện tại render children của nó:
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
Bọc chúng bằng context provider để cung cấp LevelContext
cho chúng:
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext value={level}>
{children}
</LevelContext>
</section>
);
}
Điều này nói với React: “nếu bất kỳ component nào bên trong <Section>
này hỏi về LevelContext
, hãy cung cấp cho chúng level
này.” Component sẽ sử dụng giá trị của <LevelContext>
gần nhất trong cây UI phía trên nó.
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section level={1}> <Heading>Title</Heading> <Section level={2}> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section level={3}> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section level={4}> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
Kết quả giống như code gốc, nhưng bạn không cần truyền prop level
tới từng component Heading
! Thay vào đó, nó “tìm ra” heading level bằng cách hỏi Section
gần nhất phía trên:
- Bạn truyền prop
level
tới<Section>
. Section
bọc children của nó vào<LevelContext value={level}>
.Heading
hỏi giá trị gần nhất củaLevelContext
phía trên bằnguseContext(LevelContext)
.
Sử dụng và cung cấp context từ cùng một component
Hiện tại, bạn vẫn phải chỉ định level
của từng section thủ công:
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
Vì context cho phép bạn đọc thông tin từ component phía trên, mỗi Section
có thể đọc level
từ Section
phía trên, và truyền level + 1
xuống tự động. Đây là cách bạn có thể làm điều đó:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext value={level + 1}>
{children}
</LevelContext>
</section>
);
}
Với thay đổi này, bạn không cần truyền prop level
cho cả <Section>
lẫn <Heading>
:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading>Title</Heading> <Section> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
Bây giờ cả Heading
và Section
đều đọc LevelContext
để tìm hiểu chúng “sâu” đến mức nào. Và Section
bọc children của nó vào LevelContext
để chỉ định rằng bất cứ thứ gì bên trong nó đều ở level “sâu hơn”.
Context truyền qua các component trung gian
Bạn có thể chèn bao nhiêu component tùy thích giữa component cung cấp context và component sử dụng nó. Điều này bao gồm cả những component có sẵn như <div>
và những component bạn có thể tự xây dựng.
Trong ví dụ này, cùng một component Post
(với đường viền nét đứt) được render ở hai level lồng nhau khác nhau. Chú ý rằng <Heading>
bên trong nó tự động lấy level từ <Section>
gần nhất:
import Heading from './Heading.js'; import Section from './Section.js'; export default function ProfilePage() { return ( <Section> <Heading>My Profile</Heading> <Post title="Hello traveller!" body="Read about my adventures." /> <AllPosts /> </Section> ); } function AllPosts() { return ( <Section> <Heading>Posts</Heading> <RecentPosts /> </Section> ); } function RecentPosts() { return ( <Section> <Heading>Recent Posts</Heading> <Post title="Flavors of Lisbon" body="...those pastéis de nata!" /> <Post title="Buenos Aires in the rhythm of tango" body="I loved it!" /> </Section> ); } function Post({ title, body }) { return ( <Section isFancy={true}> <Heading> {title} </Heading> <p><i>{body}</i></p> </Section> ); }
Bạn không làm gì đặc biệt để điều này hoạt động. Section
chỉ định context cho cây bên trong nó, vì vậy bạn có thể chèn <Heading>
bất cứ đâu, và nó sẽ có kích thước chính xác. Hãy thử trong sandbox phía trên!
Context cho phép bạn viết các component “thích ứng với môi trường xung quanh” và hiển thị khác nhau tùy thuộc vào nơi (hay nói cách khác, trong context nào) chúng được render.
Cách context hoạt động có thể nhắc bạn nhớ tới kế thừa thuộc tính CSS. Trong CSS, bạn có thể chỉ định color: blue
cho một <div>
, và bất kỳ DOM node nào bên trong nó, dù sâu đến đâu, sẽ kế thừa màu đó trừ khi một DOM node khác ở giữa ghi đè nó bằng color: green
. Tương tự, trong React, cách duy nhất để ghi đè một context nào đó từ phía trên là bọc children vào context provider với giá trị khác.
Trong CSS, các thuộc tính khác nhau như color
và background-color
không ghi đè lẫn nhau. Bạn có thể thiết lập color
của tất cả <div>
thành màu đỏ mà không ảnh hưởng đến background-color
. Tương tự, các context React khác nhau không ghi đè lẫn nhau. Mỗi context mà bạn tạo bằng createContext()
hoàn toàn tách biệt với những cái khác, và liên kết các component sử dụng và cung cấp context cụ thể đó. Một component có thể sử dụng hoặc cung cấp nhiều context khác nhau mà không gặp vấn đề gì.
Trước khi bạn sử dụng context
Context rất hấp dẫn để sử dụng! Tuy nhiên, điều này cũng có nghĩa là rất dễ lạm dụng nó. Chỉ vì bạn cần truyền một số props sâu qua nhiều level không có nghĩa là bạn nên đưa thông tin đó vào context.
Dưới đây là một số phương án thay thế bạn nên cân nhắc trước khi sử dụng context:
- Bắt đầu bằng truyền props. Nếu các component của bạn không quá phức tạp, việc truyền một tá props qua một tá component là điều không bất thường. Có thể cảm thấy cực nhọc, nhưng nó làm cho việc component nào sử dụng dữ liệu nào trở nên rất rõ ràng! Người duy trì code của bạn sẽ rất vui khi bạn đã làm luồng dữ liệu trở nên rõ ràng bằng props.
- Trích xuất component và truyền JSX làm
children
cho chúng. Nếu bạn truyền một số dữ liệu qua nhiều layer của những component trung gian không sử dụng dữ liệu đó (và chỉ truyền nó xuống), điều này thường có nghĩa là bạn đã quên trích xuất một số component dọc đường. Ví dụ, có thể bạn truyền data props nhưposts
tới những component trực quan không sử dụng chúng trực tiếp, như<Layout posts={posts} />
. Thay vào đó, hãy đểLayout
nhậnchildren
làm prop, và render<Layout><Posts posts={posts} /></Layout>
. Điều này giảm số lượng layer giữa component chỉ định dữ liệu và component cần nó.
Nếu cả hai cách tiếp cận này đều không phù hợp với bạn, hãy cân nhắc context.
Các trường hợp sử dụng cho context
- Theming: Nếu ứng dụng của bạn cho phép người dùng thay đổi giao diện (ví dụ như dark mode), bạn có thể đặt context provider ở đầu ứng dụng, và sử dụng context đó trong những component cần điều chỉnh giao diện trực quan.
- Tài khoản hiện tại: Nhiều component có thể cần biết người dùng hiện đang đăng nhập. Đưa nó vào context giúp việc đọc nó ở bất cứ đâu trong cây trở nên tiện lợi. Một số ứng dụng cũng cho phép bạn vận hành nhiều tài khoản cùng lúc (ví dụ để bình luận dưới tư cách người dùng khác). Trong những trường hợp đó, việc bọc một phần UI vào provider lồng nhau với giá trị tài khoản hiện tại khác có thể rất tiện lợi.
- Routing: Hầu hết các giải pháp routing sử dụng context bên trong để giữ route hiện tại. Đây là cách mỗi link “biết” nó có đang hoạt động hay không. Nếu bạn xây dựng router riêng, bạn có thể muốn làm điều đó cũng vậy.
- Quản lý state: Khi ứng dụng của bạn phát triển, bạn có thể kết thúc với rất nhiều state gần đầu ứng dụng. Nhiều component xa ở bên dưới có thể muốn thay đổi nó. Việc sử dụng reducer cùng với context để quản lý state phức tạp và truyền nó xuống những component xa mà không gặp quá nhiều rắc rối là điều phổ biến.
Context không giới hạn ở những giá trị tĩnh. Nếu bạn truyền giá trị khác vào lần render tiếp theo, React sẽ cập nhật tất cả các component đang đọc nó bên dưới! Đây là lý do tại sao context thường được sử dụng kết hợp với state.
Nhìn chung, nếu một số thông tin cần thiết cho những component xa trong các phần khác nhau của cây, đó là dấu hiệu tốt cho thấy context sẽ giúp ích cho bạn.
Tóm tắt
- Context cho phép component cung cấp một số thông tin cho toàn bộ cây bên dưới nó.
- Để truyền context:
- Tạo và export nó bằng
export const MyContext = createContext(defaultValue)
. - Truyền nó tới Hook
useContext(MyContext)
để đọc nó trong bất kỳ component con nào, dù sâu đến đâu. - Bọc children vào
<MyContext value={...}>
để cung cấp nó từ component cha.
- Tạo và export nó bằng
- Context truyền qua bất kỳ component nào ở giữa.
- Context cho phép bạn viết các component “thích ứng với môi trường xung quanh”.
- Trước khi sử dụng context, hãy thử truyền props hoặc truyền JSX làm
children
.
Challenge 1 of 1: Thay thế prop drilling bằng context
Trong ví dụ này, việc toggle checkbox thay đổi prop imageSize
được truyền tới mỗi <PlaceImage>
. Trạng thái checkbox được giữ trong component App
cấp cao nhất, nhưng mỗi <PlaceImage>
cần biết về nó.
Hiện tại, App
truyền imageSize
tới List
, sau đó truyền nó tới mỗi Place
, rồi truyền nó tới PlaceImage
. Hãy loại bỏ prop imageSize
, và thay vào đó truyền nó từ component App
trực tiếp tới PlaceImage
.
Bạn có thể khai báo context trong Context.js
.
import { useState } from 'react'; import { places } from './data.js'; import { getImageUrl } from './utils.js'; export default function App() { const [isLarge, setIsLarge] = useState(false); const imageSize = isLarge ? 150 : 100; return ( <> <label> <input type="checkbox" checked={isLarge} onChange={e => { setIsLarge(e.target.checked); }} /> Use large images </label> <hr /> <List imageSize={imageSize} /> </> ) } function List({ imageSize }) { const listItems = places.map(place => <li key={place.id}> <Place place={place} imageSize={imageSize} /> </li> ); return <ul>{listItems}</ul>; } function Place({ place, imageSize }) { return ( <> <PlaceImage place={place} imageSize={imageSize} /> <p> <b>{place.name}</b> {': ' + place.description} </p> </> ); } function PlaceImage({ place, imageSize }) { return ( <img src={getImageUrl(place)} alt={place.name} width={imageSize} height={imageSize} /> ); }