Xử lý tải dữ liệu bất đồng bộ (Async Data Loading), lazyload trong React
- 16-10-2022
- Toanngo92
- 0 Comments
Trong JavaScript tiêu chuẩn, (asynchronous code) mã không đồng bộ cung cấp cho chúng ta khả năng chạy một số phần mã của bạn trong khi các phần khác vẫn đang chờ dữ liệu hoặc giải quyết. Điều này có nghĩa là các phần quan trọng trong ứng dụng sẽ không phải đợi các phần ít quan trọng hơn trước khi chúng hiển thị. Với mã không đồng bộ, chúng ta cũng có thể cập nhật ứng dụng của mình bằng cách yêu cầu và hiển thị thông tin mới, mang lại cho người dùng trải nghiệm mượt mà ngay cả khi các chức năng và yêu cầu dài đang xử lý ở chế độ nền.
Trong phát triển React , lập trình không đồng bộ đưa ra các vấn đề duy nhất. Ví dụ, khi bạn sử dụng các functional component React , các chức năng không đồng bộ có thể tạo ra các vòng lặp vô hạn. Khi một component được tải, nó có thể bắt đầu một chức năng không đồng bộ và khi chức năng không đồng bộ được giải quyết, nó có thể kích hoạt một re-render lại khiến component đó gọi lại chức năng không đồng bộ. Hướng dẫn này sẽ giải thích cách tránh điều này với Hook useEffect đã được nhắc tới trong bài trước,hook sẽ chỉ chạy các chức năng khi dữ liệu cụ thể thay đổi. Điều này sẽ cho phép bạn chạy mã không đồng bộ của mình một cách có chủ ý thay vì trên mỗi chu kỳ hiển thị.
Mã không đồng bộ không chỉ giới hạn ở các yêu cầu đối với dữ liệu mới. React có một hệ thống tích hợp cho các thành phần tải chậm hoặc chỉ tải chúng khi người dùng cần. Khi kết hợp với cấu hình webpack mặc định trong Create React App , bạn có thể chia nhỏ mã của mình, giảm một ứng dụng lớn thành các phần nhỏ hơn để có thể tải khi cần thiết. React có một thành phần đặc biệt được gọi là Suspense component sẽ hiển thị các trình giữ chỗ trong khi trình duyệt đang tải component mới của bạn. Trong các phiên bản React trong tương lai, bạn sẽ có thể sử dụng Suspense để tải dữ liệu trong các component lồng nhau mà không bị chặn hiển thị.
Mục lục
Khởi tạo dự án
npx create-react-app reactasyncdemo
cd reactasyncdemo
npm install react-jss
npm install react-proptypes
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
Khởi tạo dữ liệu mẫu
Tạo file data/products_data.js:
const data = [
{
"id": "0001",
"type": "Laptop",
"name": "Laptop HP Notebook 15 bs1xx",
"price": 8500000,
"thumbnail": "https://laptopcutot.com/wp-content/uploads/2022/08/auto-draft-1660891888837.jpg",
"source": "https://laptopcutot.com/product/laptop-hp-notebook-15-bs1xx-i5-8250u-8g-ssd-240g-vga-on-15-6hd/",
"specs": {
"cpu": "Intel Core i5 8250U",
"Ram": 8,
"storages": [
{
"storage": 240,
"storage_type": "ssd"
}
]
}
},
{
"id": "0002",
"type": "Laptop",
"name": "Laptop HP Probook 450G4",
"price": 8200000,
"thumbnail": "https://laptopcutot.com/wp-content/uploads/2022/08/auto-draft-1660882014527.jpg",
"source": "https://laptopcutot.com/product/laptop-hp-probook-450g4-i5-7200u-8g-ssd-240g-vga-on-15-6hd/",
"specs": {
"cpu": "Intel Core i5 7200U",
"Ram": 8,
"storages": [
{
"storage": 240,
"storage_type": "ssd"
}
]
},
"description": "The machine uses the 7th generation core i5 KabyLake to help deliver stronger and faster performance than many core i5 lines at the same price today. \nThe new 8 GB DDR4 RAM reduces battery power consumption by 20% and improves performance, upgradable to 16 GB."
},
{
"id": "0003",
"type": "Laptop",
"name": "Laptop Dell Latitude E7270",
"price": 7100000,
"thumbnail": "https://laptopcutot.com/wp-content/uploads/2022/08/auto-draft-1660891888837.jpg",
"source": "https://laptopcutot.com/product/laptop-cu-dell-latitude-e7270-core-i7-6600u-ram-4gb-ssd-128gb-vga-hd-man-125inch-fhd/",
"specs": {
"cpu": "Intel Core i5 8250U",
"Ram": 4,
"storages": [
{
"storage": 128,
"storage_type": "ssd"
}
]
},
"description": "Dell Latitude E7270 Core i7-6600U is equipped with Intel's 6th generation processor, giving the machine a stable processing power and effective power saving. With 4 GB DDR4 RAM memory for good processing performance as well as smooth multitasking applications. The Intel HD Graphics 520 graphics chipset allows to run graphics applications well. In addition, Dell also equips the machine with a 128GB or 256GB M.2 SATA SSD to help the machine operate smoother and boot faster."
},
{
"id": "0004",
"type": "Laptop",
"name": "Laptop HP 340s G7",
"price": 7500000,
"thumbnail": "https://laptopcutot.com/wp-content/uploads/2022/08/auto-draft-1660114914748.jpg",
"source": "https://laptopcutot.com/product/laptop-hp-340s-g7-i3-1005g1-4gb-ssd-512gb/",
"specs": {
"cpu": "Intel Core i3 1005G1",
"Ram": 4,
"storages": [
{
"storage": 500,
"storage_type": "ssd"
}
]
}
},
{
"id": "0005",
"type": "Laptop",
"name": "Laptop HP Elitebook 840 G3",
"price": 8200000,
"thumbnail": "https://laptopcutot.com/wp-content/uploads/2022/03/auto-draft-1648029011162.jpg",
"source": "https://laptopcutot.com/product/laptop-cu-hp-elitebook-840-g3-i7-6600u-ram8gb-ssd-256gb-intel-hd-graphic-520-mh-14-full-hd/",
"specs": {
"cpu": "Intel Core i7 6600U",
"Ram": 8,
"storages": [
{
"storage": 240,
"storage_type": "ssd"
}
]
},
"description": "This is already the 3rd version in the Elitebook 840 series and HP continues to keep this name and add the G3 symbol at the back. In general, the overall design of the Elitebook 840 G3 is not much different from the previous 2 generations when the machine still possesses an ultra-thin and light design when it is only 18.9 mm thick and weighs only 1.5 kg.\nThe back of the Elitebook 840 G3 is made of silver-white magnesium alloy with scratch resistance, so it looks quite luxurious and high-class. Besides, although it is quite thin and light, when held in the hand, the device feels sturdy and durable thanks to the US military standard MIL-STD 810G with very good impact resistance, and it is also resistant to shocks. Extreme temperature, high pressure, drop resistance, impact resistance and moisture resistance are much better than conventional laptops."
},
{
"id": "0006",
"type": "Laptop",
"name": "Laptop Dell Inspiron 5493",
"price": 11900000,
"thumbnail": "https://laptopcutot.com/wp-content/uploads/2021/11/5493-2048x2048.jpg",
"source": "https://laptopcutot.com/product/laptop-cu-dell-inspiron-5493-i3-1005g1-ram-4gb-ssd-128gb-vga-microsoft-basic-display-adapter-14fhd/",
"specs": {
"cpu": "Intel Core i3 1005G1",
"Ram": 4,
"storages": [
{
"storage": 128,
"storage_type": "ssd"
}
]
},
"description": "Machines sold are strictly guaranteed for 3 months.\nThe machine is pre-installed with Windows 10 Pro OS and necessary software.\nComes with AP2REP headset\nIncluded accessories: laptop bag and wired mouse"
}
];
export default data
Tạo file data/user_data.js:
const User = {
id: 1,
name: 'toanngo123',
wishlist: ['0001','0003','0005']
}
export default User
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;
Tải dữ liệu bất đồng bộ với useEffect
Ở ví dụ này, mình tiếp tục quay trở lại với trang sản phẩm và đặt hàng, sử dụng useEffect để mô phỏng việc kéo dữ liệu về component và hiển thị. Trong tình huống này, chúng ta sẽ mô phỏng tình huống dữ liệu user_data và product_data được kéo từ server về, có độ trễ nhất định. Tiến hành cấu trúc file như sau:
Tạo các service mô phỏng lấy dữ liệu từ backend
Tạo file services/UserService.js:
import User from "../data/user_data"
const getUser = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(User)
},1500)
})
}
export default getUser;
Tạo file services/ProductService.js
import products_data from '../data/products_data'
const getProducts = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(products_data)
},1500)
})
}
export default getProducts;
Sửa các file trong thư mục layout
Sửa file App.js:
import './App.css';
import { createUseStyles } from 'react-jss';
import Header from './layout/Header/Header';
import Footer from './layout/Footer/Footer';
import UserContext from './context/UserContext';
import ProductPage from './layout/ProductPage/ProductPage';
import { useEffect, useReducer } from 'react';
import getUser from './services/UserService';
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,{});
useEffect(() => {
getUser().then((result) => {
setUser(result);
console.log(user);
})
},[])
const classes = useStyles()
return (
<UserContext.Provider value={user}>
<div className={classes.App}>
<Header />
<ProductPage />
<Footer />
</div>
</UserContext.Provider>
);
}
export default App;
Giải thích: chúng ta có thể sử dụng useState ở đây, nhưng mình dùng reducer để đa dạng hóa cách giải quyết cho mọi người dễ hình dung. Sau khi component được render, sử dụng useState hook để lấy dữ liệu ra từ services và đưa vào các component.
Sửa lại 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
Giải thích: ký tự ‘?’ ở đây mình dùng để kiểm tra nhanh nếu như có user thì mình sẽ in thuộc tính name ra ứng dụng, tránh một số lỗi không mong muốn (hay xảy ra khi thuộc tính object là một mảng hơn) .
Tạo file layout/ProductPage/ProductPage.js nội dung không thay đổi như bài trước:
import React, { createContext, useReducer } from "react";
import { createUseStyles } from "react-jss";
import Cart from "../../components/Cart/Cart";
import CheckOut from "../../components/CheckOut/CheckOut";
import Products from "../../components/Products/Products";
import CartContext from "../../context/CartContext";
import CheckOutContext from "../../context/CheckOutcontext";
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 checkOutReducer = (state, item) => {
const { item_obj, type } = item;
switch (type) {
default:
case 'close':
return {...state,activeCheckOut:false}
break;
case 'active':
const {activeCheckOut, totalQuantity, totalPrice, productsCart} = item_obj;
return { ...state, activeCheckOut,activeCheckOut, totalQuantity, totalPrice, productsCart }
}
}
const ProductPage = () => {
const classes = useStyles();
const [productsCart, setCart] = useReducer(cartReducer, [])
const [totalCart, setTotalCart] = useReducer(totalCartReducer, { totalQuantity: 0, totalPrice: 0, productsCart: [] })
const [checkOut, setCheckOut] = useReducer(checkOutReducer, { activeCheckOut: false, totalQuantity: 0, totalPrice: 0, productsCart: [] })
return (
<ProductContext.Provider value={{ productsCart, setCart }}>
<CartContext.Provider value={{ totalCart, setTotalCart }}>
<CheckOutContext.Provider value={{ checkOut, setCheckOut }}>
<Cart />
{checkOut.activeCheckOut && <CheckOut />}
<Products />
</CheckOutContext.Provider>
</CartContext.Provider>
</ProductContext.Provider >
)
}
export default ProductPage;
Tạo các components
Tạo file components/Products/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([])
useEffect(() => {
getProducts().then((result) => {
setProducts(result);
})
})
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
Giải thích: tương tự, chúng ta thấy ở file này, mình sử dụng useEffect hook để lấy dữ liệu về khi component render thành công. Dữ liệu khi lấy về có độ trễ 1500ms, nghĩa là khi ứng dụng chạy, sau 1.5s thì dữ liệu ứng dụng mới được hiển thị.
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
Output trả ra sẽ tương tự như các bài trước, tuy nhiên chỉ có sự khác biệt chúng ta có thể nhận thấy là độ trễ của ứng dụng sẽ dựa theo thời gian xử lý Promise, giống với tình huống trong thực tế hơn.
Xử lý lỗi với các unmounted component
Với tình huống này, mình giải thích nhanh rằng trong bất kỳ component React nào đều có vòng đời mount -> unmount, trong khi ứng dụng chạy, nó sẽ re render lại component nhiều lần, để đảm bảo component sau khi unmount sẽ gọi lại Promise và gán giá trị chính xác, chúng ta xử lý như phía dưới.
Sửa file App.js:
import './App.css';
import { createUseStyles } from 'react-jss';
import Header from './layout/Header/Header';
import Footer from './layout/Footer/Footer';
import UserContext from './context/UserContext';
import ProductPage from './layout/ProductPage/ProductPage';
import { useEffect, useReducer } from 'react';
import getUser from './services/UserService';
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: []
});
useEffect(() => {
let mounted = true;
getUser().then((result) => {
if(mounted){
setUser(result);
}
})
return () => {
mounted = false;
}
},[user])
const classes = useStyles()
return (
<UserContext.Provider value={user}>
<div className={classes.App}>
<Header />
<ProductPage />
<Footer />
</div>
</UserContext.Provider>
);
}
export default App;
Lazyload component với Suspense và lazy
Trong bước này, chúng sẽ tách mã bằng React Suspense và lazy. Khi các ứng dụng phát triển, kích thước của bản dựng cuối cùng cũng tăng theo nó. Thay vì buộc người dùng tải xuống toàn bộ ứng dụng, chúng ta có thể chia mã thành các phần nhỏ hơn. React Suspense và lazy làm việc với webpack và các hệ thống xây dựng khác để chia mã của bạn thành các phần nhỏ hơn mà người dùng có thể tải theo yêu cầu. Trong tương lai, có thể sử dụng Suspenseđể tải nhiều loại dữ liệu, bao gồm cả các yêu cầu API.
Cho đến nay, bạn chỉ làm việc với việc tải dữ liệu không đồng bộ, nhưng bạn cũng có thể tải các thành phần không đồng bộ. Quá trình này, thường được gọi là tách mã , giúp giảm kích thước các gói mã của bạn để người dùng của bạn không phải tải xuống ứng dụng đầy đủ nếu họ chỉ đang sử dụng một phần của nó.
Hầu hết thời gian, chúng nhập mã tĩnh, nhưng bạn có thể nhập mã động bằng cách gọi import dưới dạng một hàm thay vì một câu lệnh. Mã sẽ mô phỏng như thế này:
import('my-library')
.then(library => library.action())
React cung cấp cho chúng ta một bộ công cụ bổ sung có tên lazy và Suspense. Để ví dụ, chúng ta tiến hành sửa mã nguồn như sau:
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);
useEffect(() => {
let mounted = true;
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ổng kết
Các chức năng không đồng bộ tạo ra các ứng dụng thân thiện với người dùng hiệu quả. Tuy nhiên, lợi thế của chúng đi kèm với một số chi phí nhỏ có thể trở thành lỗi trong chương trình của bạn. Giờ đây, bạn có các công cụ cho phép bạn chia các ứng dụng lớn thành các phần nhỏ hơn và tải dữ liệu không đồng bộ trong khi vẫn cung cấp cho người dùng một ứng dụng hiển thị. Bạn có thể sử dụng kiến thức để kết hợp các yêu cầu API và thao tác dữ liệu không đồng bộ vào các ứng dụng của mình, tạo ra trải nghiệm người dùng nhanh chóng và đáng tin cậy.