Cách quản lý state(trạng thái) bằng Hooks trên React Component
- 04-10-2022
- Toanngo92
- 0 Comments
Trong phát triển React, việc theo dõi dữ liệu ứng dụng của bạn thay đổi như thế nào theo thời gian được gọi là quản lý state. Bằng cách quản lý state của ứng dụng, bạn sẽ có thể tạo các ứng dụng động phản hồi thông tin nhập của người dùng. Có nhiều phương pháp quản lý state trong React, bao gồm quản lý trạng thái dựa trên lớp và các thư viện của bên thứ ba như Redux. Trong hướng dẫn này, bạn sẽ quản lý state trên các thành phần chức năng bằng phương pháp được khuyến khích bởi tài liệu React chính thức: Hooks.
Hooks là một tập hợp rộng các công cụ chạy các chức năng tùy chỉnh khi props của một thành phần thay đổi. Vì phương pháp quản lý trạng thái này không yêu cầu bạn sử dụng các lớp, nên các nhà phát triển có thể sử dụng Hooks để viết mã ngắn hơn, dễ đọc hơn, dễ chia sẻ và duy trì. Một trong những điểm khác biệt chính giữa Hooks và quản lý state class based là không có đối tượng duy nhất nào nắm giữ tất cả trạng thái. Thay vào đó, bạn có thể chia trạng thái thành nhiều phần mà bạn có thể cập nhật độc lập.
Trong suốt hướng dẫn này, bạn sẽ học cách thiết lập trạng thái bằng cách sử dụng useState và useReducer Hooks. UseState Hook có giá trị khi đặt giá trị mà không tham chiếu đến trạng thái hiện tại; useReducer Hook rất hữu ích khi bạn cần tham chiếu đến một giá trị trước đó hoặc khi bạn có các hành động khác nhau yêu cầu các thao tác dữ liệu phức tạp. Để khám phá các cách thiết lập trạng thái khác nhau này, chúng ta sẽ viết lại component Products từ bài Quản lý state (trạng thái) trong React Class Component và phân tích sự khác nhau giữa 2 phong cách code.
Chúng ta tiếp tục khởi tạo dự án mới:
npx create-react-app reactstatefunctional
cd reactstatefunctional
// npm install --save prop-types
Tạo file layout/header/Header.js như sau:
import React, { Component } from "react"
import './Header.css'
const Header = () => {
return (<header className="d-flex">
<div className="logo">web888.vn</div>
<nav>
<ul className="d-flex">
<li>Home page</li>
<li>About</li>
<li>Contact</li>
</ul>
</nav>
</header>)
}
export default Header
Tạo file layout/header/Header.css như sau:
header{
background: #47b7e5;
color: #fff;
justify-content: space-between;
padding: 15px;
text-transform: uppercase;
font-weight: bold;
font-size: 12px;
}
nav ul{
list-style: none;
padding: 0px;
margin: 0px -5px;
justify-content: flex-end;
}
nav ul li{
margin: 0px 5px;
}
Tạo file layout/footer/Footer.js như sau:
import React, { Component } from "react";
import './Footer.css'
const Footer = () => {
return (<footer>
Copyright @ web888.vn
</footer>)
}
export default Footer
Tạo file layout/footer/Footer.css như sau:
footer{
padding: 15px;
font-size: 12px;
color: #fff;
background: #fa726c;
}
Tạo file data/data.js sử dụng làm dữ liệu mẫu:
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 Header from './layout/header/Header';
import Footer from './layout/footer/Footer';
import './App.css';
function App() {
return (
<div className="mainApp">
<Header />
<Footer />
</div>
)
}
export default App;
Sửa file App.css
.d-flex{
display: flex;
flex-wrap: wrap;
}
.mainApp{
max-width: 425px;
margin: auto;
}
Output và cấu trúc thư mục:
Chúng ta thấy phía trên, là phong cách functional component, output không thay đổi, chỉ có sự khác biệt ở cách viết sử dụng function nên code trở nên ngắn gọn hơn, tuy nhiên nếu chưa tiếp cận phong cách class component thì có phần khó hiểu hơn.
Mục lục
Tạo Component Products với phong cách functional
Đầu tiên, tạo Component component/products/Products.js:
import React, {useState} from "react";
import data from '../../data/data'
import './Products.css'
const Products = (props) => {
const [cart,setCart] = useState([])
const [totalPrice,setTotalPrice] = useState(0)
const [products,setData] = useState(data)
return (<div className="products-outer">
<div className="cart-wrap">
<h4 className="cart-title">Total: {cart.length} total items.</h4>
<p className="cart-content">total: {totalPrice} VND</p>
</div>
<div className="products-wrap d-flex">
{
products.map((item) => {
return (<div key={item.id} className="product-wrap">
<a href={item.source} target="_blank" rel="noreferrer" className="product-thumbnail">
<img alt={item.name} src={item.thumbnail} />
</a>
<div className="product-info">
<h3><a href={item.source} target="_blank" rel="noreferrer">{item.name}</a></h3>
<p>Price: {item.price}</p>
<button type="button" className="btn-addcart">Add cart</button>
</div>
</div>)
})
}
</div>
</div>)
}
export default Products
Tạo file component/products/Products.css
.products-outer{
padding: 15px 0px;
}
.cart-wrap{
border: 1px solid #e1e1e1;
padding: 15px;
margin-bottom: 10px;
}
.cart-wrap .cart-title{
margin: 0px;
}
.cart-wrap .cart-content{
margin: 0px;
}
.products-wrap{
flex-wrap: wrap;
margin: 0px -5px;
}
.product-wrap{
width: 50%;
padding: 0px 5px;
box-sizing: border-box;
}
.product-wrap .product-thumbnail {
display: block;
position: relative;
overflow: hidden;
padding-top: 100%;
border: 1px solid #e1e1e1;
}
.product-wrap{
margin-bottom: 15px;
}
.product-wrap .product-thumbnail img{
width: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
.product-wrap .product-info h3{
margin: 0px;
}
.product-wrap .product-info p{
margin: 0px;
margin-bottom: 10px;
}
.product-wrap .product-info h3 a{
font-size: 14px;
text-decoration: none;
color: #333;
}
.product-wrap .product-info .btn-addcart{
background: #ffc856;
color: #fff;
display: inline-block;
border: 5px;
border-radius: 5px;
padding: 10px;
cursor: pointer;
}
Sửa nội dung file App.js
import Header from './layout/header/Header';
import Footer from './layout/footer/Footer';
import Products from './component/products/Products';
import './App.css';
function App() {
return (
<div className="mainApp">
<Header />
<div className='mainApp'>
<Products />
</div>
<Footer />
</div>
)
}
export default App;
Cấu trúc dự án và output:
Chúng ta thấy rằng output vẫn không thay đổi, import {useState} Hook là điều khác biệt đầu tiên, và bên trong function Products, các biến được khai báo giá trị thông qua hook này thay cho thuộc tính this.state trong component class based, các biến lúc return cũng được rút ngắn bớt đi giúp code trở nên ngắn gọn hơn nhiều.
Terminal có cảnh báo phần tử thứ 2 của các mảng đã destructure chưa được sử dụng, đây chính là các phương thức sử dụng cho việc setState sau này thông qua hook, mà mình sẽ nói ở ý sau.
React cung cấp một số Hook mà bạn có thể nhập trực tiếp từ gói React. Theo quy ước, React Hooks bắt đầu bằng từ use, chẳng hạn như useState, useContext và useReducer. Hầu hết các thư viện của bên thứ ba đều tuân theo cùng một quy ước. Ví dụ, Redux có useSelector và useStore Hook.
Hook là các hàm cho phép bạn chạy các hành động như một phần của vòng đời React (React life cycle).
Hook được kích hoạt bởi các hành động khác hoặc bởi các thay đổi trong props của một component và được sử dụng để khởi tạo dữ liệu hoặc để kích hoạt các thay đổi tiếp theo. Ví dụ: useState Hook tạo ra một phần dữ liệu state cùng với một hàm để thay đổi phần dữ liệu đó và kích hoạt kết xuất lại (trigger re-rendering). Nó sẽ tạo một đoạn mã động và nối vào vòng đời bằng cách kích hoạt hiển thị khi dữ liệu thay đổi. Trong thực tế, điều đó có nghĩa là bạn có thể lưu trữ các phần dữ liệu động trong các biến bằng cách sử dụng useState Hook.
Ví dụ: trong thành phần này, bạn có hai phần dữ liệu sẽ thay đổi dựa trên hành động của người dùng: giỏ hàng và tổng chi phí. Mỗi trong số này có thể được lưu trữ ở trạng thái bằng cách sử dụng Hook ở trên.
Tiếp theo, giống như ví dụ bài trước, chúng ta cần đưa định dạng giá sản phẩm và giỏ hàng thành tiền tệ để hiển thị. Sửa file component/products/Products.js như sau:
import React, {useState} from "react";
import data from '../../data/data'
import './Products.css'
const Products = (props) => {
const [cart,setCart] = useState([])
const [totalPrice,setTotalPrice] = useState(0)
const [products,setData] = useState(data)
/* get locale price */
const currencyOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}
const getLocalePrice = (price) => {
return price.toLocaleString(price,currencyOptions)
}
/* get locale price */
return (<div className="products-outer">
<div className="cart-wrap">
<h4 className="cart-title">Total: {cart.length} total items.</h4>
<p className="cart-content">total: {getLocalePrice(totalPrice)} VND</p>
</div>
<div className="products-wrap d-flex">
{
products.map((item) => {
return (<div key={item.id} className="product-wrap">
<a href={item.source} target="_blank" rel="noreferrer" className="product-thumbnail">
<img alt={item.name} src={item.thumbnail} />
</a>
<div className="product-info">
<h3><a href={item.source} target="_blank" rel="noreferrer">{item.name}</a></h3>
<p>Price: {getLocalePrice(item.price)} VND</p>
<button type="button" className="btn-addcart">Add cart</button>
</div>
</div>)
})
}
</div>
</div>)
}
export default Products
Output:
Thay đổi state ứng dụng với hook useState
Không giống như các component class based, chúng ta không thể cập nhật nhiều thuộc tính state cùng lúc bằng một lệnh gọi hàm duy nhất. Thay vào đó, bạn phải gọi từng hàm riêng lẻ. Điều này có nghĩa là có sự tách biệt nhiều hơn, giúp giữ cho các đối tượng trạng thái được tập trung.
Sửa file Products.js như sau:
import React, {useState} from "react";
import data from '../../data/data'
import './Products.css'
const Products = (props) => {
const [cart,setCart] = useState([])
const [totalPrice,setTotalPrice] = useState(0)
const [products,setData] = useState(data)
/* get locale price */
const currencyOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}
const getLocalePrice = (price) => {
return price.toLocaleString(price,currencyOptions)
}
/* get locale price */
const addCart = (event,item) => {
setCart([item])
setTotalPrice(item.price)
}
return (<div className="products-outer">
<div className="cart-wrap">
<h4 className="cart-title">Total: {cart.length} total items.</h4>
<p className="cart-content">total: {getLocalePrice(totalPrice)} VND</p>
</div>
<div className="products-wrap d-flex">
{
products.map((item) => {
return (<div key={item.id} className="product-wrap">
<a href={item.source} target="_blank" rel="noreferrer" className="product-thumbnail">
<img alt={item.name} src={item.thumbnail} />
</a>
<div className="product-info">
<h3><a href={item.source} target="_blank" rel="noreferrer">{item.name}</a></h3>
<p>Price: {getLocalePrice(item.price)} VND</p>
<button onClick={(event) => addCart(event,item)} type="button" className="btn-addcart">Add cart</button>
</div>
</div>)
})
}
</div>
</div>)
}
export default Products
Output:
Vì không cần tham chiếu this context, bạn có thể sử dụng arrow function hoặc function. Cả hai đều hoạt động tốt như nhau và chúng ta có thể quyết định muốn sử dụng kiểu nào. Bạn thậm chí có thể bỏ qua việc xác định một hàm bổ sung và chuyển trực tiếp hàm vào thuộc tính onClick như sau:
import React, { useState } from "react";
import data from '../../data/data'
import './Products.css'
const Products = (props) => {
const [cart, setCart] = useState([])
const [totalPrice, setTotalPrice] = useState(0)
const [products, setData] = useState(data)
/* get locale price */
const currencyOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}
const getLocalePrice = (price) => {
return price.toLocaleString(price, currencyOptions)
}
/* get locale price */
// const addCart = (event,item) => {
// setCart([item])
// setTotalPrice(item.price)
// }
return (<div className="products-outer">
<div className="cart-wrap">
<h4 className="cart-title">Total: {cart.length} total items.</h4>
<p className="cart-content">total: {getLocalePrice(totalPrice)} VND</p>
</div>
<div className="products-wrap d-flex">
{
products.map((item) => {
return (<div key={item.id} className="product-wrap">
<a href={item.source} target="_blank" rel="noreferrer" className="product-thumbnail">
<img alt={item.name} src={item.thumbnail} />
</a>
<div className="product-info">
<h3><a href={item.source} target="_blank" rel="noreferrer">{item.name}</a></h3>
<p>Price: {getLocalePrice(item.price)} VND</p>
<button onClick={(event) => {
setCart(['item'])
setTotalPrice(item.price)
}} type="button" className="btn-addcart">Add cart</button>
</div>
</div>)
})
}
</div>
</div>)
}
export default Products
Để có sự khác biệt hơn so với bài viết trước, mình sẽ bổ sung thêm tính năng hiển thị danh sách sản phẩm, kèm nút xóa sản phẩm khỏi giỏ hàng, như sau:
import React, { useState } from "react";
import data from '../../data/data'
import './Products.css'
const Products = (props) => {
const [cart, setCart] = useState([])
const [totalPrice, setTotalPrice] = useState(0)
const [products, setData] = useState(data)
/* get locale price */
const currencyOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}
const getLocalePrice = (price) => {
console.log(price)
return price.toLocaleString(price, currencyOptions)
}
/* get locale price */
const addCart = (event, item) => {
setCart([item])
setTotalPrice(item.price)
}
const removeCartItem = (event,item) => {
setCart([])
setTotalPrice(0)
}
return (<div className="products-outer">
<div className="cart-wrap">
<h4 className="cart-title">Total: {cart.length} total items.</h4>
<p className="cart-content">total: {getLocalePrice(totalPrice)} VND</p>
<div className="cart-items">
{cart.map((item) => {
return (<div key={item.id} className="product-cart">
<button onClick={(event) => removeCartItem(event,item)} className="remove-product" >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>
<div className="products-wrap d-flex">
{
products.map((item) => {
return (<div key={item.id} className="product-wrap">
<a href={item.source} target="_blank" rel="noreferrer" className="product-thumbnail">
<img alt={item.name} src={item.thumbnail} />
</a>
<div className="product-info">
<h3><a href={item.source} target="_blank" rel="noreferrer">{item.name}</a></h3>
<p>Price: {getLocalePrice(item.price)} VND</p>
<button onClick={(event) => addCart(event, item)} type="button" className="btn-addcart">Add cart</button>
</div>
</div>)
})
}
</div>
</div>)
}
export default Products
Output:
Vấn đề ở đây vẫn là add cart chỉ thêm được một sản phẩm, và khi xóa cart giỏ hàng trạng thái trở thành mảng rỗng, đây không phải là logic mình mong muốn nên chúng ta tiếp tục custom 2 hàm addCart và removeCartItem.
Thay đổi state ứng dụng với hook useReducer
Việc sử dụng useState vẫn ổn khi bạn làm việc với dữ liệu chỉ có giá trị đơn. Tuy nhiên, một trang sản phẩm điển hình sẽ có nhiều mặt hàng mà bạn có thể thêm vào giỏ hàng và bạn sẽ muốn có thể cập nhật giỏ hàng trong khi vẫn giữ các mặt hàng trước đó.
Trong bước này, bạn sẽ cập nhật state bằng current state. Chúng ta sẽ phát triển thêm trang sản phẩm của mình để bao gồm một số sản phẩm và bạn sẽ tạo các chức năng cập nhật giỏ hàng và tổng số dựa trên các giá trị hiện tại. Để cập nhật các giá trị, bạn sẽ sử dụng cả useState Hook và Hook mới được gọi là useReducer.
Vì React có thể tối ưu hóa mã bằng cách gọi các hành động không đồng bộ, bạn sẽ muốn đảm bảo rằng hàm của mình có quyền truy cập vào state trạng thái cập nhật mới nhất. Cách cơ bản nhất để giải quyết vấn đề này là truyền một hàm cho hàm thiết lập state thay vì một giá trị. Nói cách khác, thay vì gọi setState (5), bạn sẽ gọi setState (prev=> prev +5).
Chúng ta bắt đầu bằng tính năng thêm sản phẩm vào giỏ hàng Sửa file component/products/Product.js như sau:
import React, { useReducer, useState } from "react";
import data from '../../data/data'
import './Products.css'
const cartReducer = (state, product) => {
return [...state, product]
}
function totalPriceReducer(state, price) {
return state + price;
}
const Products = (props) => {
const [cart, setCart] = useReducer(cartReducer, [])
const [totalPrice, setTotalPrice] = useReducer(totalPriceReducer,0)
const [products, setData] = useState(data)
/* get locale price */
const currencyOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}
const getLocalePrice = (price) => {
console.log(price)
return price.toLocaleString(price, currencyOptions)
}
/* get locale price */
const addCart = (event, item) => {
setCart(item)
setTotalPrice(item.price)
}
const removeCartItem = (event, item) => {
}
return (<div className="products-outer">
<div className="cart-wrap">
<h4 className="cart-title">Total: {cart.length} total items.</h4>
<p className="cart-content">total: {getLocalePrice(totalPrice)} VND</p>
<div className="cart-items">
{cart.map((item) => {
return (<div key={item.id} className="product-cart">
<button onClick={(event) => removeCartItem(event, item)} className="remove-product" >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>
<div className="products-wrap d-flex">
{
products.map((item) => {
return (<div key={item.id} className="product-wrap">
<a href={item.source} target="_blank" rel="noreferrer" className="product-thumbnail">
<img alt={item.name} src={item.thumbnail} />
</a>
<div className="product-info">
<h3><a href={item.source} target="_blank" rel="noreferrer">{item.name}</a></h3>
<p>Price: {getLocalePrice(item.price)} VND</p>
<button onClick={(event) => addCart(event, item)} type="button" className="btn-addcart">Add cart</button>
</div>
</div>)
})
}
</div>
</div>)
}
export default Products
Output:
Warning: Encountered two children with the same key, `0001`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
at div
at div
at div
at Products (http://localhost:3000/static/js/bundle.js:138:76)
at div
at div
at App
printWarning @ react-dom.development.js:86
error @ react-dom.development.js:60
warnOnInvalidKey @ react-dom.development.js:15293
reconcileChildrenArray @ react-dom.development.js:15330
reconcileChildFibers @ react-dom.development.js:15821
reconcileChildren @ react-dom.development.js:19174
updateHostComponent @ react-dom.development.js:19924
beginWork @ react-dom.development.js:21618
beginWork$1 @ react-dom.development.js:27426
performUnitOfWork @ react-dom.development.js:26557
workLoopSync @ react-dom.development.js:26466
renderRootSync @ react-dom.development.js:26434
performSyncWorkOnRoot @ react-dom.development.js:26085
flushSyncCallbacks @ react-dom.development.js:12042
(anonymous) @ react-dom.development.js:25651
Sản phẩm đã được thêm vào mảng, tuy nhiên logic chưa đúng khi bấm 2 lần vào nút add cart của sản phẩm, nếu đúng thì số lượng cart item trong mảng phải được giữ nguyên, và bên trong mỗi cart item cần có thuộc tính quantity để biểu diễn số lượng sản phẩm được thêm vào giỏ. Chúng ta tiếp tục chỉnh sửa như sau:
import React, { useReducer, useState } from "react";
import data from '../../data/data'
import './Products.css'
const cartReducer = (state, product) => {
let cart = [...state];
let checkExist = false;
if(cart.length === 0){
product.quantity = 1;
}
for(let i=0; i < cart.length; i++){
if(cart[i].id === product.id){
checkExist = true;
break;
}
}
product.quantity = checkExist ? product.quantity+1 : 1;
if(product.quantity > 1){
return state
}else{
return [...state, product]
}
}
function totalPriceReducer(state, price) {
return state + price;
}
const Products = (props) => {
const [cart, setCart] = useReducer(cartReducer, [])
const [totalPrice, setTotalPrice] = useReducer(totalPriceReducer,0)
const [products, setData] = useState(data)
/* get locale price */
const currencyOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}
const getLocalePrice = (price) => {
return price.toLocaleString(price, currencyOptions)
}
/* get locale price */
const addCart = (event, item) => {
setCart(item)
setTotalPrice(item.price)
}
const removeCartItem = (event, item) => {
}
return (<div className="products-outer">
<div className="cart-wrap">
<h4 className="cart-title">Total: {cart.length} total items.</h4>
<p className="cart-content">total: {getLocalePrice(totalPrice)} VND</p>
<div className="cart-items">
{cart.map((item) => {
return (<div key={item.id} className="product-cart">
<button onClick={(event) => removeCartItem(event, item)} className="remove-product" >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>
<div className="products-wrap d-flex">
{
products.map((item) => {
return (<div key={item.id} className="product-wrap">
<a href={item.source} target="_blank" rel="noreferrer" className="product-thumbnail">
<img alt={item.name} src={item.thumbnail} />
</a>
<div className="product-info">
<h3><a href={item.source} target="_blank" rel="noreferrer">{item.name}</a></h3>
<p>Price: {getLocalePrice(item.price)} VND</p>
<button onClick={(event) => addCart(event, item)} type="button" className="btn-addcart">Add cart</button>
</div>
</div>)
})
}
</div>
</div>)
}
export default Products
Trong tình huống này, mình đã xử lý cart bằng cách dùng một vòng lặp, khác với lần trước là không sử dụng method forEach là phương thức builtin của mảng trong javascript, lí do để khi kiểm tra index, mình sẽ break luôn vòng lặp, giúp cải thiện hiệu năng (break không sử dụng được trong forEach), ở productQuantity, mình không check if else như bài trước nữa mà sử dụng ternary operator hay gọi nôm na dễ hiểu là if else một dòng để mã sạch hơn.
Tuy vậy, mặc dù logic mình xác định đã chính xác nhưng chúng ta vẫn bắt gặp lỗi sau:
Đây là một lỗi sẽ rất dễ gặp phải khi mới bắt đầu khiến bạn bối rối, nguyên nhân reducer chạy lại 2 lần trong Restrict Mode, bạn có thể xem chi tiết tại bài viết React strict mode (chế độ phát triển) trong React và trong tình huống này cách giải quyết là bỏ <React.StrictMode> đang bọc component App trong file src/index.js:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
// <React.StrictMode>
<App />
// </React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
Output:
Vậy là tính năng thêm giỏ hàng có vẻ đã hoàn thiện, chúng ta tiếp tục phát triển tính năng remove cho giỏ hàng thông qua reducer như sau:
import React, { useReducer, useState } from "react";
import data from '../../data/data'
import './Products.css'
const cartReducer = (state, actionobj) => {
let cart = [...state];
switch (actionobj.type) {
default:
case 'add':
let checkExist = false;
if (cart.length === 0) {
actionobj.product.quantity = 1;
}
for (let i = 0; i < cart.length; i++) {
if (cart[i].id === actionobj.product.id) {
checkExist = true;
break;
}
}
actionobj.product.quantity = checkExist ? actionobj.product.quantity + 1 : 1;
if (actionobj.product.quantity > 1) {
return state
}
return [...state, actionobj.product]
case 'remove':
for (let i = 0; i < cart.length; i++) {
if (cart[i].id === actionobj.product.id) {
cart.splice(i, 1);
break;
}
}
return cart
}
}
function totalPriceReducer(state, productobj) {
switch (productobj.type) {
default:
case 'add':
return state + productobj.product.price;
case 'remove':
return state - productobj.product.price*productobj.product.quantity
}
}
const Products = (props) => {
const [cart, setCart] = useReducer(cartReducer, [])
const [totalPrice, setTotalPrice] = useReducer(totalPriceReducer, 0)
const [products, setData] = useState(data)
/* get locale price */
const currencyOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}
const getLocalePrice = (price) => {
return price.toLocaleString(price, currencyOptions)
}
/* get locale price */
const addCart = (event, item) => {
setCart({product: item, type: 'add'})
setTotalPrice({ product: item, type: 'add' })
}
const removeCartItem = (event, item) => {
setCart({product: item, type: 'remove'})
setTotalPrice({ product: item, type: 'remove' })
}
return (<div className="products-outer">
<div className="cart-wrap">
<h4 className="cart-title">Total: {cart.length} total items.</h4>
<p className="cart-content">total: {getLocalePrice(totalPrice)} VND</p>
<div className="cart-items">
{cart.map((item) => {
return (<div key={item.id} className="product-cart">
<button onClick={(event) => removeCartItem(event, item)} className="remove-product" >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>
<div className="products-wrap d-flex">
{
products.map((item) => {
return (<div key={item.id} className="product-wrap">
<a href={item.source} target="_blank" rel="noreferrer" className="product-thumbnail">
<img alt={item.name} src={item.thumbnail} />
</a>
<div className="product-info">
<h3><a href={item.source} target="_blank" rel="noreferrer">{item.name}</a></h3>
<p>Price: {getLocalePrice(item.price)} VND</p>
<button onClick={(event) => addCart(event, item)} type="button" className="btn-addcart">Add cart</button>
</div>
</div>)
})
}
</div>
</div>)
}
export default Products
Với tình huống này, chúng ta phải sửa lại cả hàm addCart và removeCartItem đều cần được sửa lại, truyền thẳng cả đối tượng product vào kèm type như các thuộc tính của object, và kiểm tra thuộc tính type cho reducer để xác định được hành động cụ thể như thêm hay xóa. Điểm hay ở giải pháp này là state tập trung trạng thái hiện tại, mọi cập nhật đều xử lý chỉ thông qua reducer làm tăng tính nhất quán và tập trung của mã nguồn, vốn là điểm yếu của javascript tiêu chuẩn khi xử lý dom, hay cập nhật dữ liệu sau khi có sự kiện xảy ra.
Với tính năng thanh toán, mình sẽ không demo thêm ở bài viết này, các bạn có thể quay lại bài viết trước và phát triển thêm để hiểu hơn vấn đề !
Bằng cách sử dụng useReducer Hook, chúng ta giữ cho phần thân component chính của mình được tổ chức tốt và dễ đọc, vì logic phức tạp để phân tích cú pháp và nối mảng nằm ngoài component. Bạn cũng có thể di reducer ra bên ngoài các component nếu bạn muốn sử dụng lại nó hoặc bạn có thể tạo một Hook tùy chỉnh để sử dụng trên nhiều component. Bạn có thể tạo các Hook tùy chỉnh dưới dạng các hàm xung quanh các Hook cơ bản như useState, useReducer hoặc useEffect.
Hooks cung cấp cho bạn giải pháp để di chuyển logic state vào và ra khỏi component, trái ngược với các component class based, nơi bạn thường bị ràng buộc với component. Lợi thế này giúp nó có thể mở rộng sang các component khác. Vì Hook là các hàm, bạn có thể import chúng vào nhiều component thay vìsử dụng kế thừa hoặc các dạng phức tạp khác của class component.
Trong bước này, bạn đã học cách thiết lập state bằng cách sử dụng state hiện tại. Bạn đã tạo một component cập nhật state bằng cách sử dụng cả useState và useReducer Hooks, đồng thời bạn đã cấu trúc lại component thành các Hook khác nhau để ngăn lỗi và cải thiện khả năng sử dụng lại.
Nếu gặp vấn đề gì, đừng ngần ngại để lại bình luận để team có thể hỗ trợ bạn tốt hơn ! Chúc bạn sớm hiểu khái niệm này !
Bài tập
Tạo một functional Component có tên Counter với hai nút và giá trị bộ đếm được hiển thị trên màn hình.
Sử dụng hook useState để quản lý trạng thái bộ đếm.
Khi nhấp vào nút "Increment", hãy tăng bộ đếm lên 1.
Khi nhấp vào nút "Decrement", hãy giảm bộ đếm đi 1.
Khi nhấp vào nút "Reset", bộ đếm = 0.
Kết xuất component Counter trong component App.