khái niệm React Context và chia sẻ state qua các component với React Context
- 06-10-2022
- Toanngo92
- 0 Comments
Trong hướng dẫn này, chúng ta sẽ tìm hiểu cách chia sẻ trạng thái trên nhiều thành phần bằng cách sử dụng ngữ cảnh React. React context là một interface để chia sẻ thông tin với các thành phần khác mà không cần chuyển dữ liệu làm đạo cụ một cách rõ ràng. Điều này có nghĩa là bạn có thể chia sẻ thông tin giữa thành phần cha và thành phần con lồng nhau hoặc lưu trữ dữ liệu trên toàn trang web ở một nơi duy nhất và truy cập chúng ở bất kỳ đâu trong ứng dụng. Chúng ta thậm chí có thể cập nhật dữ liệu từ các thành phần lồng nhau bằng cách cung cấp các chức năng cập nhật cùng với dữ liệu.
React Context đủ linh hoạt để sử dụng như một hệ thống quản lý trạng thái tập trung cho dự án của bạn hoặc bạn có thể mở rộng phạm vi nó đến các phần nhỏ hơn trong ứng dụng của mình. Với context, bạn có thể chia sẻ dữ liệu trên toàn ứng dụng mà không cần bất kỳ công cụ nào của bên thứ ba bổ sung và với cấu hình đơn giản. Điều này cung cấp một giải pháp thay thế trọng lượng nhẹ hơn cho các công cụ như Redux, có thể trợ giúp với các ứng dụng lớn hơn nhưng có thể yêu cầu quá nhiều thiết lập cho các dự án quy mô vừa và nhỏ.
Trong suốt hướng dẫn này, bạn sẽ sử dụng context để xây dựng một ứng dụng sử dụng các tập dữ liệu chung trên các thành phần khác nhau. Để minh họa điều này, chúng ta tiếp tục quay lại với tính năng danh sách sản phẩm, nhưng lần này chúng ta sẽ chia nhỏ component Products ra thành component Cart riêng và sử dụng context để giải quyết.
Vì chúng ta đang xây dựng một ứng dụng với nhiều thành phần, nên mình quyết định sử dụng thêm JSS để đảm bảo rằng sẽ không có bất kỳ xung đột tên class và để bạn có thể thêm các định kiểu trong cùng một tệp như một component. Để nếu bạn chưa nắm được về JSS, hãy tham khảo bài viết: Hướng dẫn định kiểu (style) cho React Component.
Mục lục
Khởi tạo dự án mới và tích hợp dữ liệu mẫu
Chúng ta tiếp tục khởi tạo dự án mới và cài đặt JSS:
npx create-react-app reactcontext
cd reactcontext
// npm install --save prop-types
npm install react-jss // yarn install react-jss
Tiếp tục bắt đầu với tạo component layout/header/Header.js:
import React from "react"
import { createUseStyles } from "react-jss"
const useStyles = createUseStyles({
header: {
background: '#47b7e5',
color: '#fff',
justifyContent: 'space-between',
padding: 15,
textTransform: 'uppercase',
fontWeight: 'bold',
fontSize: 12,
'& nav': {
'& ul': {
listStyle: 'none',
padding: 0,
margin: '0px -5px',
justifyContent: 'flex-end',
'& li': {
margin: '0px 5px'
}
}
}
}
});
const Header = () => {
const classes = useStyles();
return (<header className={classes.header + ' d-flex'}>
<div className="logo">web888.vn</div>
<nav>
<ul className="d-flex">
<li>Home page</li>
<li>About</li>
<li>Contact</li>
<li>Hello, Toanngo92</li>
</ul>
</nav>
</header>)
}
export default Header
Tạo component layout/header/Footer.js:
import React from "react"
import { createUseStyles } from "react-jss"
const useStyles = createUseStyles({
footer: {
padding: 15,
fontSize: 12,
color: '#fff',
background: '#fa726c'
}
});
const Footer = () => {
const classes = useStyles()
return (<footer className={classes.footer}>
Copyright @ web888.vn
</footer>)
}
export default Footer
Sửa file App.js như sau:
import './App.css';
import { createUseStyles } from 'react-jss';
import Header from './layout/header/Header';
import Footer from './layout/footer/Footer';
const useStyles = createUseStyles({
App: {
maxWidth: 425,
margin: 'auto'
}
});
function App() {
const classes = useStyles()
return (
<div className={classes.App}>
<Header />
<Footer />
</div>
);
}
export default App;
Sửa file App.css (định nghĩa một số class toàn cục):
.d-flex{
display: flex;
flex-wrap: wrap;
}
Output:
Trong tình huống này, mình có thêm một thẻ <li> với dữ liệu cứng là Hello, toanngo92, bước tiếp theo, chúng ta sẽ bắt đầu khởi tạo dữ liệu mẫu để tìm hiểu khái niệm React Context.
Cung cấp dữ liệu từ Root Component
Trong bước này, chúng ta sẽ tìm hiểu cách sử dụng context (bối cảnh) để lưu trữ thông tin user component root. Chúng ta sẽ tạo một context tùy chỉnh, sau đó sử dụng một wrapper component đặc biệt có tên Provider, context này sẽ lưu trữ thông tin ở tầng cơ sở (root) của dự án. Sau đó, chúng ta sử dụng useContext Hook để kết nối với Provider trong các component lồng nhau để có thể hiển thị thông tin tĩnh. Đến cuối bước này, bạn có thể hiểu nôm na giống việc cung cấp một kho thông tin tập trung và sử dụng thông tin được lưu trữ trong context ở nhiều component khác nhau.
Context cơ bản nhất của nó là một interface (giao diện) để chia sẻ thông tin. Nhiều ứng dụng có một số thông tin chung mà họ cần chia sẻ trên toàn ứng dụng, chẳng hạn như tùy chọn người dùng, thông tin theme (tính năng chế độ sáng tối), và các thay đổi ứng dụng trên toàn bộ trang web … Với context, chúng ta có thể lưu trữ thông tin đó ở cấp cơ sở (root) sau đó truy cập nó ở bất kỳ đâu. Vì bạn thông tin này được lưu trữ ở tầng root, nên trong toàn bộ các component con nó vẫn sẽ luôn có sẵn và luôn được cập nhật.
Tạo file context/userContext.js:
import { createContext } from "react";
const UserContext = createContext();
export default UserContext;
Giải thích: Khởi tạo context Bằng cách thực thi hàm createContext(). Sau khi khởi tạo, chúng ta sử dụng trong JSX như một component.
Tạo file /data/user_data.js mô phỏng dữ liệu mẫu user sẽ được lấy về từ backend:
const User = {
id: 1,
name: 'toanngo123',
wishlist: ['0001','0003','0005']
}
export default User
Sửa file App.js:
import './App.css';
import { createUseStyles } from 'react-jss';
import Header from './layout/header/Header';
import Footer from './layout/footer/Footer';
import User from './data/user_data';
import UserContext from './context/userContext';
const user = User
const useStyles = createUseStyles({
App: {
maxWidth: 425,
margin: 'auto'
}
});
function App() {
const classes = useStyles()
return (
<UserContext.Provider value={user}>
<div className={classes.App}>
<Header />
<Footer />
</div>
</UserContext.Provider>
);
}
export default App;
Ở ví dụ trên, chúng ta bọc component <UserContext/> vào <div class=App>, trong component này có một thuộc tính value, và giá trị bằng đối tượng user, chúng ta sẽ có thể sử dụng giá trị của user trong toàn bộ dự án.
Lưu ý, nếu không đưa thuộc tính value vào component UserContext, trình duyệt sẽ warning lỗi:
Warning: The `value` prop is required for the `<Context.Provider>`. Did you misspell it or forget to pass it?
at App (http://localhost:3000/main.00f5f8166bfc592a3e4b.hot-update.js:45:19)
Bước tiếp theo, mình sẽ truyền dữ liệu của user xuống component <Header/> như sau:
import React, { useContext } from "react"
import { createUseStyles } from "react-jss"
import UserContext from "../../context/userContext";
const useStyles = createUseStyles({
header: {
background: '#47b7e5',
color: '#fff',
justifyContent: 'space-between',
padding: 15,
textTransform: 'uppercase',
fontWeight: 'bold',
fontSize: 12,
'& nav': {
'& ul': {
listStyle: 'none',
padding: 0,
margin: '0px -5px',
justifyContent: 'flex-end',
'& li': {
margin: '0px 5px'
}
}
}
}
});
const Header = () => {
const classes = useStyles();
const user = useContext(UserContext)
return (<header className={classes.header + ' d-flex'}>
<div className="logo">web888.vn</div>
<nav>
<ul className="d-flex">
<li>Home page</li>
<li>About</li>
<li>Contact</li>
<li>Hello, {user.name}</li>
</ul>
</nav>
</header>)
}
export default Header
Giải thích: ở ví dụ trên, mình tiếp tục import UserContext vào Header Component và lấy dữ liệu vào biến user thông qua Hook useContext. Sau khi khai báo đầy đủ, biến user sẽ nhận được giá trị từ component bên ngoài mà không cần sử dụng tới props. Vậy sự khác biệt giữa nó với props là gì ? Với props, chúng ta cần gọi trực tiếp tới các component con và đưa giá trị và cho chúng cho mỗi lần gọi, còn với Context, giá trị toàn cục và có thể tham chiếu tới ở bất kì đâu. Bạn có thể hoàn toàn sử dụng props ở tình huống này, đó có thể là một chiến lược hiệu quả. Nhưng khi ứng dụng phát triển, có khả năng component sẽ có thể bị thay đổi cấu trúc, bằng cách sử dụng context, chúng ta sẽ không phải cấu trúc lại header miễn là Provider đã hoàn thiện, giúp việc tái cấu trúc dễ dàng hơn.
Output:
Bước tiếp theo, chúng ta tiếp tục tạo component products, lần này mình sẽ tạo thêm component product để làm component con của component product (trong thực tế việc chia nhỏ component giúp quá trình bảo trì dễ dàng hơn) từng bước như sau:
Tạo file component/products/products.js:
import React, { useState } from "react";
import propTypes from "prop-types";
import { createUseStyles } from "react-jss";
import products_data from '../../data/products_data'
import ProductItem from "../productitem/ProductItem"
const useStyles = createUseStyles({
productsOuter: {
maxWidth: 425,
margin: 'auto'
},
productsWrap: {
marginTop: 10,
marginLeft: -5,
marginRight: -5
}
})
const Products = () => {
const classes = useStyles();
const [products,seProducts] = useState(products_data)
return(<div className={classes.productsOuter}>
<div className={classes.productsWrap+' d-flex'}>
{
products.map((item) => {
return (<ProductItem key={item.id} product={item} />)
})
}
</div>
</div>)
}
export default Products
Tạo file components/productitem/ProductItem.js:
import React from "react";
import propTypes from "prop-types";
import { createUseStyles } from "react-jss";
const useStyles = createUseStyles({
productWrap: {
width: '50%',
padding: '0px 5px',
boxSizing: 'border-box',
marginBottom: 15
},
productThumbnail: {
display: 'block',
position: 'relative',
overflow: 'hidden',
paddingTop: '100%',
border: '1px solid #e1e1e1',
'& img': {
width: '100%',
objectFit: 'cover',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
margin: 'auto'
}
},
productInfo: {
'& h3': {
margin: 0,
'& a': {
fontSize: 14,
textDecoration: 'none',
color: '#333'
}
},
'& p': {
margin: 0,
marginBottom: 10
}
},
btnAddCart: {
background: '#ffc856',
color: '#fff',
display: 'inline-block',
border: 5,
borderRadius: 5,
padding: 10,
cursor: 'pointer'
}
})
const ProductItem = (props) => {
const classes = useStyles()
const item = props.product
return (<div key={item.id} className={classes.productWrap}>
<a href={item.source} target="_blank" rel="noreferrer" className={classes.productThumbnail}>
<img alt={item.name} src={item.thumbnail} />
</a>
<div className={classes.productInfo}>
<h3><a href={item.source} target="_blank" rel="noreferrer">{item.name}</a></h3>
<p>Price: {item.price}</p>
<button type="button" className={classes.btnAddCart}>Add cart</button>
</div>
</div>)
}
export default ProductItem
Tạo file data/products_data.js:
const data = [
{
"id": "0001",
"type": "Laptop",
"name": "Laptop HP Notebook 15 bs1xx",
"price": 8500000,
"thumbnail": "https://laptopcutot.com/wp-content/uploads/2022/08/auto-draft-1660891888837.jpg",
"source": "https://laptopcutot.com/product/laptop-hp-notebook-15-bs1xx-i5-8250u-8g-ssd-240g-vga-on-15-6hd/",
"specs": {
"cpu": "Intel Core i5 8250U",
"Ram": 8,
"storages": [
{
"storage": 240,
"storage_type": "ssd"
}
]
}
},
{
"id": "0002",
"type": "Laptop",
"name": "Laptop HP Probook 450G4",
"price": 8200000,
"thumbnail": "https://laptopcutot.com/wp-content/uploads/2022/08/auto-draft-1660882014527.jpg",
"source": "https://laptopcutot.com/product/laptop-hp-probook-450g4-i5-7200u-8g-ssd-240g-vga-on-15-6hd/",
"specs": {
"cpu": "Intel Core i5 7200U",
"Ram": 8,
"storages": [
{
"storage": 240,
"storage_type": "ssd"
}
]
},
"description": "The machine uses the 7th generation core i5 KabyLake to help deliver stronger and faster performance than many core i5 lines at the same price today. \nThe new 8 GB DDR4 RAM reduces battery power consumption by 20% and improves performance, upgradable to 16 GB."
},
{
"id": "0003",
"type": "Laptop",
"name": "Laptop Dell Latitude E7270",
"price": 7100000,
"thumbnail": "https://laptopcutot.com/wp-content/uploads/2022/08/auto-draft-1660891888837.jpg",
"source": "https://laptopcutot.com/product/laptop-cu-dell-latitude-e7270-core-i7-6600u-ram-4gb-ssd-128gb-vga-hd-man-125inch-fhd/",
"specs": {
"cpu": "Intel Core i5 8250U",
"Ram": 4,
"storages": [
{
"storage": 128,
"storage_type": "ssd"
}
]
},
"description": "Dell Latitude E7270 Core i7-6600U is equipped with Intel's 6th generation processor, giving the machine a stable processing power and effective power saving. With 4 GB DDR4 RAM memory for good processing performance as well as smooth multitasking applications. The Intel HD Graphics 520 graphics chipset allows to run graphics applications well. In addition, Dell also equips the machine with a 128GB or 256GB M.2 SATA SSD to help the machine operate smoother and boot faster."
},
{
"id": "0004",
"type": "Laptop",
"name": "Laptop HP 340s G7",
"price": 7500000,
"thumbnail": "https://laptopcutot.com/wp-content/uploads/2022/08/auto-draft-1660114914748.jpg",
"source": "https://laptopcutot.com/product/laptop-hp-340s-g7-i3-1005g1-4gb-ssd-512gb/",
"specs": {
"cpu": "Intel Core i3 1005G1",
"Ram": 4,
"storages": [
{
"storage": 500,
"storage_type": "ssd"
}
]
}
},
{
"id": "0005",
"type": "Laptop",
"name": "Laptop HP Elitebook 840 G3",
"price": 8200000,
"thumbnail": "https://laptopcutot.com/wp-content/uploads/2022/03/auto-draft-1648029011162.jpg",
"source": "https://laptopcutot.com/product/laptop-cu-hp-elitebook-840-g3-i7-6600u-ram8gb-ssd-256gb-intel-hd-graphic-520-mh-14-full-hd/",
"specs": {
"cpu": "Intel Core i7 6600U",
"Ram": 8,
"storages": [
{
"storage": 240,
"storage_type": "ssd"
}
]
},
"description": "This is already the 3rd version in the Elitebook 840 series and HP continues to keep this name and add the G3 symbol at the back. In general, the overall design of the Elitebook 840 G3 is not much different from the previous 2 generations when the machine still possesses an ultra-thin and light design when it is only 18.9 mm thick and weighs only 1.5 kg.\nThe back of the Elitebook 840 G3 is made of silver-white magnesium alloy with scratch resistance, so it looks quite luxurious and high-class. Besides, although it is quite thin and light, when held in the hand, the device feels sturdy and durable thanks to the US military standard MIL-STD 810G with very good impact resistance, and it is also resistant to shocks. Extreme temperature, high pressure, drop resistance, impact resistance and moisture resistance are much better than conventional laptops."
},
{
"id": "0006",
"type": "Laptop",
"name": "Laptop Dell Inspiron 5493",
"price": 11900000,
"thumbnail": "https://laptopcutot.com/wp-content/uploads/2021/11/5493-2048x2048.jpg",
"source": "https://laptopcutot.com/product/laptop-cu-dell-inspiron-5493-i3-1005g1-ram-4gb-ssd-128gb-vga-microsoft-basic-display-adapter-14fhd/",
"specs": {
"cpu": "Intel Core i3 1005G1",
"Ram": 4,
"storages": [
{
"storage": 128,
"storage_type": "ssd"
}
]
},
"description": "Machines sold are strictly guaranteed for 3 months.\nThe machine is pre-installed with Windows 10 Pro OS and necessary software.\nComes with AP2REP headset\nIncluded accessories: laptop bag and wired mouse"
}
];
export default data
Sửa file App.js:
import './App.css';
import { createUseStyles } from 'react-jss';
import Header from './layout/header/Header';
import Footer from './layout/footer/Footer';
import User from './data/user_data';
import UserContext from './context/userContext';
import Products from './components/products/Products';
const user = User
const useStyles = createUseStyles({
App: {
maxWidth: 425,
margin: 'auto'
}
});
function App() {
const classes = useStyles()
return (
<UserContext.Provider value={user}>
<div className={classes.App}>
<Header />
<Products />
<Footer />
</div>
</UserContext.Provider>
);
}
export default App;
Cấu trúc thư mục và Output:
Bước tiếp theo, để mô tả cách truyền context từ cấp cơ sở (root) tới product items, chúng ta sẽ làm tính năng hiển thị một symbol trái tim ở sản phẩm mà id sản phẩm đó được lưu trữ trong thuộc tính wishlist của người dùng. Tiến hành sửa file components/productitem/productitem.js như sau:
import React, { useContext } from "react";
import propTypes from "prop-types";
import { createUseStyles } from "react-jss";
import UserContext from "../../context/userContext";
const useStyles = createUseStyles({
productWrap: {
width: '50%',
padding: '0px 5px',
boxSizing: 'border-box',
marginBottom: 15,
position: 'relative'
},
productThumbnail: {
display: 'block',
position: 'relative',
overflow: 'hidden',
paddingTop: '100%',
border: '1px solid #e1e1e1',
'& img': {
width: '100%',
objectFit: 'cover',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
margin: 'auto'
}
},
productInfo: {
'& h3': {
margin: 0,
'& a': {
fontSize: 14,
textDecoration: 'none',
color: '#333'
}
},
'& p': {
margin: 0,
marginBottom: 10
}
},
btnAddCart: {
background: '#ffc856',
color: '#fff',
display: 'inline-block',
border: 5,
borderRadius: 5,
padding: 10,
cursor: 'pointer'
},
wishList : {
'&:before':{
content: '"\\2665"',
fontSize: 24,
color: 'red',
display: 'block',
position: 'absolute',
right: 15,
top: 0
}
}
})
const ProductItem = (props) => {
const classes = useStyles();
const user = useContext(UserContext);
const item = props.product;
return (<div key={item.id} className={classes.productWrap+' '+(user.wishlist.includes(item.id) ? classes.wishList : '')}>
<a href={item.source} target="_blank" rel="noreferrer" className={classes.productThumbnail}>
<img alt={item.name} src={item.thumbnail} />
</a>
<div className={classes.productInfo}>
<h3><a href={item.source} target="_blank" rel="noreferrer">{item.name}</a></h3>
<p>Price: {item.price}</p>
<button type="button" className={classes.btnAddCart}>Add cart</button>
</div>
</div>)
}
export default ProductItem
Output:
Trong tình huống trên, chúng ta thấy rằng không cần thiết phải truyền user qua từng props, chỉ cần sử dụng useContext Hook là có thể truyền dữ liệu user từ cấp cơ sở vào thẳng <ProductItem />, rất tiện và dễ dàng bảo trì ứng dụng về sau. Chúng ta có thể sử dụng props hoặc context tùy ngữ cảnh phù hợp để phát triển ứng dụng nhanh và hiệu quả hơn.
Thông tin đã bỏ qua hai component trung gian mà không có bất kỳ props nào. Nếu bạn phải chuyển dữ liệu bằng props xuyên suốt cây ứng dụng, thì sẽ phải xử lý rất nhiều trong mã, và có nguy cơ gặp phải vấn đề khi trong tương lai ứng dụng được cấu trúc lại mã và quên chuyển props xuống. Với context, chúng ta có thể đảm bảo mã sẽ hoạt động khi ứng dụng phát triển phức tạp hơn.
Trong bước này, bạn đã tạo Context và sử dụng Provider để đặt dữ liệu trong cây component. Bạn cũng đã truy cập context với useContext Hook và sử dụng context trên nhiều thành phần. Dữ liệu này là tĩnh và do đó không bao giờ thay đổi sau khi thiết lập ban đầu, nhưng sẽ có lúc bạn cần chia sẻ dữ liệu và cũng có thể sửa đổi dữ liệu trên nhiều component. Trong bước tiếp theo, bạn sẽ cập nhật dữ liệu lồng nhau bằng context.
Cập nhật dữ liệu ở các Component lồng nhau
Trong bước trên, chúng ta đã tạo một component riêng biệt cho Context của mình, nhưng trong trường hợp này, bạn đang tạo nó trong cùng một tệp mà bạn đang sử dụng nó. Vì context User không như không liên quan trực tiếp đến Ứng dụng, nên việc tách chúng ra có thể hợp lý hơn. Tuy nhiên, vì ProductItemContext sẽ được liên kết chặt chẽ với thành phần Cart, việc giữ chúng lại với nhau sẽ tạo ra mã dễ đọc hơn.
Ngoài ra, chúng có thể tạo một ngữ cảnh chung chung hơn được gọi là OrderContext, có thể sử dụng lại ngữ cảnh này trên nhiều component. Trong trường hợp đó, chúng ta muốn tạo một component riêng biệt. Còn bây giờ, hãy giữ chúng bên nhau. Bạn luôn có thể cấu trúc lại sau nếu bạn quyết định chuyển sang một mẫu khác.
Trước khi bạn thêm Provider, hãy hình dung dữ liệu bạn muốn chia sẻ. Bạn sẽ cần một mảng các mục và một hàm để thêm các mục. Không giống như các công cụ quản lý state tập trung khác, context không xử lý các cập nhật cho dữ liệu của bạn. Nó chỉ giữ dữ liệu để sử dụng sau này. Để cập nhật dữ liệu, chúng ta sẽ cần sử dụng các công cụ quản lý state khác như Hooks. Nếu bạn đang thu thập dữ liệu cho cùng một component, bạn sẽ sử dụng useState hoặc useReducer Hooks. Nếu bạn chưa quen với các Hook này, hãy xem bài viêt quản lý trạng Cách quản lý state(trạng thái) bằng Hooks trên React Component.
UseReducer Hook rất phù hợp vì bạn sẽ cần cập nhật state mới nhất cho mọi hành động.
Ở phần này, chúng ta sẽ tiếp tục tạo một component Cart riêng biệt, sử dụng context để có thể truyền dữ liệu từ ProductItem sang Cart trong quá trình thêm giỏ hàng, khác với ví dụ bài trước khi giỏ hàng là một phần của component Products. Để làm được điều này, chúng ta hình dung dữ liệu của sản phẩm phải đọc được bên trong component Cart, tạo một hàm reducer để thêm một mục mới vào một mảng state, sau đó sử dụng useReducer Hook để tạo một mảng product và một hàm setProduct. Để làm được việc này, chúng ta tiến hành cấu trúc file như ví dụ phía dưới, vì công đoạn này phức tạp và khá rắc rối, nên mình sẽ mô tả tuần tự tạo các file như sau:
Tạo file định nghĩa các hàm dùng chung trong dự án, trong tìn huống này vì hàm chuyển đổi từ số sang tiền tệ dùng lại nhiều lần nên mình sẽ định nghĩa vào đây, bắt đầu bằng cách tạo file ultils/ultils.js:
const currencyOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}
const getLocalePrice = (price) => {
return price.toLocaleString(price, currencyOptions)
}
export default getLocalePrice
Bước đầu tiên, khởi tạo một context mới trong file context/Productcontext.js:
import { createContext } from "react";
const UserContext = createContext();
export default UserContext;
Tạo file layout/productpage/ProductPage.js với mục đích bọc components products và component Cart phía dưới:
import React, {createContext,useReducer} from "react";
import { createUseStyles } from "react-jss";
import Cart from "../../components/cart/cart";
import Products from "../../components/products/Products";
import ProductContext from "../../context/ProductContext";
const useStyles = createUseStyles({
})
const cartReducer = (state,item) => {
return [...state,item];
}
const ProductPage = () => {
const classes = useStyles();
const [productsCart,setCart] = useReducer(cartReducer,[])
return (
<ProductContext.Provider value={{productsCart,setCart}}>
<Cart/>
<Products/>
</ProductContext.Provider>
)
}
export default ProductPage;
Giải thích: trong tình huống này, chúng ta thấy mình import ProductContext, và sử dụng Provider component để bọc các component con lại là <Cart /> và <Products/> giúp nó có thể cung cấp context từ cấp này tới các cấp con bên trong. Và ngoài ra, chúng ta thấy rằng mình đang khai báo một reducer, trong reducer này dữ liệu cần return là productsCart với giá trị ban đầu được khởi tạo là một mảng rỗng, phương thức để cập nhật dữ liệu là setCart, tiếp theo, cung cấp giá trị vào trong Context thông qua props value. Để có thể truyền reducer này vào, mình đưa thêm dấu {} vào để bọc 2 tham số mang hàm ý đây là một object có thuộc tính productsCart, và phương thức setCart, là 2 nhân tố để xử lý cập nhật dữ liệu trong reducer.
Sửa file App.js:
import './App.css';
import { createUseStyles } from 'react-jss';
import Header from './layout/header/Header';
import Footer from './layout/footer/Footer';
import User from './data/user_data';
import UserContext from './context/userContext';
import ProductPage from './layout/productpage/ProductPage';
const user = User
const useStyles = createUseStyles({
App: {
maxWidth: 425,
margin: 'auto'
}
});
function App() {
const classes = useStyles()
return (
<UserContext.Provider value={user}>
<div className={classes.App}>
<Header />
<ProductPage />
<Footer />
</div>
</UserContext.Provider>
);
}
export default App;
Giải thích: bên trong App.js, không đưa trực tiếp component compontent <Products/> vào nữa mà đưa <ProductPage/> vào.
Sửa file components/productitem/productitem.js:
import React, { createContext, useContext } from "react";
import propTypes from "prop-types";
import { createUseStyles } from "react-jss";
import UserContext from "../../context/userContext";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../utitls/ultils";
const useStyles = createUseStyles({
productWrap: {
width: '50%',
padding: '0px 5px',
boxSizing: 'border-box',
marginBottom: 15,
position: 'relative'
},
productThumbnail: {
display: 'block',
position: 'relative',
overflow: 'hidden',
paddingTop: '100%',
border: '1px solid #e1e1e1',
'& img': {
width: '100%',
objectFit: 'cover',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
margin: 'auto'
}
},
productInfo: {
'& h3': {
margin: 0,
'& a': {
fontSize: 14,
textDecoration: 'none',
color: '#333'
}
},
'& p': {
margin: 0,
marginBottom: 10
}
},
btnAddCart: {
background: '#ffc856',
color: '#fff',
display: 'inline-block',
border: 5,
borderRadius: 5,
padding: 10,
cursor: 'pointer'
},
wishList : {
'&:before':{
content: '"\\2665"',
fontSize: 24,
color: 'red',
display: 'block',
position: 'absolute',
right: 15,
top: 0
}
}
})
const ProductItem = (props) => {
const classes = useStyles();
const user = useContext(UserContext);
const item = props.product;
const {setCart} = useContext(ProductContext)
const addCart = (event,item) =>{
setCart(item)
}
return (<div key={item.id} className={classes.productWrap+' '+(user.wishlist.includes(item.id) ? classes.wishList : '')}>
<a href={item.source} target="_blank" rel="noreferrer" className={classes.productThumbnail}>
<img alt={item.name} src={item.thumbnail} />
</a>
<div className={classes.productInfo}>
<h3><a href={item.source} target="_blank" rel="noreferrer">{item.name}</a></h3>
<p>Price: {getLocalePrice(item.price)} VND</p>
<button type="button" onClick={(event) => addCart(event,item)} className={classes.btnAddCart}>Add cart</button>
</div>
</div>)
}
export default ProductItem
Giải thích: Trong đoạn mã này, mình sử dụng hàm getLocalePrice được import từ ultils.js, và sử dụng ProductContext mới định nghĩa thông qua hook useContext, và sử dụng kỹ thuật destructuring để tạo một object mới với phương thức setCart bằng context hiện tại. Vì vậy, khi cập nhật dữ liệu, giá trị của productsCart trong context sẽ được cập nhật.
Tạo file components/cart/cart.js:
import React, { useContext } from "react";
import { createUseStyles } from "react-jss";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../utitls/ultils";
const useStyles = createUseStyles({
cartWrap: {
border: '1px solid #e1e1e1',
padding: 15,
marginBottom: 10
},
cartTitle: {
margin: 0
},
cartContent: {
margin: 0
},
cartItems: {
position: 'relative'
},
productCart: {
position: 'relative',
marginBottom: 15,
'& h3': {
margin: '0px 0px 5px 0px'
}
},
removeProduct: {
position: 'absolute',
right: 0,
top: 0,
width: 30,
height: 30,
background: 'red',
color: '#fff',
border: 'none'
}
})
const Cart = () => {
const classes = useStyles()
const {productsCart} = useContext(ProductContext)
return (<div className={classes.cartWrap}>
<h4 className={classes.cartTitle}>Total: 0 total items.</h4>
<p className={classes.cartContent}>total: 0 VND</p>
<div className={classes.cartItems}>
{productsCart.map((item) => {
return (<div key={item.id} className={classes.productCart}>
<button className={classes.removeProduct}>x</button>
<h3>{item.name}</h3>
<div className="item-price">Price: {getLocalePrice(item.price)}</div>
<div className="item-quantity">Quantity: {item.quantity}</div>
</div>)
})}
</div>
</div>)
}
export default Cart
Giải thích: Tương tự như trên, ở component cart mình sử dụng hàm getLocalePrice để đổi cách hiển thị giá trị item.price, và dùng destructuring để định nghĩa là một object với thuộc tính productsCart là giá trị bên trong context, vì vậy, nó sẽ hứng được giá trị mới nhất được cập nhật theo context.
Khi này cấu trúc cây component sẽ có thể mô phỏng như sau:
- <UserContext.Provider value={user}>
- <div className={classes.App}>
- <ProductPage/>
- <ProductContext.Provider>
- <Products/>
- <ProductItem/>
- <Cart/>
- <Products/>
- <ProductContext.Provider>
- <ProductPage/>
- <div className={classes.App}>
Chúng ta thấy dưa theo cấu trúc này, UserContext nằm ở cấp cơ sở (root), nghĩa là toàn bộ ứng dụng có thể hiểu được dữ liệu của Context đang cung cấp thông qua compp, với ProductContext thì chỉ có các component con như Products,ProductItem,Cart là hiểu được thôi, nếu component của bạn nằm ngoài cấu trúc này, context sẽ không thể tham chiếu tới được.
Cấu trúc thư mục và Output:
Chúng ta thấy, khi bấm nút Add cart, sản phẩm đã được thêm vào giỏ hàng thành công, tuy nhiên vẫn có vấn đề ở đây:
- Cart chưa hiển thị chính xác số lượng item và totalPrice
- logic thêm giỏ cần được chuẩn hóa, đầu tiên là quantity bằng 1, khi bấm 2 lần vào nút add cart trên cùng 1 sản phẩm, giỏ hàng sẽ không thêm item mới mà phải tăng quantity sản phẩm hiện tại trong giỏ lên thành 2.
- Chưa có tính năng remove sản phẩm khi bấm vào dấu x
Xử lý nghiệp vụ quantity trên giỏ hàng
Trước hết, chúng ta sẽ fix nghiệp vụ quantity trên giỏ hàng, cũng tương tự với cách xử lý reducer như bài viết Cách quản lý state(trạng thái) bằng Hooks trên React Component đã mô tả, chúng ta chỉnh sửa file ProductItem.js như sau:
import React, { createContext, useContext } from "react";
import propTypes from "prop-types";
import { createUseStyles } from "react-jss";
import UserContext from "../../context/userContext";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../utitls/ultils";
const useStyles = createUseStyles({
productWrap: {
width: '50%',
padding: '0px 5px',
boxSizing: 'border-box',
marginBottom: 15,
position: 'relative'
},
productThumbnail: {
display: 'block',
position: 'relative',
overflow: 'hidden',
paddingTop: '100%',
border: '1px solid #e1e1e1',
'& img': {
width: '100%',
objectFit: 'cover',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
margin: 'auto'
}
},
productInfo: {
'& h3': {
margin: 0,
'& a': {
fontSize: 14,
textDecoration: 'none',
color: '#333'
}
},
'& p': {
margin: 0,
marginBottom: 10
}
},
btnAddCart: {
background: '#ffc856',
color: '#fff',
display: 'inline-block',
border: 5,
borderRadius: 5,
padding: 10,
cursor: 'pointer'
},
wishList : {
'&:before':{
content: '"\\2665"',
fontSize: 24,
color: 'red',
display: 'block',
position: 'absolute',
right: 15,
top: 0
}
}
})
const ProductItem = (props) => {
const classes = useStyles();
const user = useContext(UserContext);
const item = props.product;
const {setCart} = useContext(ProductContext)
const addCart = (event,item) =>{
setCart({product: item,type: 'add'})
}
return (<div key={item.id} className={classes.productWrap+' '+(user.wishlist.includes(item.id) ? classes.wishList : '')}>
<a href={item.source} target="_blank" rel="noreferrer" className={classes.productThumbnail}>
<img alt={item.name} src={item.thumbnail} />
</a>
<div className={classes.productInfo}>
<h3><a href={item.source} target="_blank" rel="noreferrer">{item.name}</a></h3>
<p>Price: {getLocalePrice(item.price)} VND</p>
<button type="button" onClick={(event) => addCart(event,item)} className={classes.btnAddCart}>Add cart</button>
</div>
</div>)
}
export default ProductItem
Giải thích: đóng gói tham số lại thành một object, lúc này, giá trị truyền vào reducer sẽ là một object với 2 thuộc tính bao gồm product và type. Kiểu dữ liệu của product là object, kiểu dữ liệu của type là string.
sửa cartReducer trong file layout/productpage/ProductPage.js như sau:
import React, {createContext,useReducer} from "react";
import { createUseStyles } from "react-jss";
import Cart from "../../components/cart/cart";
import Products from "../../components/products/Products";
import ProductContext from "../../context/ProductContext";
const useStyles = createUseStyles({
})
const cartReducer = (state,item) => {
const {product,type} = item;
const cart = [...state];
switch(type){
default:
case 'add':
// state value is array of product object
if(cart.length === 0){
product.quantity = 1;
}
let checkExist = false;
for(let i = 0 ; i < cart.length ; i++){
if(product.id === cart[i].id){
checkExist = true;
break;
}
}
product.quantity = checkExist ? product.quantity+1 : 1;
if(product.quantity > 1){
return [...state];
}
return [...state, product];
// break;
case 'remove':
break;
}
}
const ProductPage = () => {
const classes = useStyles();
const [productsCart,setCart] = useReducer(cartReducer,[])
return (
<ProductContext.Provider value={{productsCart,setCart}}>
<Cart/>
<Products/>
</ProductContext.Provider>
)
}
export default ProductPage;
Giải thích: Bây giờ dữ liệu được biến item hứng đã là một object bao gồm product và type là thuộc tính, chúng ta xử lý logic và return state mới cho ứng dụng.
Xử lý nghiệp vụ cập nhật lại total items và total price
import React, { useContext, useState } from "react";
import { createUseStyles } from "react-jss";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../utitls/ultils";
const useStyles = createUseStyles({
cartWrap: {
border: '1px solid #e1e1e1',
padding: 15,
marginBottom: 10
},
cartTitle: {
margin: 0
},
cartContent: {
margin: 0
},
cartItems: {
position: 'relative'
},
productCart: {
position: 'relative',
marginBottom: 15,
'& h3': {
margin: '0px 0px 5px 0px'
}
},
removeProduct: {
position: 'absolute',
right: 0,
top: 0,
width: 30,
height: 30,
background: 'red',
color: '#fff',
border: 'none'
}
})
const Cart = () => {
const classes = useStyles()
const {productsCart} = useContext(ProductContext)
let totalPrice = 0;
productsCart.forEach((elm,index) =>{
totalPrice += elm.price*elm.quantity
})
return (<div className={classes.cartWrap}>
<h4 className={classes.cartTitle}>Total: {productsCart.length} total items.</h4>
<p className={classes.cartContent}>total: {getLocalePrice(totalPrice)} VND</p>
<div className={classes.cartItems}>
{productsCart.map((item) => {
return (<div key={item.id} className={classes.productCart}>
<button className={classes.removeProduct}>x</button>
<h3>{item.name}</h3>
<div className="item-price">Price: {getLocalePrice(item.price)}</div>
<div className="item-quantity">Quantity: {item.quantity}</div>
</div>)
})}
</div>
</div>)
}
export default Cart
Ở đây, mình xử lý totalPrice theo logic cộng giá toàn bộ sản phẩm * số lượng, giải pháp này có thể hợp lý trong tình huống trên, còn nếu bạn không muốn xử lý logic bên trong cart mà sử dụng state để gán lại trạng thái cho cart, giúp việc bảo trì logic ứng dụng dễ dàng hơn, chúng ta sẽ có thể sử dụng cách sửa dữ liệu của ProductContext từ mảng product thành object bao gồm mảng product và totalPrice, hoặc tiếp tục tạo Cart Context, bọc bên ngoài các component, ở đây để giảm thiểu độ phức tạp và giúp mọi người hiểu hơn về Context, mình sẽ sử dụng cách 2.
Đầu tiên, tạo file context/CartContext.js:
import React, { createContext } from "react";
const CartContext = createContext();
export default CartContext
Đầu tiên, Cấu trúc lại file components/productpage/ProductPage.js như sau:
import React, { createContext, useReducer } from "react";
import { createUseStyles } from "react-jss";
import Cart from "../../components/cart/cart";
import Products from "../../components/products/Products";
import CartContext from "../../context/CartContext";
import ProductContext from "../../context/ProductContext";
const useStyles = createUseStyles({
})
const cartReducer = (state, item) => {
const { product, type } = item;
const cart = [...state];
switch (type) {
default:
case 'add':
// state value is array of product object
if (cart.length === 0) {
product.quantity = 1;
}
let checkExist = false;
for (let i = 0; i < cart.length; i++) {
if (product.id === cart[i].id) {
checkExist = true;
break;
}
}
product.quantity = checkExist ? product.quantity + 1 : 1;
if (product.quantity > 1) {
return [...state];
}
return [...state, product];
// break;
case 'remove':
break;
}
}
const totalCartReducer = (state, item) => {
const { product, type,productsCart } = item;
const cartTotal = state;
let checkExist = false;
for (let i = 0; i < productsCart.length; i++) {
if (product.id === productsCart[i].id) {
checkExist = true;
break;
}
}
switch (type) {
default:
case 'add':
cartTotal.totalPrice += product.price;
cartTotal.totalQuantity = checkExist ? productsCart.length : productsCart.length+1;
return {...cartTotal}
break;
case 'remove':
break;
}
return { ...state };
}
const ProductPage = () => {
const classes = useStyles();
const [productsCart, setCart] = useReducer(cartReducer, [])
const [totalCart, setTotalCart] = useReducer(totalCartReducer, { totalQuantity: 0, totalPrice: 0,productsCart: []})
return (
<ProductContext.Provider value={{ productsCart, setCart }}>
<CartContext.Provider value={{ totalCart, setTotalCart }}>
<Cart />
<Products />
</CartContext.Provider>
</ProductContext.Provider >
)
}
export default ProductPage;
Giải thích: ở đây, chúng ta thấy mình tạo ra một reducer mới với totalCart khởi tạo ban đầu là một object gồm 3 thuộc tính, totalQuantity,totalPrice và productsCart. Bước tiếp theo, sử dụng component Provider để truyền reducer này vào CartContext, và định nghĩa phương thức setTottalCart nhằm mục đích xử lý logic cho giỏ hàng.
Bước cuối cùng, sửa file components/productitem/ProductItem.js:
import React, { createContext, useContext } from "react";
import propTypes from "prop-types";
import { createUseStyles } from "react-jss";
import UserContext from "../../context/userContext";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../utitls/ultils";
import CartContext from "../../context/CartContext";
const useStyles = createUseStyles({
productWrap: {
width: '50%',
padding: '0px 5px',
boxSizing: 'border-box',
marginBottom: 15,
position: 'relative'
},
productThumbnail: {
display: 'block',
position: 'relative',
overflow: 'hidden',
paddingTop: '100%',
border: '1px solid #e1e1e1',
'& img': {
width: '100%',
objectFit: 'cover',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
margin: 'auto'
}
},
productInfo: {
'& h3': {
margin: 0,
'& a': {
fontSize: 14,
textDecoration: 'none',
color: '#333'
}
},
'& p': {
margin: 0,
marginBottom: 10
}
},
btnAddCart: {
background: '#ffc856',
color: '#fff',
display: 'inline-block',
border: 5,
borderRadius: 5,
padding: 10,
cursor: 'pointer'
},
wishList : {
'&:before':{
content: '"\\2665"',
fontSize: 24,
color: 'red',
display: 'block',
position: 'absolute',
right: 15,
top: 0
}
}
})
const ProductItem = (props) => {
const classes = useStyles();
const user = useContext(UserContext);
const item = props.product;
const {productsCart,setCart} = useContext(ProductContext)
const {setTotalCart} = useContext(CartContext)
const addCart = (event,item) =>{
setCart({product: item,type: 'add'})
setTotalCart({product:item,type:'add',productsCart: productsCart})
}
return (<div key={item.id} className={classes.productWrap+' '+(user.wishlist.includes(item.id) ? classes.wishList : '')}>
<a href={item.source} target="_blank" rel="noreferrer" className={classes.productThumbnail}>
<img alt={item.name} src={item.thumbnail} />
</a>
<div className={classes.productInfo}>
<h3><a href={item.source} target="_blank" rel="noreferrer">{item.name}</a></h3>
<p>Price: {getLocalePrice(item.price)} VND</p>
<button type="button" onClick={(event) => addCart(event,item)} className={classes.btnAddCart}>Add cart</button>
</div>
</div>)
}
export default ProductItem
Giải thích: bên trong sự kiện addCart, mình truyền dữ liệu vào bao gồm mảng sản phẩm đang tồn tại trong giỏ hàng, có một lưu ý ở đây, chúng ta nhìn lên file components/productpage/ProductPage.js, trong phương thức totalCartReducer, mảng được truyền vào vẫn sẽ là giá trị cũ của state nên mình vẫn cần xử lý lại logic để cộng trừ totalQuantity.
Cấu trúc thư mục vào output
Xử lý nghiệp vụ xóa sản phẩm trong giỏ hàng
Tiến hành chỉnh sửa file components/cart/cart.js như sau:
import React, { useContext, useState } from "react";
import { createUseStyles } from "react-jss";
import CartContext from "../../context/CartContext";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../utitls/ultils";
const useStyles = createUseStyles({
cartWrap: {
border: '1px solid #e1e1e1',
padding: 15,
marginBottom: 10
},
cartTitle: {
margin: 0
},
cartContent: {
margin: 0
},
cartItems: {
position: 'relative'
},
productCart: {
position: 'relative',
marginBottom: 15,
'& h3': {
margin: '0px 0px 5px 0px'
}
},
removeProduct: {
position: 'absolute',
right: 0,
top: 0,
width: 30,
height: 30,
background: 'red',
color: '#fff',
border: 'none'
}
})
const Cart = () => {
const classes = useStyles();
const {productsCart,setCart} = useContext(ProductContext);
const {totalCart,setTotalCart} = useContext(CartContext);
const {totalQuantity,totalPrice} = totalCart;
const removeProductCart = (event,item) => {
setCart({product: item,type: 'remove'})
setTotalCart({product:item,type:'remove',productsCart: productsCart})
}
return (<div className={classes.cartWrap}>
<h4 className={classes.cartTitle}>Total: {totalQuantity} total items.</h4>
<p className={classes.cartContent}>total: {getLocalePrice(totalPrice)} VND</p>
<div className={classes.cartItems}>
{productsCart.map((item) => {
return (<div key={item.id} className={classes.productCart}>
<button onClick={(event) => removeProductCart(event,item)} className={classes.removeProduct}>x</button>
<h3>{item.name}</h3>
<div className="item-price">Price: {getLocalePrice(item.price)}</div>
<div className="item-quantity">Quantity: {item.quantity}</div>
</div>)
})}
</div>
</div>)
}
export default Cart
Tiếp tục chỉnh sửa file layout/productpage/ProductPage.js:
import React, { createContext, useReducer } from "react";
import { createUseStyles } from "react-jss";
import Cart from "../../components/cart/cart";
import Products from "../../components/products/Products";
import CartContext from "../../context/CartContext";
import ProductContext from "../../context/ProductContext";
const useStyles = createUseStyles({
})
const cartReducer = (state, item) => {
const { product, type } = item;
const cart = [...state];
switch (type) {
default:
case 'add':
// state value is array of product object
if (cart.length === 0) {
product.quantity = 1;
}
let checkExist = false;
for (let i = 0; i < cart.length; i++) {
if (product.id === cart[i].id) {
checkExist = true;
break;
}
}
product.quantity = checkExist ? product.quantity + 1 : 1;
if (product.quantity > 1) {
return [...state];
}
return [...state, product];
// break;
case 'remove':
for (let i = 0; i < cart.length; i++) {
if (cart[i].id === product.id) {
cart.splice(i, 1);
break;
}
}
return [...cart]
break;
}
}
const totalCartReducer = (state, item) => {
const { product, type,productsCart } = item;
const cartTotal = state;
let checkExist = false;
for (let i = 0; i < productsCart.length; i++) {
if (product.id === productsCart[i].id) {
checkExist = true;
break;
}
}
switch (type) {
default:
case 'add':
cartTotal.totalPrice += product.price;
cartTotal.totalQuantity = checkExist ? productsCart.length : productsCart.length+1;
return {...cartTotal}
break;
case 'remove':
cartTotal.totalPrice -= product.price*product.quantity;
cartTotal.totalQuantity = productsCart.length-1;
return {...cartTotal}
break;
}
return { ...state };
}
const ProductPage = () => {
const classes = useStyles();
const [productsCart, setCart] = useReducer(cartReducer, [])
const [totalCart, setTotalCart] = useReducer(totalCartReducer, { totalQuantity: 0, totalPrice: 0,productsCart: []})
return (
<ProductContext.Provider value={{ productsCart, setCart }}>
<CartContext.Provider value={{ totalCart, setTotalCart }}>
<Cart />
<Products />
</CartContext.Provider>
</ProductContext.Provider >
)
}
export default ProductPage;
Sau khi xử lý, chúng ta đã có thể remove item trong giỏ hàng. Với ví dụ này, chúng ta thấy rằng cả ProductItem.js và cart.js đều có thể hiểu và sử dụng CartContext mặc dù 2 component này cùng cấp và không liên quan tới nhau, chúng ta cũng hoàn toàn không cần sử dụng props để truyền dữ liệu, đây là điểm mạnh giúp Context trong React có thể sử dụng thay thế props trong nhiều tình huống dự án lớn, công đoạn bảo trì phức tạp.
Tổng kết
Context là một công cụ mạnh mẽ và linh hoạt cung cấp khả năng lưu trữ và sử dụng dữ liệu trên một ứng dụng. Nó cung cấp cho chúng ta khả năng xử lý dữ liệu phân tán bằng các công cụ tích hợp mà không yêu cầu bất kỳ cài đặt hoặc cấu hình bên thứ ba bổ sung nào.
Việc tạo context có thể sử dụng lại trên rất quan trọng trên nhiều component chung, Context cho phép bạn tự do xây dựng các component có thể truy cập dữ liệu mà không cần lo lắng về cách chuyển dữ liệu qua các component trung gian hoặc cách lưu trữ dữ liệu trong một store tập trung mà không làm cho store quá lớn.