Gọi API trong React với useEffect Hook
- 17-10-2022
- Toanngo92
- 0 Comments
Trong quá trình phát triển React , các Application Programming Interface (API) là một phần không thể thiếu trong các thiết kế ứng dụng một trang (SPA) . API là cách chính để các ứng dụng giao tiếp theo chương trình với máy chủ để cung cấp cho người dùng dữ liệu thời gian thực và lưu các thay đổi của người dùng. Trong các ứng dụng React, bạn sẽ sử dụng các API để tải tùy chọn của người dùng, hiển thị thông tin người dùng, tìm nạp cấu hình hoặc thông tin bảo mật và lưu các thay đổi trạng thái ứng dụng.
Trong hướng dẫn này, chúng ta sẽ sử dụng useEffect và useState Hooks để tìm nạp và hiển thị thông tin trong một ứng dụng mẫu, sử dụng máy chủ JSON làm API cục bộ cho mục đích thử nghiệm. Bạn sẽ tải thông tin khi một thành phần được render lần đầu tiên và lưu dữ liệu đầu vào của khách hàng bằng một API. Bạn cũng sẽ làm mới dữ liệu khi người dùng thực hiện thay đổi và tìm hiểu cách bỏ qua các yêu cầu API khi một thành phần ngắt kết nối.
Để bắt đầu bài này, mình sẽ quay lại với ứng dụng danh sách sản phẩm, nhưng lần này sẽ cài đặt thêm một API REST cục bộ json server, sử dụng để test dữ liệu mẫu. JSON Server sẽ là API cục bộ của bạn và sẽ cung cấp cho bạn một URL (endpoint) trực tiếp để thực hiện các request GET và POST, các bạn có thể tham khảo thêm tại bài viết: Services communicate và khái niệm REST API. Với API cục bộ, bạn có cơ hội tạo mẫu và thử nghiệm các component trong khi team backend khác phát triển các API trực tiếp.
Mục lục
Khởi tạo dự án
npx create-react-app reactapidemo
cd reactasyncdemo
npm install react-jss
npm install react-proptypes
npm install --save-dev json-server
json-server tạo một API dựa trên một JavaScript Object . Các khóa là các đường dẫn URL và các giá trị được trả về dưới dạng phản hồi. Bạn lưu trữ cục bộ đối tượng JavaScript cục bộ và sử dụng như sau.
Tạo file db.json ở cấp cơ sở của ứng dụng (cùng cấp với file package.json):
{
"user": {
"id": 1,
"name": "toanngo123",
"wishlist": ["0001","0003","0005"]
},
"products": [
{
"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"
}
],
"orders": [
]
}
Mở file package.json và thêm mã vào cấu hình script như sau:
{
"name": "reactapidemo",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-jss": "^10.9.2",
"react-proptypes": "^1.0.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"api": "json-server db.json -p 3333 --delay 1500"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"json-server": "^0.17.0"
}
}
Bước tiếp theo,, mở một terminal mới và gõ lệnh:
npm run api
Output:
reactapidemo@0.1.0 api
> json-server src/db/db.json -p 3333 --delay 1500
\{^_^}/ hi!
Loading src/db/db.json
Done
Resources
http://localhost:3333/user
http://localhost:3333/products
Home
http://localhost:3333
Từ đây, chúng ta có thể test ngay bằng Postman hoặc dùng lệnh để gửi request:
curl -H 'Content-Type: application/json' -X GET http://localhost:3333/user
curl -H 'Content-Type: application/json' -X GET http://localhost:3333/products
Chúng ta tiếp tục tiến hành build lại dự án cũ như sau:
Tạo các component layout Header, Footer
Tạo file layout/Header/Header.js:
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
Tạo file layout/Footer/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
Tạo 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;
Tạo Ultils cho dự án
Tạo file ultils/ultils.js:
const currencyOptions = {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}
const getLocalePrice = (price) => {
return price.toLocaleString(price, currencyOptions)
}
export default getLocalePrice
Tạo các file context
Tạo file context CartContext.js:
import React, { createContext } from "react";
const CartContext = createContext();
export default CartContext
Tạo file context ProductContext.js:
import { createContext } from "react";
const UserContext = createContext();
export default UserContext;
Tạo file context UserContext.js:
import { createContext } from "react";
const UserContext = createContext();
export default UserContext;
Fetch dữ liệu từ API
Trong bước này, chúng ta sẽ tìm nạp danh sách sản phẩm bằng useEffect Hook. Chúng ta sẽ tạo service để sử dụng các API trong các thư mục riêng biệt và gọi dịch vụ đó trong các component React của mình. Sau khi bạn gọi service, bạn sẽ lưu dữ liệu bằng useState Hook và hiển thị kết quả trong component của bạn.
Tạo các service lấy dữ liệu từ backend json-server
Tạo file services/UserService.js:
const getUser = () => {
return fetch('http://localhost:3333/user').then((data) => data.json())
}
export default getUser;
Tạo file services/ProductService.js
const getProducts = () => {
return fetch('http://localhost:3333/products').then((data) => data.json())
}
export default getProducts;
Ngoài fetch, còn có các thư viện phổ biến khác như Axios có thể cung cấp cho bạn một interface trực quan và sẽ cho phép bạn thêm các header mặc định hoặc thực hiện các hành động khác trên dịch vụ. Nhưng tất nhiên fetch vẫn sẽ hoạt động cho hầu hết các yêu cầu.
Sửa các file trong thư mục layout
Sửa file App.js:
import React, { lazy, Suspense } from 'react';
import './App.css';
import { createUseStyles } from 'react-jss';
// import Header from './layout/Header/Header';
import Footer from './layout/Footer/Footer';
import UserContext from './context/UserContext';
import ProductPage from './layout/ProductPage/ProductPage';
import { useEffect, useReducer } from 'react';
import getUser from './services/UserService';
const Header = lazy(() => import('./layout/Header/Header'));
const useStyles = createUseStyles({
App: {
maxWidth: 425,
margin: 'auto'
}
});
const userReducer = (state, item) => {
const { id, name, wishlist } = item;
return { ...state, id, name, wishlist };
}
function App() {
const [user, setUser] = useReducer(userReducer, {
id: 0,
name: '',
wishlist: []
});
const [show, toggle] = useReducer(state => true, false);
let mounted = true;
useEffect(() => {
getUser().then((result) => {
if (mounted) {
setUser(result);
toggle()
}
})
return () => {
mounted = false;
}
}, [user])
const classes = useStyles()
return (
<UserContext.Provider value={user}>
<div className={classes.App}>
<Suspense fallback={<p>Loading</p>}>
<Header />
</Suspense>
<ProductPage />
<Footer />
</div>
</UserContext.Provider>
);
}
export default App;
Tạo các components
Tạo 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 "../../ultils/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
Tạo file components/Cart/Cart.js:
import React, { useContext, useEffect, useState } from "react";
import { createUseStyles } from "react-jss";
import CartContext from "../../context/CartContext";
import CheckOutContext from "../../context/CheckOutcontext";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../ultils/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'
},
btnCheckOut:{
color: '#fff',
border: 5,
cursor: 'pointer',
display: 'inline-block',
padding: 10,
background: '#ffc856',
borderRadius: 5
}
})
const Cart = () => {
const classes = useStyles();
const {productsCart,setCart} = useContext(ProductContext);
const {checkOut,setCheckOut} = useContext(CheckOutContext);
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})
}
const activeCheckOut = (event) => {
setCheckOut({type:'active',item_obj: {activeCheckOut:true,totalQuantity: totalQuantity,totalPrice: totalPrice,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>
{productsCart.length > 0 && <button onClick={(event) => activeCheckOut(event)} className={classes.btnCheckOut}>checkout</button>}
</div>)
}
export default Cart
Tạo file components/CheckOut/CheckOut.js:
import React, { useContext, useReducer, useState } from "react";
import { createUseStyles } from "react-jss";
import CartContext from "../../context/CartContext";
import CheckOutContext from "../../context/CheckOutcontext";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../ultils/ultils";
const useStyles = createUseStyles({
formCheckOut: {
border: '1px solid #e1e1e1',
padding: 15,
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
width: 425,
margin: 'auto',
zIndex: 1,
boxSizing: 'border-box',
background: '#fff'
},
inputWrap: {
'& input': {
width: '100%',
display: 'block',
padding: 10,
border: '1px solid #e1e1e1',
boxSizing: 'border-box',
marginBottom: 5
},
'& select': {
width: '100%',
display: 'block',
padding: 10,
border: '1px solid #e1e1e1',
boxSizing: 'border-box',
marginBottom: 5
}
},
checkboxWrap: {
marginBottom: 5
},
closeCheckOut: {
position: 'absolute',
right: 15,
background: 'red',
color: '#fff',
border: 'none',
height: 30,
width: 30,
cursor: 'pointer'
}
});
const formReducer = (state, item) => {
if(item.reset){
return {
cusName: '',
cusphone: '',
paymentMethod: 'cod',
products: [],
extraorder: [],
totalCheckOutPrice: 0
}
}
return { ...state, [item.name]: item.value }
}
const CheckOut = () => {
const classes = useStyles();
const [submitting, setSubmitting] = useState(false);
const [formData, setFormData] = useReducer(formReducer, {
cusName: '',
cusphone: '',
paymentMethod: 'cod',
products: [],
extraorder: [],
totalCheckOutPrice: 0
});
const { checkOut, setCheckOut } = useContext(CheckOutContext);
const { totalCart } = useContext(CartContext);
const { totalQuantity, totalPrice } = totalCart;
const { productsCart } = useContext(ProductContext);
const [totalPriceCheckOut, setTotalPriceCheckOut] = useState(totalPrice);
const handleSubmit = (event) => {
event.preventDefault();
formData.products = productsCart
setSubmitting(true);
setTimeout(() => {
setSubmitting(false);
setFormData({reset: true});
}, 5000);
console.log(formData);
alert("Form submited");
}
const handleChange = (event) => {
let totalExtraPrice = 0;
const isCheckbox = event.target.type == 'checkbox' ? true : false;
if (!isCheckbox) {
setFormData({
name: event.target.name,
value: event.target.value,
});
} else {
const lst_checked = document.querySelectorAll(`input[name=${event.target.name}]:checked`);
let lst_checked_data = [];
lst_checked.forEach((elm,index) => {
console.log(elm);
lst_checked_data.push(elm.value)
})
lst_checked.forEach((elm, index) => {
totalExtraPrice += parseInt(elm.value);
})
setFormData({
name: event.target.name,
value: lst_checked_data,
});
}
/* set checkout price */
setTotalPriceCheckOut(totalPrice + totalExtraPrice)
setFormData({
name: 'totalCheckOutPrice',
value: totalPriceCheckOut,
});
/* end set checkout price */
}
const handleCloseCheckOut = (event) => {
setCheckOut({ type: 'close' })
}
return (<form onSubmit={(event) => handleSubmit(event)} className={classes.formCheckOut}>
<button onClick={(event) => handleCloseCheckOut(event)} type="button" className={classes.closeCheckOut}>x</button>
<h4 className={classes.cartTitle}>Total: {totalQuantity} total items.</h4>
<p className={classes.cartContent}>Total cart price: {getLocalePrice(totalPrice)} VND</p>
<hr />
<p className={classes.cartContent}><strong>Total Order: {getLocalePrice(totalPriceCheckOut)} VND</strong></p>
<div className={classes.cartItems}>
{productsCart.map((item) => {
return (<div key={item.id} className={classes.productCart}>
<h3>{item.name}</h3>
<div className="item-price">Price: {getLocalePrice(item.price)}</div>
<div className="item-quantity">Quantity: {item.quantity}</div>
</div>)
})}
</div>
<div>
<p>Total order price: {0}</p>
</div>
<p><strong>Customer name:</strong></p>
<div className={classes.inputWrap}>
<input disabled={submitting ? true : false} onChange={(event) => handleChange(event)} type="text" name="cusName" value={formData.cusName ? formData.cusName : ''} />
</div>
<p><strong>Customer phone:</strong></p>
<div className={classes.inputWrap}>
<input disabled={submitting ? true : false} onChange={(event) => handleChange(event)} type="tel" name="cusPhone" value={formData.cusPhone ? formData.cusPhone : ''} />
</div>
<p><strong>Payment method:</strong></p>
<div className={classes.inputWrap}>
<select disabled={submitting ? true : false} defaultValue={formData.paymentMethod == 'paypal' ? 'paypal' : 'cod' } onChange={(event) => handleChange(event)} name="paymentMethod">
<option value="paypal">Paypal</option>
<option value="cod">Cash on delivery</option>
</select>
</div>
<p><strong>Extra Order:</strong></p>
<div className={classes.checkboxWrap}>
<input disabled={submitting ? true : false} checked={formData.extraorder.includes('100') ? true : false} onChange={(event) => handleChange(event)} type="checkbox" name="extraorder" value="100" /> <label>Flash delivery</label>
<input disabled={submitting ? true : false} checked={formData.extraorder.includes('200') ? true : false} onChange={(event) => handleChange(event)} type="checkbox" name="extraorder" value="200" /> <label>Gift Box</label>
<input disabled={submitting ? true : false} checked={formData.extraorder.includes('300') ? true : false} onChange={(event) => handleChange(event)} type="checkbox" name="extraorder" value="300" /> <label>Laptop Bags & Mouse</label>
</div>
<div className={classes.inputWrap}>
<input disabled={submitting ? true : false} type="submit" />
</div>
{submitting &&
<div>Submtting Form...</div>
}
</form>)
}
export default CheckOut
Tạo file component/product/products.js:
import React, { useEffect, useState } from "react";
import propTypes from "prop-types";
import { createUseStyles } from "react-jss";
import ProductItem from "../ProductItem/ProductItem"
import getProducts from "../../services/ProductService";
const useStyles = createUseStyles({
productsOuter: {
maxWidth: 425,
margin: 'auto'
},
productsWrap: {
marginTop: 10,
marginLeft: -5,
marginRight: -5
}
})
const Products = () => {
const classes = useStyles();
const [products, setProducts] = useState([])
let mounted = true;
useEffect(() => {
getProducts().then((result) => {
if (mounted) {
setProducts(result);
}
})
return () => {
mounted = false;
}
},[products])
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
Output sẽ trả ra tương tự các bài trước, chỉ có duy nhất sự khác biệt là lần này chúng ta mô phỏng tình huống gửi request sát với thực tế hơn.
Gửi dữ liệu tới API
Ở ví dụ lần này, chúng ta sẽ làm tiếp tính năng khi người dùng submit đơn hàng, request sẽ được gửi tới backend, và theo logic ở đây chúng ta sẽ cần thay đổi dữ liệu trong CSDL, tình huống này là trường “orders” bên trong file db.json hiện tại đang lưu trữ dữ liệu là một mảng rỗng.
Sửa file components/CheckOut/CheckOut.js:
import React, { useContext, useReducer, useState } from "react";
import { createUseStyles } from "react-jss";
import CartContext from "../../context/CartContext";
import CheckOutContext from "../../context/CheckOutcontext";
import ProductContext from "../../context/ProductContext";
import getLocalePrice from "../../ultils/ultils";
const useStyles = createUseStyles({
formCheckOut: {
border: '1px solid #e1e1e1',
padding: 15,
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
width: 425,
margin: 'auto',
zIndex: 1,
boxSizing: 'border-box',
background: '#fff'
},
inputWrap: {
'& input': {
width: '100%',
display: 'block',
padding: 10,
border: '1px solid #e1e1e1',
boxSizing: 'border-box',
marginBottom: 5
},
'& select': {
width: '100%',
display: 'block',
padding: 10,
border: '1px solid #e1e1e1',
boxSizing: 'border-box',
marginBottom: 5
}
},
checkboxWrap: {
marginBottom: 5
},
closeCheckOut: {
position: 'absolute',
right: 15,
background: 'red',
color: '#fff',
border: 'none',
height: 30,
width: 30,
cursor: 'pointer'
}
});
const formReducer = (state, item) => {
if(item.reset){
return {
cusName: '',
cusphone: '',
paymentMethod: 'cod',
products: [],
extraorder: [],
totalCheckOutPrice: 0
}
}
return { ...state, [item.name]: item.value }
}
const CheckOut = () => {
const classes = useStyles();
const [submitting, setSubmitting] = useState(false);
const [formData, setFormData] = useReducer(formReducer, {
cusName: '',
cusphone: '',
paymentMethod: 'cod',
products: [],
extraorder: [],
totalCheckOutPrice: 0
});
const { checkOut, setCheckOut } = useContext(CheckOutContext);
const { totalCart } = useContext(CartContext);
const { totalQuantity, totalPrice } = totalCart;
const { productsCart } = useContext(ProductContext);
const [totalPriceCheckOut, setTotalPriceCheckOut] = useState(totalPrice);
const handleSubmit = (event) => {
event.preventDefault();
formData.products = productsCart
setSubmitting(true);
fetch('http://localhost:3333/orders/',{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData),
}).then((data) => {
setSubmitting(false);
setFormData({reset: true});
alert("Order successfully !")
}).catch( error => console.error('error:', error) );
}
const handleChange = (event) => {
let totalExtraPrice = 0;
const isCheckbox = event.target.type == 'checkbox' ? true : false;
if (!isCheckbox) {
setFormData({
name: event.target.name,
value: event.target.value,
});
} else {
const lst_checked = document.querySelectorAll(`input[name=${event.target.name}]:checked`);
let lst_checked_data = [];
lst_checked.forEach((elm,index) => {
console.log(elm);
lst_checked_data.push(elm.value)
})
lst_checked.forEach((elm, index) => {
totalExtraPrice += parseInt(elm.value);
})
setFormData({
name: event.target.name,
value: lst_checked_data,
});
}
/* set checkout price */
setTotalPriceCheckOut(totalPrice + totalExtraPrice)
setFormData({
name: 'totalCheckOutPrice',
value: totalPriceCheckOut,
});
/* end set checkout price */
}
const handleCloseCheckOut = (event) => {
setCheckOut({ type: 'close' })
}
return (<form onSubmit={(event) => handleSubmit(event)} className={classes.formCheckOut}>
<button onClick={(event) => handleCloseCheckOut(event)} type="button" className={classes.closeCheckOut}>x</button>
<h4 className={classes.cartTitle}>Total: {totalQuantity} total items.</h4>
<p className={classes.cartContent}>Total cart price: {getLocalePrice(totalPrice)} VND</p>
<hr />
<p className={classes.cartContent}><strong>Total Order: {getLocalePrice(totalPriceCheckOut)} VND</strong></p>
<div className={classes.cartItems}>
{productsCart.map((item) => {
return (<div key={item.id} className={classes.productCart}>
<h3>{item.name}</h3>
<div className="item-price">Price: {getLocalePrice(item.price)}</div>
<div className="item-quantity">Quantity: {item.quantity}</div>
</div>)
})}
</div>
<div>
<p>Total order price: {0}</p>
</div>
<p><strong>Customer name:</strong></p>
<div className={classes.inputWrap}>
<input disabled={submitting ? true : false} onChange={(event) => handleChange(event)} type="text" name="cusName" value={formData.cusName ? formData.cusName : ''} />
</div>
<p><strong>Customer phone:</strong></p>
<div className={classes.inputWrap}>
<input disabled={submitting ? true : false} onChange={(event) => handleChange(event)} type="tel" name="cusPhone" value={formData.cusPhone ? formData.cusPhone : ''} />
</div>
<p><strong>Payment method:</strong></p>
<div className={classes.inputWrap}>
<select disabled={submitting ? true : false} defaultValue={formData.paymentMethod == 'paypal' ? 'paypal' : 'cod' } onChange={(event) => handleChange(event)} name="paymentMethod">
<option value="paypal">Paypal</option>
<option value="cod">Cash on delivery</option>
</select>
</div>
<p><strong>Extra Order:</strong></p>
<div className={classes.checkboxWrap}>
<input disabled={submitting ? true : false} checked={formData.extraorder.includes('100') ? true : false} onChange={(event) => handleChange(event)} type="checkbox" name="extraorder" value="100" /> <label>Flash delivery</label>
<input disabled={submitting ? true : false} checked={formData.extraorder.includes('200') ? true : false} onChange={(event) => handleChange(event)} type="checkbox" name="extraorder" value="200" /> <label>Gift Box</label>
<input disabled={submitting ? true : false} checked={formData.extraorder.includes('300') ? true : false} onChange={(event) => handleChange(event)} type="checkbox" name="extraorder" value="300" /> <label>Laptop Bags & Mouse</label>
</div>
<div className={classes.inputWrap}>
<input disabled={submitting ? true : false} type="submit" />
</div>
{submitting &&
<div>Submtting Form...</div>
}
</form>)
}
export default CheckOut
Sau khi thực hiện, chúng ta tiến hành kiểm thử tính năng và kiểm tra lại file db.json xem dữ liệu đã được cập nhật chưa.
Output:
Sử dụng useRef Hook trong React
Ở ví dụ phía trên trong bài này, các bạn thấy mình cố tình đưa biến mount ra bên ngoài useEffect Hook, như một thuộc tính của component, nhằm mục đích ngăn re-render component và gọi lại API nhiều lần. Vấn đề gọi lại ở đây nằm ở chỗ sau khi gọi dữ liệu trên backend, dữ liệu sẽ luôn trả về kiểu Promise và không thể so khớp với biến cũ nên ứng dụng sẽ liên tục gọi useEffect dẫn đến lỗi infinitive loop. Tuy nhiên, React phát ra cảnh báo:
Line 31:23: Assignments to the 'mounted' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect react-hooks/exhaustive-deps
Để giải quyết, mình sử dụng useRef Hook như sau. Sửa file Products.js:
import React, { useEffect, useRef, useState } from "react";
import propTypes from "prop-types";
import { createUseStyles } from "react-jss";
import ProductItem from "../ProductItem/ProductItem"
import getProducts from "../../services/ProductService";
const useStyles = createUseStyles({
productsOuter: {
maxWidth: 425,
margin: 'auto'
},
productsWrap: {
marginTop: 10,
marginLeft: -5,
marginRight: -5
}
})
const Products = () => {
const classes = useStyles();
const [products, setProducts] = useState([])
const mounted = useRef(true);
useEffect(() => {
getProducts().then((result) => {
if (mounted.current) {
setProducts(result);
}
})
return () => {
mounted.current = false;
}
},[products])
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
Áp dụng tương tự cho App.js:
import React, { lazy, Suspense, useRef } from 'react';
import './App.css';
import { createUseStyles } from 'react-jss';
// import Header from './layout/Header/Header';
import Footer from './layout/Footer/Footer';
import UserContext from './context/UserContext';
import ProductPage from './layout/ProductPage/ProductPage';
import { useEffect, useReducer } from 'react';
import getUser from './services/UserService';
const Header = lazy(() => import('./layout/Header/Header'));
const useStyles = createUseStyles({
App: {
maxWidth: 425,
margin: 'auto'
}
});
const userReducer = (state, item) => {
const { id, name, wishlist } = item;
return { ...state, id, name, wishlist };
}
function App() {
const [user, setUser] = useReducer(userReducer, {
id: 0,
name: '',
wishlist: []
});
const [show, toggle] = useReducer(state => true, false);
let mounted = useRef(true);
useEffect(() => {
getUser().then((result) => {
if (mounted.current) {
setUser(result);
toggle()
}
})
return () => {
mounted.current = false;
}
}, [user])
const classes = useStyles()
return (
<UserContext.Provider value={user}>
<div className={classes.App}>
<Suspense fallback={<p>Loading</p>}>
<Header />
</Suspense>
<ProductPage />
<Footer />
</div>
</UserContext.Provider>
);
}
export default App;
Ngoài ra, hãy thận trọng về việc đặt biến trong hàm dọn dẹp khi sử dụng useEffect. Chức năng dọn dẹp sẽ luôn chạy trước khi hiệu ứng chạy lại. Điều đó có nghĩa là chức năng dọn dẹp () => mounted.current = false sẽ chạy mỗi thay đổi orders. Để tránh bất kỳ kết quả không mong muốn nào, hãy nhớ cập nhật mounted.current thành true khi bắt đầu hiệu ứng. Sau đó, bạn có thể chắc chắn rằng nó sẽ chỉ được đặt thành false khi component đã mounted.