hocvietcode.com
  • Trang chủ
  • Học lập trình
    • Lập trình C/C++
    • Lập trình HTML
    • Lập trình Javascript
      • Javascript cơ bản
      • ReactJS framework
      • AngularJS framework
      • Typescript cơ bản
      • Angular
    • Lập trình Mobile
      • Lập Trình Dart Cơ Bản
        • Dart Flutter Framework
    • Cơ sở dữ liệu
      • MySQL – MariaDB
      • Micrsoft SQL Server
      • Extensible Markup Language (XML)
      • JSON
    • Lập trình PHP
      • Lập trình PHP cơ bản
      • Laravel Framework
    • Lập trình Java
      • Java Cơ bản
    • Cấu trúc dữ liệu và giải thuật
    • Lập Trình C# Cơ Bản
    • Machine Learning
  • WORDPRESS
    • WordPress cơ bản
    • WordPress nâng cao
    • Chia sẻ WordPress
  • Kiến thức hệ thống
    • Microsoft Azure
    • Docker
    • Linux
  • Chia sẻ IT
    • Tin học văn phòng
      • Microsoft Word
      • Microsoft Excel
    • Marketing
      • Google Adwords
      • Facebook Ads
      • Kiến thức khác
    • Chia sẻ phần mềm
    • Review công nghệ
    • Công cụ – tiện ích
      • Kiểm tra bàn phím online
      • Kiểm tra webcam online
Đăng nhập
  • Đăng nhập / Đăng ký

Please enter key search to display results.

Home
  • ReactJS
Gọi API trong React với useEffect Hook

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
    • Tạo các component layout Header, Footer
    • Tạo Ultils cho dự án
    • Tạo các file context
  • Fetch dữ liệu từ API
    • Tạo các service lấy dữ liệu từ backend json-server
    • Sửa các file trong thư mục layout
    • Tạo các components
  • Gửi dữ liệu tới API
  • Sử dụng useRef Hook trong React

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:

 [email protected] 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.

Bài tập

Đề bài: Xây dựng trang web bán hàng bằng ReactJS

Mô tả bài tập

Bạn sẽ xây dựng một trang web bán hàng với các chức năng cơ bản:

  1. Hiển thị danh sách sản phẩm được lấy từ tệp JSON thông qua useEffect.
  2. Quản lý giỏ hàng sử dụng useContext.
  3. Thanh toán và đặt hàng với form thông tin khách hàng.

Nội dung tệp JSON:

[
  {
    "id": 1,
    "name": "Laptop HP Notebook 15 bs1xx",
    "price": 8500000,
    "image": "link-to-image"
  },
  {
    "id": 2,
    "name": "Laptop HP Probook 450G4",
    "price": 8200000,
    "image": "link-to-image"
  },
  {
    "id": 3,
    "name": "Laptop Dell Latitude E7270",
    "price": 7100000,
    "image": "link-to-image"
  },
  {
    "id": 4,
    "name": "Laptop HP 340s G7",
    "price": 7500000,
    "image": "link-to-image"
  }
]

 

Yêu cầu chi tiết

1. Danh sách sản phẩm

  • Tạo component ProductList để hiển thị danh sách sản phẩm.
  • Sử dụng useEffect để kéo dữ liệu từ tệp products.json (nằm trong thư mục public).
  • Hiển thị mỗi sản phẩm với thông tin:
    • Hình ảnh
    • Tên sản phẩm
    • Giá
    • Nút "Add to Cart".
  • Khi nhấn "Add to Cart", sản phẩm được thêm vào giỏ hàng (sử dụng useContext).

2. Giỏ hàng

  • Tạo component Cart để hiển thị các sản phẩm đã thêm vào giỏ hàng.
  • Sử dụng useContext để quản lý trạng thái giỏ hàng.
  • Hiển thị:
    • Tên sản phẩm
    • Giá
    • Số lượng
    • Nút "Remove" để xóa sản phẩm khỏi giỏ hàng.
  • Hiển thị tổng số lượng sản phẩm và tổng giá trị đơn hàng.

3. Thanh toán

  • Tạo component CheckoutForm với các trường nhập liệu:
    • Tên khách hàng
    • Số điện thoại
    • Phương thức thanh toán (Cash on Delivery, Online Payment)
  • Khi nhấn nút "Submit", hiển thị thông báo "Order successfully!" và reset giỏ hàng.

 

Bài viết liên quan:

useMemo trong ReactJS
Định tuyến trong ứng dụng React với React Router
Giới thiệu Redux và quản lý state với Redux trong React
Xử lý tải dữ liệu bất đồng bộ (Async Data Loading), lazyload trong React
Xử lý Form (biểu mẫu) trong React
Xử lý các sự kiện trong DOM và Window bằng React
Debug (gỡ lỗi) React component thông qua React Developer Tools
khái niệm React Context và chia sẻ state qua các component với React Context
Cách quản lý state(trạng thái) bằng Hooks trên React Component
React strict mode (chế độ phát triển) trong React
Quản lý state (trạng thái) trong React Class Component
Hướng dẫn định kiểu (style) cho React Component

THÊM BÌNH LUẬN Cancel reply

Dịch vụ thiết kế Wesbite

NỘI DUNG MỚI CẬP NHẬT

2. PHÂN TÍCH VÀ ĐẶC TẢ HỆ THỐNG

1. TỔNG QUAN KIẾN THỨC THỰC HÀNH TRIỂN KHAI DỰ ÁN CÔNG NGHỆ THÔNG TIN

Hướng dẫn tự cài đặt n8n comunity trên CyberPanel, trỏ tên miền

Mẫu prompt tạo mô tả chi tiết bối cảnh

Một số cải tiến trong ASP.NET Core, Razor Page, Model Binding, Gabbage collection

Giới thiệu

hocvietcode.com là website chia sẻ và cập nhật tin tức công nghệ, chia sẻ kiến thức, kỹ năng. Chúng tôi rất cảm ơn và mong muốn nhận được nhiều phản hồi để có thể phục vụ quý bạn đọc tốt hơn !

Liên hệ quảng cáo: [email protected]

Kết nối với HỌC VIẾT CODE

© hocvietcode.com - Tech888 Co .Ltd since 2019

Đăng nhập

Trở thành một phần của cộng đồng của chúng tôi!
Registration complete. Please check your email.
Đăng nhập bằng google
Đăng kýBạn quên mật khẩu?

Create an account

Welcome! Register for an account
The user name or email address is not correct.
Registration confirmation will be emailed to you.
Log in Lost your password?

Reset password

Recover your password
Password reset email has been sent.
The email could not be sent. Possible reason: your host may have disabled the mail function.
A password will be e-mailed to you.
Log in Register
×