Bài 18.5: Bài tập thực hành Context API

Bài 18.5: Bài tập thực hành Context API
Chào mừng trở lại với chuỗi bài viết về lập trình Front-end! Hôm nay, chúng ta sẽ đắm chìm vào một công cụ cực kỳ hữu ích trong React giúp quản lý trạng thái (state) của ứng dụng một cách thanh lịch hơn: Context API. Đặc biệt, chúng ta sẽ tập trung vào thực hành để thấy rõ sức mạnh của nó.
Bạn đã bao giờ cảm thấy mệt mỏi khi phải truyền một prop (dữ liệu) từ component cha xuống component con, rồi xuống component cháu, rồi thậm chí xuống tận component chắt chỉ vì một component ở rất sâu cần nó? Cái cảm giác phải "khoan" xuyên qua cả cây component chỉ để đưa một mẩu dữ liệu nhỏ tới nơi cần thiết? Đó chính là vấn đề "prop drilling" (hay tạm dịch là "khoan prop") - một cơn ác mộng tiềm ẩn trong các ứng dụng React lớn.
Context API ra đời chính là để giải quyết vấn đề này. Nó cho phép bạn tạo ra một "ngữ cảnh" (context) nơi bạn có thể cung cấp (provide) dữ liệu và các component ở bất kỳ cấp độ sâu nào trong cây component, miễn là chúng nằm trong phạm vi của provider đó, đều có thể tiêu thụ (consume) dữ liệu này một cách trực tiếp mà không cần truyền qua các component trung gian.
Hãy tưởng tượng Context API như một đài phát thanh:
- Bạn có một Đài phát thanh (Context object).
- Bạn có một Máy phát sóng (Provider component) phát tín hiệu (dữ liệu) lên đài này.
- Bất kỳ ai có Máy thu sóng (useContext hook) và nằm trong phạm vi phủ sóng của máy phát sóng đều có thể nghe được tín hiệu (truy cập dữ liệu) mà không cần dây nhợ lằng nhằng.
Nghe có vẻ tuyệt vời đúng không? Giờ chúng ta hãy bắt tay vào một bài tập thực hành cụ thể để thấy nó hoạt động như thế nào trong thực tế.
Bài tập thực hành: Xây dựng chức năng chuyển đổi Theme (Sáng/Tối) bằng Context API
Một trong những ví dụ kinh điển và dễ hiểu nhất về việc sử dụng Context API là quản lý trạng thái Theme (giao diện sáng - light
hoặc tối - dark
) cho toàn bộ ứng dụng. Trạng thái theme thường được cần ở rất nhiều nơi: background, màu chữ, màu viền của các component khác nhau. Truyền theme
và hàm setTheme
xuống qua prop sẽ rất nhanh chóng dẫn đến prop drilling. Context API là một giải pháp lý tưởng cho trường hợp này.
Chúng ta sẽ xây dựng một ứng dụng React đơn giản với các bước sau:
- Tạo Context: Định nghĩa Context object với giá trị mặc định.
- Tạo Provider Component: Một component sẽ quản lý trạng thái theme thực tế và cung cấp nó cho các component con.
- Sử dụng Context (Consume): Các component con sẽ truy cập trạng thái theme và hàm chuyển đổi theme từ Context.
- Kết hợp: Đặt Provider ở cấp cao trong cây component để các component con có thể truy cập.
Bắt đầu nào!
Bước 1: Tạo Context Object
Đầu tiên, chúng ta cần tạo một Context object. File này sẽ chứa định nghĩa Context và sau này có thể thêm Provider và Custom Hook.
Tạo một file mới, ví dụ: src/contexts/ThemeContext.js
// src/contexts/ThemeContext.js
import { createContext, useContext } from 'react';
// Định nghĩa giá trị mặc định cho Context.
// Giá trị này sẽ được sử dụng nếu một component gọi useContext
// mà không có ThemeProvider nào ở phía trên trong cây component.
// Nó cũng giúp định hình cấu trúc dữ liệu mà context sẽ cung cấp.
const ThemeContext = createContext({
theme: 'light', // Giá trị theme mặc định ban đầu
setTheme: () => {}, // Một hàm rỗng làm placeholder cho hàm cập nhật theme
});
// Chúng ta sẽ export Context này để các component khác có thể sử dụng.
export { ThemeContext };
// (Chúng ta sẽ thêm Provider và Custom Hook vào file này sau)
Giải thích code:
import { createContext } from 'react';
: Chúng ta import hàmcreateContext
từ thư việnreact
.const ThemeContext = createContext({...});
: GọicreateContext()
để tạo một Context object mới. Đối số truyền vào{ theme: 'light', setTheme: () => {} }
là giá trị mặc định của context. Việc cung cấp giá trị mặc định này rất quan trọng để:- Giúp định hình cấu trúc dữ liệu mà context sẽ cung cấp.
- Cung cấp giá trị fallback nếu component sử dụng
useContext
nằm ngoài phạm vi củaProvider
. - Hữu ích cho việc kiểm tra bằng Storybook hoặc unit test mà không cần bọc toàn bộ app trong Provider.
export { ThemeContext };
: Export Context object để các component khác có thể import và sử dụng nó.
Bước 2: Tạo Provider Component
Tiếp theo, chúng ta sẽ tạo một component đặc biệt đóng vai trò là Provider. Component này sẽ quản lý trạng thái theme thực tế (ví dụ: dùng useState
) và cung cấp trạng thái này cùng với hàm cập nhật trạng thái xuống cho tất cả các component con nằm bên trong nó.
Chúng ta sẽ thêm ThemeProvider
vào cùng file src/contexts/ThemeContext.js
cho gọn.
// src/contexts/ThemeContext.js (Tiếp tục)
import { createContext, useContext, useState } from 'react'; // Import useState
// ... (ThemeContext đã định nghĩa ở trên)
const ThemeContext = createContext({
theme: 'light',
setTheme: () => {},
});
// Tạo component Provider
const ThemeProvider = ({ children }) => {
// Sử dụng useState để quản lý trạng thái theme thực tế
const [theme, setTheme] = useState('light'); // Khởi tạo theme mặc định là 'light'
// Hàm để chuyển đổi theme
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
// Gói giá trị và hàm cần cung cấp vào một object
const contextValue = {
theme, // Cung cấp trạng thái theme hiện tại
setTheme: toggleTheme, // Cung cấp hàm để component con có thể thay đổi theme
};
return (
// ThemeContext.Provider là component tích hợp của Context API
// Prop 'value' nhận dữ liệu mà bạn muốn cung cấp xuống cây component.
// Bất kỳ component nào bên trong <ThemeProvider> và sử dụng useContext(ThemeContext)
// sẽ nhận được object { theme, setTheme } này.
<ThemeContext.Provider value={contextValue}>
{children} {/* children là các component được bao bọc bởi ThemeProvider */}
</ThemeContext.Provider>
);
};
// Export Provider để sử dụng trong App hoặc các component cha
export { ThemeContext, ThemeProvider };
// (Sẽ thêm Custom Hook ở bước tiếp theo)
Giải thích code:
import { useState } from 'react';
: Import hookuseState
để quản lý trạng thái local của Provider.const ThemeProvider = ({ children }) => { ... };
: Định nghĩa componentThemeProvider
. Nó nhận propchildren
, đại diện cho tất cả các component con mà nó sẽ bao bọc.const [theme, setTheme] = useState('light');
: Khởi tạo trạng tháitheme
với giá trị ban đầu là'light'
. HàmsetTheme
sẽ được dùng để cập nhật trạng thái này.const toggleTheme = () => { ... };
: Một hàm helper đơn giản để chuyển đổi giá trị theme giữa 'light' và 'dark'.const contextValue = { theme, setTheme: toggleTheme };
: Tạo một object chứa dữ liệu và hàm mà Provider sẽ cung cấp. Chúng ta cung cấp trạng tháitheme
hiện tại và hàmtoggleTheme
(đổi tên thànhsetTheme
trong object cung cấp để nhất quán với giá trị mặc định).<ThemeContext.Provider value={contextValue}>
: Đây là điểm mấu chốt! Chúng ta sử dụng componentThemeContext.Provider
(được sinh ra cùng vớiThemeContext
). Propvalue
của nó nhận chính xác dữ liệu mà bạn muốn tất cả các consumer bên dưới nhận được.{children}
: Render tất cả các component con bên trongProvider
. Điều này đảm bảo rằng tất cả chúng đều nằm trong phạm vi củaProvider
.
Bước 3: Sử dụng Context (Consume) trong Components
Bây giờ, bất kỳ component nào nằm bên trong <ThemeProvider>
đều có thể dễ dàng truy cập giá trị theme
và hàm setTheme
bằng cách sử dụng hook useContext
.
Hãy tạo hai component ví dụ: một component hiển thị theme hiện tại và một component có nút bấm để chuyển đổi theme.
Tạo file src/components/ThemedComponent.js
:
// src/components/ThemedComponent.js
import React from 'react';
import { ThemeContext } from '../contexts/ThemeContext'; // Import ThemeContext
const ThemedComponent = () => {
// Sử dụng hook useContext và truyền vào Context object bạn muốn truy cập.
// React sẽ tìm ThemeContext.Provider gần nhất ở phía trên trong cây component
// và trả về giá trị của prop 'value' từ Provider đó.
const { theme } = React.useContext(ThemeContext);
// Áp dụng styles dựa trên theme
const styles = {
backgroundColor: theme === 'light' ? '#f0f0f0' : '#333',
color: theme === 'light' ? '#000' : '#fff',
padding: '20px',
margin: '10px',
borderRadius: '8px',
border: `1px solid ${theme === 'light' ? '#ccc' : '#555'}`,
};
return (
<div style={styles}>
<h3>Component này biết về Theme!</h3>
<p>Chủ đề hiện tại: <strong>{theme.toUpperCase()}</strong></p>
<p>Dữ liệu này được lấy từ Context API.</p>
</div>
);
};
export default ThemedComponent;
Tạo file src/components/ThemeToggler.js
:
// src/components/ThemeToggler.js
import React from 'react';
import { ThemeContext } from '../contexts/ThemeContext'; // Import ThemeContext
const ThemeToggler = () => {
// Lần này, chúng ta cần cả theme hiện tại và hàm setTheme để thay đổi nó.
const { theme, setTheme } = React.useContext(ThemeContext);
return (
<button
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
style={{
padding: '10px 15px',
fontSize: '1em',
cursor: 'pointer',
backgroundColor: theme === 'light' ? '#007bff' : '#6610f2',
color: 'white',
border: 'none',
borderRadius: '5px',
margin: '10px',
}}
>
Chuyển sang Theme {theme === 'light' ? 'Dark' : 'Light'}
</button>
);
};
export default ThemeToggler;
Giải thích code:
import { ThemeContext } from '../contexts/ThemeContext';
: Import Context object mà chúng ta đã tạo.const { theme } = React.useContext(ThemeContext);
: Đây là cách "tiêu thụ" context. Chúng ta gọiuseContext
, truyền vào Context object (ThemeContext
). Hook này sẽ trả về giá trị màThemeProvider
gần nhất đã cung cấp qua propvalue
. Chúng ta dùng destructuring để lấy ra biếntheme
và/hoặcsetTheme
từ object này.- Bây giờ, biến
theme
và hàmsetTheme
có sẵn để sử dụng trực tiếp trong component, bất kể component này nằm sâu bao nhiêu cấp so vớiThemeProvider
. Không cần truyền qua prop nữa!
Bước 4: Kết hợp Provider và Consumers trong App
Cuối cùng, chúng ta cần bọc phần ứng dụng cần truy cập theme bên trong <ThemeProvider>
. Thông thường, bạn sẽ bọc phần lớn hoặc toàn bộ ứng dụng trong Provider
ở cấp cao nhất (App.js
hoặc index.js
).
Sửa file src/App.js
:
// src/App.js
import React from 'react';
import { ThemeProvider } from './contexts/ThemeContext'; // Import ThemeProvider
import ThemedComponent from './components/ThemedComponent';
import ThemeToggler from './components/ThemeToggler';
import './App.css'; // (Nếu bạn có file CSS)
function App() {
return (
// Bọc các component cần truy cập theme bên trong ThemeProvider.
// Mọi component nằm trong đây đều có thể dùng useContext(ThemeContext).
<ThemeProvider>
<div className="App" style={{ textAlign: 'center', padding: '20px' }}>
<h1>Ví dụ về Theme Switching với Context API</h1>
{/* Components con sử dụng context */}
<ThemeToggler />
<ThemedComponent />
<ThemedComponent /> {/* Bạn có thể dùng ThemedComponent nhiều lần, tất cả đều nhận theme */}
{/* Các components khác của ứng dụng */}
</div>
</ThemeProvider>
);
}
export default App;
Giải thích code:
import { ThemeProvider } from './contexts/ThemeContext';
: Import componentThemeProvider
.<ThemeProvider> ... </ThemeProvider>
: Chúng ta đặt các componentThemeToggler
vàThemedComponent
vào giữa thẻ mở và đóng củaThemeProvider
. Điều này làm choThemeProvider
trở thành component cha gần nhất, và giá trị context mà nó cung cấp sẽ sẵn sàng cho tất cả các component con bên trong nó sử dụng thông quauseContext
.
Khi bạn chạy ứng dụng, bạn sẽ thấy ThemedComponent
hiển thị theme hiện tại ('light' ban đầu) và nút "Chuyển sang Theme Dark". Khi bạn nhấn nút, hàm setTheme
(mà thực chất là toggleTheme
từ Provider) sẽ được gọi. Điều này làm thay đổi state theme
bên trong ThemeProvider
. Khi state của ThemeProvider
thay đổi, ThemeProvider
và tất cả các component con (như ThemedComponent
và ThemeToggler
) mà đang sử dụng useContext(ThemeContext)
sẽ được re-render với giá trị context mới! Kết quả là giao diện của ThemedComponent
sẽ thay đổi theo theme mới, và label trên nút ThemeToggler
cũng cập nhật.
Bước 5: Tạo Custom Hook để sử dụng Context một cách gọn gàng hơn (Best Practice)
Mặc dù React.useContext(ThemeContext)
hoạt động tốt, một pattern phổ biến và được khuyến khích là tạo một Custom Hook để đóng gói logic sử dụng context. Điều này giúp:
- Code ở các component tiêu thụ context trở nên ngắn gọn và dễ đọc hơn (
useTheme()
thay vìuseContext(ThemeContext)
). - Tập trung logic kiểm tra lỗi (ví dụ: đảm bảo hook được sử dụng trong Provider) tại một nơi duy nhất.
Hãy thêm Custom Hook useTheme
vào file src/contexts/ThemeContext.js
:
// src/contexts/ThemeContext.js (Cuối file)
// ... (ThemeContext và ThemeProvider đã định nghĩa ở trên)
// Tạo Custom Hook để tiện sử dụng Context
const useTheme = () => {
const context = useContext(ThemeContext); // Sử dụng useContext gốc bên trong hook
// Kiểm tra xem hook có được gọi bên ngoài ThemeProvider không
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context; // Trả về giá trị context { theme, setTheme }
};
// Export thêm Custom Hook
export { ThemeContext, ThemeProvider, useTheme };
Bây giờ, bạn có thể sửa lại các component ThemedComponent
và ThemeToggler
để sử dụng useTheme
thay vì React.useContext(ThemeContext)
:
Sửa file src/components/ThemedComponent.js
:
// src/components/ThemedComponent.js
import React from 'react';
// Import chỉ custom hook useTheme
import { useTheme } from '../contexts/ThemeContext';
const ThemedComponent = () => {
// Sử dụng custom hook useTheme()
const { theme } = useTheme();
const styles = {
backgroundColor: theme === 'light' ? '#f0f0f0' : '#333',
color: theme === 'light' ? '#000' : '#fff',
padding: '20px',
margin: '10px',
borderRadius: '8px',
border: `1px solid ${theme === 'light' ? '#ccc' : '#555'}`,
};
return (
<div style={styles}>
<h3>Component này biết về Theme! (Dùng Custom Hook)</h3>
<p>Chủ đề hiện tại: <strong>{theme.toUpperCase()}</strong></p>
<p>Dữ liệu này được lấy từ Context API thông qua Custom Hook.</p>
</div>
);
};
export default ThemedComponent;
Sửa file src/components/ThemeToggler.js
:
// src/components/ThemeToggler.js
import React from 'react';
// Import chỉ custom hook useTheme
import { useTheme } from '../contexts/ThemeContext';
const ThemeToggler = () => {
// Sử dụng custom hook useTheme()
const { theme, setTheme } = useTheme();
return (
<button
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
style={{
padding: '10px 15px',
fontSize: '1em',
cursor: 'pointer',
backgroundColor: theme === 'light' ? '#007bff' : '#6610f2',
color: 'white',
border: 'none',
borderRadius: '5px',
margin: '10px',
}}
>
Chuyển sang Theme {theme === 'light' ? 'Dark' : 'Light'} (Dùng Custom Hook)
</button>
);
};
export default ThemeToggler;
Việc sử dụng Custom Hook useTheme()
làm cho code của các component sử dụng context trông sạch sẽ và có ngữ nghĩa hơn rất nhiều!
Khi nào nên dùng Context API và khi nào không?
Context API là một công cụ tuyệt vời, nhưng nó không phải là giải pháp cho mọi vấn đề quản lý state.
- Nên dùng khi:
- Bạn cần chia sẻ dữ liệu toàn cục (global) hoặc dữ liệu được coi là "theme" của ứng dụng (như user authentication status, theme, ngôn ngữ, cài đặt chung).
- Dữ liệu này không thay đổi quá thường xuyên.
- Bạn muốn tránh prop drilling khi dữ liệu cần được truyền qua nhiều cấp component không liên quan trực tiếp đến dữ liệu đó.
- Không nên dùng khi:
- Chỉ cần truyền dữ liệu giữa một component cha và component con trực tiếp hoặc chỉ cách nhau một vài cấp. Prop drilling ở mức độ nhỏ là hoàn toàn chấp nhận được và đôi khi dễ theo dõi hơn.
- Dữ liệu thay đổi rất thường xuyên (ví dụ: vị trí con trỏ chuột, input của một ô search gõ liên tục). Việc sử dụng Context cho dữ liệu thay đổi nhanh có thể gây ra hiệu năng kém vì mỗi khi context value thay đổi, tất cả các component sử dụng
useContext
đó sẽ re-render. - Bạn cần quản lý state phức tạp với nhiều actions, side effects, và cần các tính năng như middleware, devtools (ví dụ: giỏ hàng phức tạp, quản lý dữ liệu server cache). Trong trường hợp này, các thư viện quản lý state chuyên dụng như Redux, Zustand, MobX hoặc React Query/SWR (cho dữ liệu server) có thể là lựa chọn tốt hơn.
Context API mạnh mẽ nhất khi được dùng cho các giá trị "static" hoặc ít thay đổi trong ứng dụng, cung cấp một cách hiệu quả để "tiêm" dữ liệu vào sâu trong cây component mà không làm lộn xộn code của các component trung gian.
Tổng kết (không phải Kết luận):
Thông qua bài tập thực hành về chuyển đổi Theme này, chúng ta đã thấy cách Context API giúp giải quyết vấn đề prop drilling. Chúng ta đã học cách:
- Tạo Context Object để định nghĩa "kênh" truyền dữ liệu.
- Tạo Provider Component để quản lý state và phát dữ liệu lên kênh đó.
- Sử dụng hook
useContext
để "thu sóng" và lấy dữ liệu từ kênh ở bất kỳ component nào nằm trong phạm vi phủ sóng. - Áp dụng Custom Hook (
useTheme
) như một best practice để làm cho việc sử dụng context trở nên đẹp và dễ bảo trì hơn.
Context API là một công cụ thiết yếu trong hộp đồ nghề của lập trình viên React hiện đại. Nắm vững cách sử dụng nó sẽ giúp bạn xây dựng các ứng dụng lớn và phức tạp hơn một cách có tổ chức và dễ quản lý.
Comments