Quản lý state (trạng thái) trong React Class Component
- 03-10-2022
- Toanngo92
- 0 Comments
Trong React, trạng thái hay còn gọi là state có thể hiểu nôm na là một cấu trúc theo dõi dữ liệu thay đổi như thế nào theo thời gian trong ứng dụng của bạn. Quản lý trạng thái (state management) là một kỹ năng quan trọng trong React vì nó cho phép bạn tạo các component tương tác và các ứng dụng web động. State được sử dụng cho mọi thứ, từ theo dõi các trường nhập liệu trong form đến thu thập dữ liệu động từ một API. Trong hướng dẫn này, chúng ta sẽ xem qua một ví dụ về quản lý state trong các class component.
Trong React, trạng thái đề cập đến một cấu trúc theo dõi dữ liệu thay đổi như thế nào theo thời gian trong ứng dụng của bạn. Quản lý trạng thái là một kỹ năng quan trọng trong React vì nó cho phép bạn tạo các thành phần tương tác và các ứng dụng web động. Trạng thái được sử dụng cho mọi thứ, từ theo dõi đầu vào biểu mẫu đến thu thập dữ liệu động từ một API. Trong hướng dẫn này, bạn sẽ xem qua một ví dụ về quản lý trạng thái trên các thành phần dựa trên lớp.
Mặc dù trong thực tế, tài liệu React khuyến khích các nhà phát triển sử dụng React Hooks để quản lý trạng thái với các functional component khi viết mã mới, thay vì sử dụng các component class based. Mặc dù việc sử dụng React Hooks được coi là một phương pháp hiện đại hơn, nhưng vấn đề là cần phải hiểu rõ cách quản lý state trên các thành component class based. Hểu các khái niệm đằng sau quản lý state sẽ giúp bạn điều hướng và khắc phục sự cố quản lý state dựa trên class trong các cơ sở mã hiện có và giúp bạn quyết định khi nào quản lý state dựa trên class phù hợp hơn. Ngoài ra còn có một phương thức dựa trên class được gọi là componentDidCatch không có sẵn trong Hooks và sẽ yêu cầu thiết lập state bằng cách sử dụng các phương thức class (class methods).
Hướng dẫn này trước tiên sẽ chỉ cho bạn cách đặt state bằng cách sử dụng các giá trị tĩnh từ dữ liệu mẫu, mô phỏng việc dữ liệu sản phẩm được lấy từ API về và hiển thị để người dùng có thể tiến hành thao tác và thêm giỏ hàng & đặt hàng đơn giản.
Mục lục
Khởi tạo dự án và dữ liệu mẫu
Bước đầu tiên, tạo một dự án mới:
npx create-react-app reactstatedemo
cd react statedmo
// npm install --save prop-types
Vì ở bài hướng dẫn này, chúng ta tiếp cận giải pháp sử dụng state thông qua Component Class based, vì vậy với tất cả các component, mình sẽ viết theo cách sử dụng class
Tạo file layout/header/Header.js như sau:
import React, { Component } from "react"
import './Header.css'
class Header extends Component{
render(){
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'
class Footer extends Component{
render(){
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 './App.css';
import Header from './layout/header/Header';
import Footer from './layout/footer/Footer';
import { Component } from 'react';
class App extends Component{
render(){
return (
<div className="mainApp">
<Header />
<Footer />
</div>
)
}
}
export default App;
Tạo 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:
Tạo Component Products và sử dụng current state
Đầu tiên, tạo Component component/products/Products.js:
import React, { Component } from "react";
import data from "../../data/data";
import "./Product.css"
class Products extends Component {
constructor(props) {
super(props)
console.log(data)
}
state = {
products: data,
cart: [],
totalPrice: 0
}
render() {
return (<div className="products-outer">
<div className="cart-wrap">
<h4 className="cart-title">Total: {this.state.cart.length} total items.</h4>
<p className="cart-content">total: {this.state.totalPrice} VND</p>
</div>
<div className="products-wrap d-flex">
{
this.state.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">{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 './App.css';
import Header from './layout/header/Header';
import Footer from './layout/footer/Footer';
import { Component } from 'react';
import Products from './component/products/Products';
class App extends Component{
render(){
return (
<div className="mainApp">
<Header />
<div className='mainApp'>
<Products />
</div>
<Footer />
</div>
)
}
}
export default App;
Cấu trúc dự án và output:
Giải thích: Component Products được viết theo hướng class component based, theo hướng này, như chúng ta đã được tìm hiểu từ những bài viết đầu tiên, HTML sẽ được trả ra thông qua phương thức render() được kế thừa từ class Component của React. Trong ví dụ này, chúng ta tiếp cận với phương thức constructor(), đây là khái niệm hàm khởi tạo (hoặc hàm xây dựng) khi đối tượng được khởi tạo từ class, hàm này sẽ được chạy đầu tiên.Hàm constructor() này hứng vào biến props làm tham số bên trong (biến props sử dụng để hứng các giá trị từ attribute của thẻ ) Chúng ta thấy bên trong hàm constructor(), chúng ta gọi một hàm super(props), hàm này giúp component Products có thể lấy được toàn bộ giá trị của biến props và sử dụng tùy theo ý tưởng của lập trình viên.
Có hai giá trị trong các giá trị của component sẽ thay đổi trong khi thao tác đặt hàng: total items và total price. Thay vì sử dụng javascript tiêu chuẩn xử lý chúng một cách phức tạp, chúng ta sẽ chuyển chúng vào một đối tượng được gọi là state.
Trong mã nguồn phía trên, chúng ta thấy có một thuộc tính (propotype) của class Component là state, được gán giá trị là một object:
{
products: data, // dữ liệu mẫu lấy từ data.js
cart: [], // mảng rỗng
totalPrice: 0 // giá trị là 0
}
State của một React class là một thuộc tính đặc biệt kiểm soát việc hiển thị một trang. Khi bạn thay đổi trạng thái, React nhận diện được rằng thành phần đó đã được cập nhật và sẽ tự động hiển thị lại. Khi một thành phần hiển thị lại, nó sẽ sửa đổi đầu ra được render bao gồm thông tin cập nhật ở state. Trong ví dụ này, thành phần sẽ hiển thị lại bất cứ khi nào bạn thêm sản phẩm vào giỏ hàng hoặc xóa sản phẩm đó khỏi giỏ hàng. Tất nhiên, chúng ta có thể thêm các thuộc tính (propotype) khác vào một React class, nhưng chúng sẽ không có cùng khả năng thông dịch lại khi cập nhật dữ liệu (trigger re-rendering) như thuộc tính (propotype) state.
Giới thiệu phương thức setState()
Ở phần này, chúng ta sẽ cần thực hiện những công việc sau:
- Trước hết, ở chúng ta xét lại ouput phía trên, hiện tại giá sản phẩm và total price của giỏ hàng đang hiển thị ra giá trị số thuần túy, nhưng nếu làm vậy thì sẽ khá bất tiện cho người dùng, cách làm đúng là cần hiển thị ra giá sản phẩm dưới định dạng tiền tệ, vì vậy, chúng ta sẽ tạo một hàm để lọc giá trị này trước khi hiển thị cho người dùng.
- Mục tiêu của chúng ta là ở bước tếp theo, khi bấm nút add cart, dữ liệu bên trong <div className=”cart-wrap”> được thay đổi.
Với công việc đầu tiên, chúng ta sử dụng toLocaleString() của javascript để giải quyết.
Công việc tiếp theo là truyền dữ liệu đã thay đổi vào một đối tượng mới chứa các giá trị cập nhật với một phương thức đặc biệt có tên là setState(), phương thức này sẽ giúp đặt cho state của ứng dụng với giá trị đã được cập nhật. Để cập nhật state, các nhà phát triển React sử dụng một phương thức đặc biệt gọi là setState() được kế thừa từ Component class based. Phương thức setState() có thể lấy một đối tượng hoặc một hàm làm đối số đầu tiên. Nếu bạn có một giá trị tĩnh không cần tham chiếu trạng thái, tốt nhất bạn nên chuyển một đối tượng có chứa giá trị mới, vì nó dễ đọc hơn. Nếu bạn cần tham chiếu tới state hiện tại, bạn truyền vào một hàm để tránh bất kỳ tham chiếu nào đến state đã lỗi thời.
Quay trở lại với ví dụ, để bắt được sự kiện bấm nút, chúng ta sử dụng onClick để bắt sự kiện, code mẫu như sau:
import React, { Component } from "react";
import data from "../../data/data";
import "./Product.css"
class Products extends Component {
constructor(props) {
super(props)
console.log(data)
}
state = {
products: data,
cart: [],
totalPrice: 0
}
/* get locale price */
currencyOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}
getLocalePrice = (price) => {
return price.toLocaleString(price,this.currencyOptions)
}
/* get locale price */
addCart = (event,item) => {
console.log(item)
this.setState({
cart: [item],
totalPrice: item.price
})
}
render() {
return (<div className="products-outer">
<div className="cart-wrap">
<h4 className="cart-title">Total: {this.state.cart.length} total items.</h4>
<p className="cart-content">total: {this.getLocalePrice(this.state.totalPrice)} VND</p>
</div>
<div className="products-wrap d-flex">
{
this.state.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: {this.getLocalePrice(item.price)} VND</p>
<button onClick={(event) => this.addCart(event,item)} type="button" className="btn-addcart">Add cart</button>
</div>
</div>)
})
}
</div>
</div>)
}
}
export default Products
Output:
Lưu ý: function trong onclick bắt buộc phải viết theo phong cách arrow function, nếu không ReactJS sẽ báo lỗi, đây là vấn đề các developer mới bắt đầu thường hay gặp phải, hãy ghi nhớ nó để phòng tránh hoặc debug
Chúng ta xét với tình huống trên, khi click vào nút add cart, giỏ hàng đã thay đổi, tuy nhiên giá trị của mảng vẫn chỉ là mảng có 1 phần tử mang giá trị là object product, khi bấm add cart sản phẩm tiếp theo, giá trị giỏ hàng thay đổi theo sản phẩm vừa thao tác. Nhưng nếu nếu xử lý đúng, logic sẽ phải là thêm phần tử vào mảng mỗi khi bấm Add Cart, vì vậy chúng ta sửa source code như sau:
import React, { Component } from "react";
import data from "../../data/data";
import "./Product.css"
class Products extends Component {
constructor(props) {
super(props)
console.log(data)
}
state = {
products: data,
cart: [],
totalPrice: 0
}
/* get locale price */
currencyOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}
getLocalePrice = (price) => {
return price.toLocaleString(price,this.currencyOptions)
}
/* get locale price */
addCart = (event,item) => {
console.log(item)
this.setState((state) => ( {
cart: [...state.cart, item],
totalPrice: state.totalPrice + item.price
}))
}
render() {
return (<div className="products-outer">
<div className="cart-wrap">
<h4 className="cart-title">Total: {this.state.cart.length} total items.</h4>
<p className="cart-content">total: {this.getLocalePrice(this.state.totalPrice)} VND</p>
</div>
<div className="products-wrap d-flex">
{
this.state.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: {this.getLocalePrice(item.price)} VND</p>
<button onClick={(event) => this.addCart(event,item)} type="button" className="btn-addcart">Add cart</button>
</div>
</div>)
})
}
</div>
</div>)
}
}
export default Products
Output:
Sử dụng state để hoàn thiện tính năng giỏ hàng
Tới đây, chúng ta đã thêm được item vào giỏ hàng theo đúng ý đồ, tuy nhiên vẫn gặp một vấn đề, nếu bấm 2 lần Add Cart trên cùng một sản phẩm, item vẫn cộng thêm trong khi, thực tế, giỏ hàng nên hiển thị 1 item với số lượng bằng 2, vì vậy mình sẽ custom thêm giỏ hàng để hiển thị cho giống thực tế (các bạn hoàn toàn có thể optimize code lại để đẹp hơn để gia tăng hiệu năng và khả năng đọc code ):
import React, { Component } from "react";
import data from "../../data/data";
import "./Product.css"
class Products extends Component {
constructor(props) {
super(props)
console.log(data)
}
state = {
products: data,
cart: [],
totalPrice: 0
}
/* get locale price */
currencyOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}
getLocalePrice = (price) => {
return price.toLocaleString(price, this.currencyOptions)
}
/* get locale price */
addCart = (event, item) => {
if(this.state.cart.length === 0){
item.quantity = 1;
}
let check = false;
this.state.cart.forEach((cart_item,index) => {
if(cart_item.id === item.id ){
item.quantity++;
check = true;
}
})
if(check === false){
item.quantity = 1;
}
if(item.quantity == 1){
this.setState((state) => ({
cart: [...state.cart, item],
totalPrice: state.totalPrice + item.price
}))
}else{
this.setState((state) => ({
totalPrice: state.totalPrice + item.price
}))
}
}
render() {
return (<div className="products-outer">
<div className="cart-wrap">
<button type="button" className="open-cart">Cart</button>
<div className="cart-content-outer">
<h4 className="cart-title">Total: {this.state.cart.length} total items.</h4>
<p className="cart-content">total: {this.getLocalePrice(this.state.totalPrice)} VND</p>
<div className="cart-items">
{this.state.cart.map((item) => {
return (<div key={item.id} className="product-cart">
<h3>{item.name}</h3>
<div className="item-price">Price: {this.getLocalePrice(item.price)}</div>
<div className="item-quantity">Quantity: {item.quantity}</div>
</div>)
})}
</div>
</div>
</div>
<div className="products-wrap d-flex">
{
this.state.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: {this.getLocalePrice(item.price)} VND</p>
<button onClick={(event) => this.addCart(event, item)} type="button" className="btn-addcart">Add cart</button>
</div>
</div>)
})
}
</div>
</div>)
}
}
export default Products
Sửa file component/product/Product.css
.products-outer{
padding: 15px 0px;
}
.open-cart{
display: inline-block;
padding: 10px 15px;
background: #fa726c;
color: #fff;
border: none;
font-weight: bold;
text-transform: uppercase;
}
.cart-wrap{
margin-bottom: 10px;
justify-content: flex-end;
display: flex;
position: relative;
}
.cart-wrap .cart-title{
margin: 0px;
}
.cart-wrap .cart-content-outer{
position: absolute;
top: 100%;
/* visibility: hidden; */
/* opacity: 0; */
width: 300px;
border: 1px solid #e1e1e1;
padding: 15px;
background: #fff;
z-index: 1;
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;
}
Output:
Còn một vấn đề nữa của giỏ hàng cần xử lý: giỏ hàng ban đầu sẽ ẩn dữ liệu, khi bấm vào add cart sẽ hiện dữ liệu lên, bấm lại vào nút CART màu đỏ, nội dung cart lại được ẩn đi, chúng ta sửa mã nguồn như sau:
import React, { Component } from "react";
import data from "../../data/data";
import "./Product.css"
class Products extends Component {
constructor(props) {
super(props)
console.log(data)
}
state = {
products: data,
cart: [],
totalPrice: 0,
activeCart: false
}
/* get locale price */
currencyOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}
getLocalePrice = (price) => {
return price.toLocaleString(price, this.currencyOptions)
}
/* get locale price */
addCart = (event, item) => {
if(this.state.cart.length === 0){
item.quantity = 1;
}
let check = false;
this.state.cart.forEach((cart_item,index) => {
if(cart_item.id === item.id ){
item.quantity++;
check = true;
}
})
if(check === false){
item.quantity = 1;
}
if(item.quantity === 1){
this.setState((state) => ({
cart: [...state.cart, item],
totalPrice: state.totalPrice + item.price
}))
}else{
this.setState((state) => ({
totalPrice: state.totalPrice + item.price
}))
}
this.setState((state) => ({
activeCart: true
}))
}
toggleCart = (event) => {
this.setState((state) => ({
activeCart: state.activeCart ? false : true
}))
}
render() {
return (<div className="products-outer">
<div className="cart-wrap">
<button type="button" onClick={(event) => this.toggleCart(event)} className="open-cart">Cart</button>
<div className={`cart-content-outer ${this.state.activeCart ? 'active' : ''}`}>
<h4 className="cart-title">Total: {this.state.cart.length} total items.</h4>
<p className="cart-content">total: {this.getLocalePrice(this.state.totalPrice)} VND</p>
<div className="cart-items">
{this.state.cart.map((item) => {
return (<div key={item.id} className="product-cart">
<h3>{item.name}</h3>
<div className="item-price">Price: {this.getLocalePrice(item.price)}</div>
<div className="item-quantity">Quantity: {item.quantity}</div>
</div>)
})}
</div>
</div>
</div>
<div className="products-wrap d-flex">
{
this.state.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: {this.getLocalePrice(item.price)} VND</p>
<button onClick={(event) => this.addCart(event, item)} type="button" className="btn-addcart">Add cart</button>
</div>
</div>)
})
}
</div>
</div>)
}
}
export default Products
Output:
Phát triển tính năng đặt hàng đơn giản cho Products Component
Chúng ta thấy cart đã có thể toggle ẩn hiện được ngon lành, giờ đến những công đoạn cuối là remove item, và phát triển tính năng checkout đơn giản yêu cầu ngời dùng điền thông tin thanh toán cho giỏ hàng. Vì chúng ta tìm hiểu tới router và cách chia sẻ state với các component con trong ReactJS, nên tính năng checkout mình sẽ xử lý trực tiếp trên Products Component. Sửa mã nguồn component/products/Products.js như sau:
import React, { Component } from "react";
import data from "../../data/data";
import "./Product.css";
class Products extends Component {
constructor(props) {
super(props)
console.log(data)
}
state = {
products: data,
cart: [],
totalPrice: 0,
activeCart: false,
activeCheckOut: true
}
/* get locale price */
currencyOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}
getLocalePrice = (price) => {
return price.toLocaleString(price, this.currencyOptions)
}
/* get locale price */
addCart = (event, item) => {
if (this.state.cart.length === 0) {
item.quantity = 1;
}
let check = false;
this.state.cart.forEach((cart_item, index) => {
if (cart_item.id === item.id) {
item.quantity++;
check = true;
}
})
if (check === false) {
item.quantity = 1;
}
if (item.quantity === 1) {
this.setState((state) => ({
cart: [...state.cart, item],
totalPrice: state.totalPrice + item.price
}))
} else {
this.setState((state) => ({
totalPrice: state.totalPrice + item.price
}))
}
this.setState((state) => ({
activeCart: true
}))
}
toggleCart = (event) => {
this.setState((state) => ({
activeCart: state.activeCart ? false : true
}))
}
render() {
return (<div className="products-outer">
<div className="cart-wrap">
<button type="button" onClick={(event) => this.toggleCart(event)} className="open-cart">Cart</button>
<div className={`cart-content-outer ${this.state.activeCart ? 'active' : ''}`}>
<h4 className="cart-title">Total: {this.state.cart.length} total items.</h4>
<p className="cart-content">total: {this.getLocalePrice(this.state.totalPrice)} VND</p>
<div className="cart-items">
{this.state.cart.map((item) => {
return (<div key={item.id} className="product-cart">
<h3>{item.name}</h3>
<div className="item-price">Price: {this.getLocalePrice(item.price)}</div>
<div className="item-quantity">Quantity: {item.quantity}</div>
</div>)
})}
</div>
<div className="button-checkout-wrap">
<button className="btn-checkout">Checkout</button>
</div>
</div>
</div>
<div className={`checkout-wrap ${this.state.activeCheckOut ? 'active' : ''}`}>
<button className="close-checkout">X</button>
<h3>Your oders</h3>
<h4 className="cart-title">Total: {this.state.cart.length} total items.</h4>
<p className="cart-content">total: {this.getLocalePrice(this.state.totalPrice)} VND</p>
<div className="cart-items">
{this.state.cart.map((item) => {
return (<div key={item.id} className="product-cart">
<h3>{item.name}</h3>
<div className="item-price">Price: {this.getLocalePrice(item.price)}</div>
<div className="item-quantity">Quantity: {item.quantity}</div>
</div>)
})}
</div>
<h3>Your Information</h3>
<div className="checkout-info">
<label>Your name:</label>
<input type="text" placeholder="Your name" />
<label>Your Address:</label>
<input type="text" placeholder="Your address" />
<label>Phone number:</label>
<input type="text" placeholder="Phone number" />
</div>
<button className="btn-addcart">Pay Now</button>
</div>
<div className="products-wrap d-flex">
{
this.state.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: {this.getLocalePrice(item.price)} VND</p>
<button onClick={(event) => this.addCart(event, item)} type="button" className="btn-addcart">Add cart</button>
</div>
</div>)
})
}
</div>
</div>)
}
}
export default Products
Sửa file component/products/Products.css
.products-outer{
padding: 15px 0px;
}
.open-cart{
display: inline-block;
padding: 10px 15px;
background: #fa726c;
color: #fff;
border: none;
font-weight: bold;
text-transform: uppercase;
}
.cart-wrap{
margin-bottom: 10px;
justify-content: flex-end;
display: flex;
position: relative;
}
.cart-wrap .cart-title{
margin: 0px;
}
.cart-wrap .cart-content-outer{
position: absolute;
top: 100%;
visibility: hidden;
opacity: 0;
width: 300px;
border: 1px solid #e1e1e1;
padding: 15px;
background: #fff;
z-index: 1;
margin: 0px;
transition: all 0.3s;
}
.cart-wrap .cart-content-outer.active{
visibility: visible;
opacity: 1;
}
.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;
}
.btn-addcart{
background: #ffc856;
color: #fff;
display: inline-block;
border: 5px;
border-radius: 5px;
padding: 10px;
cursor: pointer;
}
.checkout-wrap {
background: #fff;
z-index: 2;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 425px;
margin: auto;
visibility: hidden;
opacity: 0;
}
.checkout-wrap.active{
visibility: visible;
opacity: 1;
}
.checkout-info label,.checkout-info input{
margin-bottom: 5px;
width: 100%;
display: block;
}
.checkout-info input{
box-sizing: border-box;
border: 1px solid #e1e1e1;
padding: 10px;
}
.close-checkout{
position: absolute;
right: 0px;
top: 21px;
border: none;
display: block;
width: 40px;
height: 40px;
cursor: pointer;
background: red;
color: #fff;
border-radius: 5px;
}
Output:
Mình đã CSS cho thẻ checkout đè lên các component còn lại, CSS ẩn hiện dựa theo class active, class này xác định bằng thuộc tính của state có tên activeCheckOut, chúng ta sẽ tiếp tục xử lý các sự kiện:
- khi bấm vào nut checkout của giỏ hàng, box này mới hiển thị ra.
- khi bấm pay now sẽ thông báo order successfully kèm thông tin đơn hàng.
- bấm nút tắt, checkout sẽ lại ẩn đi.
Tiếp tục sửa mã nguồn component/products/Products.js như sau:
import React, { Component } from "react";
import data from "../../data/data";
import "./Product.css";
class Products extends Component {
constructor(props) {
super(props)
console.log(data)
}
state = {
products: data,
cart: [],
totalPrice: 0,
activeCart: false,
activeCheckOut: false,
cusName: '',
cusAddress: '',
cusPhone: ''
}
/* get locale price */
currencyOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}
getLocalePrice = (price) => {
return price.toLocaleString(price, this.currencyOptions)
}
/* get locale price */
addCart = (event, item) => {
if (this.state.cart.length === 0) {
item.quantity = 1;
}
let check = false;
this.state.cart.forEach((cart_item, index) => {
if (cart_item.id === item.id) {
item.quantity++;
check = true;
}
})
if (check === false) {
item.quantity = 1;
}
if (item.quantity === 1) {
this.setState((state) => ({
cart: [...state.cart, item],
totalPrice: state.totalPrice + item.price
}))
} else {
this.setState((state) => ({
totalPrice: state.totalPrice + item.price
}))
}
this.setState((state) => ({
activeCart: true
}))
}
toggleCart = (event) => {
this.setState((state) => ({
activeCart: state.activeCart ? false : true
}))
}
activeCheckOut = (event) => {
this.setState((state) => ({
activeCheckOut: true
}))
}
disableCheckOut = (event) => {
this.setState((state) => ({
activeCheckOut: false
}))
}
bindingCustomerInfo = (event) => {
// console.log(event.target.name)
const targetname = event.target.name;
this.state[targetname] = event.target.value
}
submitCheckOut = (event) =>{
//destructuring
const {cusName,cusAddress,cusPhone,...state} = this.state;
alert(`Your order successfully placed !\n Thanks ${cusName}, Your items will delivery to ${cusAddress} !`);
this.setState((state) => ({
cart: [],
totalPrice: 0,
activeCart: false,
activeCheckOut: false,
cusName: '',
cusAddress: '',
cusPhone: ''
}))
}
render() {
return (<div className="products-outer">
<div className="cart-wrap">
<button type="button" onClick={(event) => this.toggleCart(event)} className="open-cart">Cart</button>
<div className={`cart-content-outer ${this.state.activeCart ? 'active' : ''}`}>
<h4 className="cart-title">Total: {this.state.cart.length} total items.</h4>
<p className="cart-content">total: {this.getLocalePrice(this.state.totalPrice)} VND</p>
<div className="cart-items">
{this.state.cart.map((item) => {
return (<div key={item.id} className="product-cart">
<h3>{item.name}</h3>
<div className="item-price">Price: {this.getLocalePrice(item.price)}</div>
<div className="item-quantity">Quantity: {item.quantity}</div>
</div>)
})}
</div>
<div className="button-checkout-wrap">
<button onClick={(event) => this.activeCheckOut(event)} className="btn-addcart">Checkout</button>
</div>
</div>
</div>
<div className={`checkout-wrap ${this.state.activeCheckOut ? 'active' : ''}`}>
<button className="close-checkout" onClick={(event) => this.disableCheckOut(event)}>X</button>
<h3>Your oders</h3>
<h4 className="cart-title">Total: {this.state.cart.length} total items.</h4>
<p className="cart-content">total: {this.getLocalePrice(this.state.totalPrice)} VND</p>
<div className="cart-items">
{this.state.cart.map((item) => {
return (<div key={item.id} className="product-cart">
<h3>{item.name}</h3>
<div className="item-price">Price: {this.getLocalePrice(item.price)}</div>
<div className="item-quantity">Quantity: {item.quantity}</div>
</div>)
})}
</div>
<h3>Your Information</h3>
<form className="checkout-info">
<label>Your name:</label>
<input onChange={(event) => this.bindingCustomerInfo(event)} name="cusName" type="text" placeholder="Your name" />
<label>Your Address:</label>
<input onChange={(event) => this.bindingCustomerInfo(event)} name="cusAddress" type="text" placeholder="Your address" />
<label>Phone number:</label>
<input onChange={(event) => this.bindingCustomerInfo(event)} name="cusPhone" type="text" placeholder="Phone number" />
</form>
<button onClick={(event) => this.submitCheckOut(event)} className="btn-addcart">Pay Now</button>
</div>
<div className="products-wrap d-flex">
{
this.state.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: {this.getLocalePrice(item.price)} VND</p>
<button onClick={(event) => this.addCart(event, item)} type="button" className="btn-addcart">Add cart</button>
</div>
</div>)
})
}
</div>
</div>)
}
}
export default Products
Output:
Tạm thời là như vậy, qua bài này chúng ta đã hiểu rõ hơn về cách quản lý state trong component class based, ở bài tiếp theo, mình sẽ giới thiệu về cách quản lý state trong component functional, cách này được recomend bởi React documentation offical, chúc bạn sớm PRO !
Bài tập
Bài tập 1: Tính tổng đơn giản
Tạo một ứng dụng với 2 ô nhập liệu để người dùng nhập số:
- Hiển thị tổng của hai số này ngay khi người dùng thay đổi giá trị.
Yêu cầu:
- Sử dụng
useState
để quản lý giá trị của hai ô nhập liệu. - Hiển thị tổng được tính động.
Bài tập 2: Biểu mẫu nhập liệu
Tạo một biểu mẫu nhập thông tin người dùng gồm:
- Tên, Email, Tuổi (name, email, age).
- Khi nhấn nút "Submit", hiển thị thông tin đã nhập.
Yêu cầu:
- Sử dụng
useState
để quản lý trạng thái của các trường nhập liệu. - Hiển thị thông tin người dùng sau khi nhấn "Submit".
Bài tập 3: Bộ chọn ngôn ngữ
Tạo một ứng dụng với menu thả xuống để chọn ngôn ngữ:
- Danh sách các ngôn ngữ: "English", "Vietnamese", "Spanish".
- Hiển thị "Hello" theo ngôn ngữ được chọn.
Yêu cầu:
- Sử dụng
useState
để lưu trạng thái ngôn ngữ hiện tại. - Hiển thị nội dung thay đổi tương ứng với ngôn ngữ.
Bài tập 4: Bộ đếm thời gian (Timer)
Tạo một ứng dụng đếm thời gian đơn giản:
- Hiển thị số giây đã trôi qua.
- Có các nút: "Bắt đầu", "Dừng", "Đặt lại".
Yêu cầu:
- Sử dụng
useState
để quản lý thời gian. - Sử dụng
useEffect
để cập nhật thời gian mỗi giây khi đếm đang chạy.
Bài tập 5: Hiển thị/Ẩn đoạn văn bản
Tạo một ứng dụng hiển thị một đoạn văn bản và một nút:
- Nút "Hiển thị/Ẩn" (Toggle) để hiển thị hoặc ẩn đoạn văn bản.
Yêu cầu:
- Sử dụng
useState
để quản lý trạng thái ẩn/hiển thị. - Đoạn văn bản chỉ hiển thị khi trạng thái là "hiển thị".