ReactJS: Cách quản lý state bằng Hook trên các component React

Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực

Giới thiệu

Trong phát triển React, việc theo dõi dữ liệu ứng dụng của bạn thay đổi như thế nào theo thời gian được gọi là quản lý trạng thái. Bằng cách quản lý trạng thái ứng dụng của mình, bạn sẽ có thể tạo các ứng dụng động phản hồi thông tin đưa vào của người dùng. Có nhiều phương pháp quản lý trạng thái trong React, bao gồm quản lý trạng thái trên component dạng class và các thư viện của bên thứ ba như Redux. Trong bài hướng dẫn này, bạn sẽ quản lý trạng thái trên các component dạng function bằng phương pháp được khuyến khích bởi tài liệu React chính thức: Hook.

Hook là một tập hợp rộng rãi các công cụ chạy các chức năng tùy chỉnh khi prop của một component thay đổi. Vì phương pháp quản lý trạng thái này không yêu cầu bạn sử dụng các class, nên các nhà phát triển có thể sử dụng Hook để viết mã ngắn hơn, dễ đọc hơn, dễ chia sẻ và duy trì. Một trong những điểm khác biệt chính giữa Hook và quản lý state dựa trên class là không có đối tượng duy nhất nào nắm giữ tất cả state. Thay vào đó, bạn có thể chia nhỏ state thành nhiều phần mà bạn có thể cập nhật độc lập.

Trong suốt hướng dẫn này, bạn sẽ học cách thiết lập state bằng cách sử dụng các Hook useState và useReducer. Hook useState là có giá trị khi thiết lập một giá trị mà không cần tham chiếu hiện trạng; còn Hook useReducer rất hữu ích khi bạn cần phải tham khảo một giá trị trước đó hoặc khi bạn có những hành động khác nhau đòi hỏi thao tác dữ liệu phức tạp. Để khám phá các cách thiết lập state khác nhau này, bạn sẽ tạo component trang sản phẩm với giỏ hàng mà bạn sẽ cập nhật bằng cách thêm các giao dịch mua từ danh sách các tùy chọn. Đến cuối bài hướng dẫn này, bạn sẽ có state quản lý thoải mái trong một component dạng function sử dụng Hook, và bạn sẽ có một nền tảng cho Hook tiên tiến hơn như useEffectuseMemo, và useContext.

Bước 1 - Tạo một dự án trống

Trong bước này, bạn sẽ tạo một dự án mới bằng Create React App. Sau đó, bạn sẽ xóa dự án mẫu và các tệp liên quan được cài đặt khi bạn khởi động dự án. Cuối cùng, bạn sẽ tạo một cấu trúc tệp đơn giản để tổ chức các component của mình. Điều này sẽ cung cấp cho bạn một cơ sở vững chắc để xây dựng ứng dụng mẫu của hướng dẫn này để tạo style trong bước tiếp theo.

Để bắt đầu, hãy thực hiện một dự án mới. Bạn mở terminal và chạy tập lệnh sau để cài đặt một dự án mới bằng cách sử dụng create-react-app:

npx create-react-app state-function-tutorial

Sau khi dự án kết thúc, hãy thay đổi vào thư mục:

cd state-function-tutorial

Khởi động dự án với lệnh:

npm start
Bạn sẽ nhận được một máy chủ cục bộ đang chạy. Nếu dự án không mở trong cửa sổ trình duyệt, bạn có thể mở nó bằng http://localhost:3000/. Nếu bạn đang chạy điều này từ một máy chủ từ xa, địa chỉ sẽ là .http://your_domain:3000

Trình duyệt của bạn sẽ tải với một ứng dụng React đơn giản được bao gồm như một phần của Create React App:

Dự án mẫu React

Bạn sẽ xây dựng một tập hợp các component tùy chỉnh hoàn toàn mới, vì vậy bạn sẽ cần bắt đầu bằng cách xóa một số mã soạn sẵn để bạn có thể có một dự án trống.

Để bắt đầu, hãy mở component src/App.js. Đây là component gốc được đưa vào trang. Tất cả các component sẽ bắt đầu từ đây. Bạn sửa lại file để trong đó chỉ còn chứa như sau:

import './App.css';

function App() {
  return <></>;
}

export default App;

Mở một terminal khác và thực hiện thao tác xóa file logo.svg:

rm src/logo.svg

Tạo thư mục components:

mkdir src/components

Tạo thư mục App:

mkdir src/components/App

Di chuyển các file App.* vào thư mục App:

mv src/App.* src/components/App

Mở file index.js và chỉnh sửa:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App/App';
import * as serviceWorker from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Lưu file lại và quay lại trang web ta được một trang web trống.

màn hình trống trong chrome

Bây giờ bạn đã hoàn thành dự án Tạo ứng dụng React mẫu, hãy tạo một cấu trúc tệp đơn giản. Điều này sẽ giúp bạn giữ cho các component của bạn cô lập và độc lập.

Tạo một thư mục được gọi components trong thư mục src, components sẽ là nơi chứa tất cả các component tùy chỉnh của bạn. 

Bước 2 - Thiết lập trạng thái ban đầu trong một component

Trong bước này, bạn sẽ đặt trạng thái ban đầu trên một component bằng cách gán trạng thái ban đầu cho một biến tùy chỉnh bằng cách sử dụng Hook useState. Để khám phá Hook, bạn sẽ tạo một trang sản phẩm có giỏ hàng, sau đó hiển thị các giá trị ban đầu dựa trên trạng thái. Đến cuối bước này, bạn sẽ biết các cách khác nhau để giữ một giá trị trạng thái bằng cách sử dụng Hook và khi nào thì sử dụng trạng thái thay vì một giá trị prop hoặc một giá trị tĩnh.

Trước tiên bạn hãy bắt đầu bằng cách tạo một thư mục cho một component có tên Product:

mkdir src/components/Product

Bạn tạo file Product.js rồi mở ra. Bắt đầu bằng cách tạo một component không có state. Component sẽ có hai phần: Giỏ hàng, có số lượng mặt hàng và tổng giá, và sản phẩm, có nút thêm bớt một mặt hàng. Hiện tại, các nút sẽ không có hành động nào.

Thêm mã sau vào Product.js:

import './Product.css';

export default function Product() {
  return(
    <div className="wrapper">
      <div>
        Shopping Cart: 0 total items.
      </div>
      <div>Total: 0</div>

      <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
      <button>Add</button> <button>Remove</button>
    </div>
  )
}

Như vậy ở đây bạn đã đưa vào một số phần tử div có tên class dạng JSX để bạn có thể thêm một số style cơ bản.

Lưu file lại.

Tiếp theo bạn tạo file Product.css và đưa vào đoạn code sau:

.product span {
    font-size: 100px;
}

.wrapper {
    padding: 20px;
    font-size: 20px;
}

.wrapper button {
    font-size: 20px;
    background: none;
}

Ở đây ta tạo một số style đơn giản để tăng font-size cho phần văn bản và biểu tượng cảm xúc. Biểu tượng cảm xúc sẽ cần kích thước phông chữ lớn hơn nhiều so với văn bản, vì nó hoạt động như hình ảnh sản phẩm trong ví dụ này. Ngoài ra, bạn đang xóa nền gradient mặc định trên các nút bằng cách đặt background thành none.

Lưu file lại.

Bây giờ, kết xuất component Product trong component App để bạn có thể xem kết quả trong trình duyệt. Mở App.js ra và update code như sau:

import Product from '../Product/Product';

function App() {
  return <Product />
}

export default App;

Lưu file lại và quay lại trang web bạn refresh và bạn sẽ thấy sự xuất hiện của component Product.

Trang sản phẩm

Bây giờ bạn đã có thêm một component mới và đang hoạt động, bạn có thể thay thế dữ liệu được mã hóa cứng bằng các giá trị động.

React xuất một số Hook mà bạn có thể import trực tiếp từ gói React. Theo quy ước, Hook bắt đầu với từ use, chẳng hạn như useStateuseContext, và useReducer. Hầu hết các thư viện của bên thứ ba đều tuân theo cùng một quy ước. Ví dụ, Redux có các Hook là useSelector và useStore.

Hook là các hàm cho phép bạn chạy các hành động như một phần của vòng đời React. Hook được kích hoạt bởi các hành động khác hoặc bởi những thay đổi trong prop của một component và được sử dụng để tạo dữ liệu hoặc để kích hoạt các thay đổi tiếp theo. Ví dụ như Hook useState tạo ra một phần dữ liệu state cùng với một hàm để thay đổi phần dữ liệu đó và kích hoạt kết xuất lại. Nó sẽ tạo một đoạn mã động và nối vào vòng đời bằng cách kích hoạt kết xuất khi dữ liệu thay đổi. Trong thực tế, điều đó có nghĩa là bạn có thể lưu trữ các phần dữ liệu động trong các biến bằng cách sử dụng useStateHook.

Ví dụ: trong component Product, bạn có hai phần dữ liệu sẽ thay đổi dựa trên hành động của người dùng: giỏ hàng và tổng tiền. Mỗi trong số này có thể được lưu trữ ở state bằng cách sử dụng Hook ở trên.

Để thử điều này, hãy mở file Product.js ra, sau đó import useState Hook từ React bằng cách thêm mã được đánh dấu như sau:

import { useState } from 'react';
import './Product.css';

export default function Product() {
  return(
    <div className="wrapper">
      <div>
        Shopping Cart: 0 total items.
      </div>
      <div>Total: 0</div>

      <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
      <button>Add</button> <button>Remove</button>
    </div>
  )
}

useState là một hàm nhận trạng thái ban đầu làm đối số và trả về một mảng có hai mục. Mục đầu tiên là một biến chứa trạng thái mà bạn sẽ thường sử dụng trong JSX của mình. Mục thứ hai trong mảng là một hàm sẽ cập nhật trạng thái. Vì React trả về dữ liệu dưới dạng một mảng, bạn có thể sử dụng hàm hủy để gán giá trị cho bất kỳ tên biến nào bạn muốn. Điều đó có nghĩa là bạn có thể gọi useState nhiều lần và không bao giờ phải lo lắng về xung đột tên, vì bạn có thể gán mọi trạng thái và hàm cập nhật cho một biến được đặt tên rõ ràng.

Tạo Hook đầu tiên của bạn bằng cách gọi Hook useState với một mảng trống. Thêm vào mã được đánh dấu sau:

import { useState } from 'react';
import './Product.css';

export default function Product() {
  const [cart, setCart] = useState([]);
  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: 0</div>

      <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
      <button>Add</button> <button>Remove</button>
    </div>
  )
}

Trong đoạn code trên, bạn đã gán giá trị đầu tiên đó là state cho một biến được gọi là cartcart sẽ là một mảng chứa các sản phẩm trong giỏ hàng. Bằng cách truyền một mảng trống làm đối số của useState, bạn đặt state trống ban đầu làm giá trị đầu tiên của cart.

Ngoài biến cart, bạn đã gán hàm cập nhật cho cho biến được gọi là setCart. Tại thời điểm này, bạn không sử dụng hàm setCart và bạn có thể thấy cảnh báo về việc có một biến không được sử dụng ở console. Trong bước tiếp theo, bạn sẽ sử dụng setCart để cập nhật trạng thái cart.

Lưu file lại và quay lại trang web bạn sẽ vẫn thấy nó không thay đổi

Một điểm khác biệt quan trọng giữa Hook và quản lý state dựa trên class là, trong quản lý state dựa trên class, có một đối tượng state duy nhất. Với Hook, các đối tượng trạng thái hoàn toàn độc lập với nhau, vì vậy bạn có thể có bao nhiêu đối tượng state tùy thích. Điều đó có nghĩa là nếu bạn muốn một phần dữ liệu state mới, tất cả những gì bạn cần làm là gọi useState với một giá trị mặc định mới và gán kết quả cho các biến mới.

Bên trong Product.js, hãy thử điều này bằng cách tạo một phần state mới để giữ total. Đặt giá trị mặc định thành 0 và gán giá trị và hàm cho total và setTotal:

import { useState } from 'react';
import './Product.css';

export default function Product() {
  const [cart, setCart] = useState([]);
  const [total, setTotal] = useState(0);
  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {total}</div>

      <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
      <button>Add</button> <button>Remove</button>
    </div>
  )
}

Bây giờ bạn đã có thêm một dữ liệu state, bạn có thể chuẩn hóa dữ liệu được hiển thị để tạo ra trải nghiệm dễ đoán hơn. Ví dụ: vì tổng trong ví dụ này là giá nên nó sẽ luôn có hai chữ số thập phân. Bạn có thể sử dụng phương thức toLocaleString để chuyển đổi total từ một số thành một chuỗi có hai chữ số thập phân. Nó cũng sẽ chuyển đổi số thành chuỗi theo quy ước số phù hợp với ngôn ngữ của trình duyệt. Bạn sẽ đặt các tùy chọn minimumFractionDigits và maximumFractionDigits để cung cấp một số vị trí thập phân nhất quán.

Tạo một hàm được gọi là getTotal. Hàm này sẽ sử dụng biến trong phạm vi total và trả về một chuỗi được bản địa hóa mà bạn sẽ sử dụng để hiển thị tổng. Sử dụng hàm undefined đối số đầu tiên toLocaleString để sử dụng ngôn ngữ của hệ thống thay vì chỉ định ngôn ngữ:

import { useState } from 'react';
import './Product.css';

const currencyOptions = {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
}

export default function Product() {
  const [cart, setCart] = useState([]);
  const [total, setTotal] = useState(0);

  function getTotal() {
    return total.toLocaleString(undefined, currencyOptions)
  }

  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {getTotal()}</div>

      <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
      <button>Add</button> <button>Remove</button>
    </div>
  )
}

Bây giờ bạn đã thêm một số xử lý chuỗi vào tổng số được hiển thị. Mặc dù getTotal là một hàm riêng biệt, nhưng nó lại có thể chia sẻ dữ liệu, có nghĩa là nó có thể tham chiếu đến các biến của component.

Lưu file lại và quay lại trang web ta được kết quả dạng như sau:

Giá được chuyển đổi thành số thập phân

Tuy nhiên hiện tại, getTotal chỉ có thể hoạt động trong đoạn mã này. Trong trường hợp này, bạn có thể chuyển đổi nó thành một hàm thuần túy, cung cấp các đầu ra giống nhau khi được cung cấp các đầu vào giống nhau và không dựa vào môi trường cụ thể để hoạt động. Bằng cách chuyển đổi hàm thành một hàm thuần túy, bạn làm cho nó có thể tái sử dụng nhiều hơn. Ví dụ: bạn có thể đưa nó vào một file riêng biệt và sử dụng nó trong nhiều component.

Cập nhật getTotal để lấy total làm đối số. Sau đó ta đưa hàm ra bên ngoài component:

import { useState } from 'react';
import './Product.css';

const currencyOptions = {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
}

function getTotal(total) {
  return total.toLocaleString(undefined, currencyOptions)
}

export default function Product() {
  const [cart, setCart] = useState([]);
  const [total, setTotal] = useState(0);


  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {getTotal(total)}</div><^>

      <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
      <button>Add</button> <button>Remove</button>
    </div>
  )
}

Lưu file lại và quay lại trang web bạn sẽ vẫn thấy trang web không đổi.

Các thành phần chức năng như thế này giúp di chuyển các chức năng xung quanh dễ dàng hơn. Miễn là không có xung đột phạm vi, bạn có thể di chuyển các hàm chuyển đổi này đến bất cứ đâu bạn muốn.

Trong bước này, bạn đặt giá trị mặc định cho một phần dữ liệu trạng thái bằng cách sử dụng useState. Sau đó, bạn đã lưu dữ liệu trạng thái và một hàm để cập nhật trạng thái cho các biến bằng cách sử dụng cấu trúc mảng. Trong bước tiếp theo, bạn sẽ sử dụng chức năng cập nhật để thay đổi giá trị trạng thái nhằm hiển thị lại trang với thông tin cập nhật.

Bước 3 - Thiết lập state bằng useState

Trong bước này, bạn sẽ cập nhật trang sản phẩm của mình bằng cách đặt state mới với giá trị tĩnh. Bạn đã tạo hàm để cập nhật một phần state, vì vậy bây giờ bạn sẽ tạo một sự kiện để cập nhật cả hai biến state với các giá trị được định nghĩa trước. Đến cuối bước này, bạn sẽ có một trang với state mà người dùng có thể cập nhật chỉ bằng một cú nhấp chuột.

Không giống như các component dạng class, bạn không thể cập nhật một số phần state bằng một lệnh gọi hàm duy nhất. Thay vào đó, bạn phải gọi từng hàm riêng lẻ.

Tạo một hàm để thêm một mặt hàng vào giỏ hàng và cập nhật tổng tiền với giá của mặt hàng đó, sau đó thêm hàm đó vào nút Add:

import { useState } from 'react';

...

export default function Product() {
  const [cart, setCart] = useState([]);
  const [total, setTotal] = useState(0);

  function add() {
    setCart(['ice cream']);
    setTotal(5);
  }

  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {getTotal(total)}</div>

      <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
      <button onClick={add}>Add</button><^>
      <button>Remove</button>
    </div>
  )
}

Trong đoạn mã trên, trong hàm add() bạn đã gọi setCart và truyền đối số là một mảng có chứa phần tử "kem" và gọi setTotal truyền đi 5. Sau đó, bạn đã gọi hàm add() từ thuộc tính sự kiện onClick của nút nút Add.

Chú ý rằng hàm phải có cùng phạm vi với các hàm để thiết lập trạng thái, vì vậy nó phải được định nghĩa bên trong component function.

Lưu file lại và quay lại trang web bạn sẽ thấy kết quả dạng như sau:

Nhấp vào nút và xem trạng thái được cập nhật

Vì bạn không tham chiếu đến ngữ cảnh this, bạn có thể sử dụng hàm mũi tên hoặc khai báo hàm. Cả hai đều hoạt động tốt như nhau và mỗi nhà phát triển hoặc nhóm có thể quyết định sử dụng cách thức nào. Bạn thậm chí có thể bỏ qua việc định nghĩa một hàm phụ và truyền trực tiếp hàm vào thuộc tính onClick.

Để thử điều này, hãy tạo một hàm để loại bỏ các giá trị bằng cách đặt giỏ hàng thành một đối tượng trống và tổng tiền thành 0. Tạo hàm trong prop onClick của nút Delete:

import { useState } from 'react';
...
export default function Product() {
  const [cart, setCart] = useState([]);
  const [total, setTotal] = useState(0);

  function add() {
    setCart(['ice cream']);
    setTotal(5);
  }

  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {getTotal(total)}</div>

      <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
      <button onClick={add}>Add</button>
      <button
        onClick={() => {
          setCart([]);
          setTotal(0);
        }}
      >
        Remove
      </button>
    </div>
  )
}

Lưu file lại và quay lại trang web bạn sẽ thấy trang web hoạt động giống như sau:

Thêm và Xóa

Cả hai chiến lược để gán hàm đều hoạt động, nhưng có một số tác động hiệu suất nhỏ đối với việc tạo một hàm mũi tên trực tiếp trong một prop. Trong mỗi lần kết xuất lại, React sẽ tạo một hàm mới, hàm này sẽ kích hoạt một thay đổi prop và khiến component hiển thị lại. Khi bạn định nghĩa một hàm bên ngoài một prop, bạn có thể tận dụng một Hook khác được gọi là useCallback. Điều này sẽ ghi nhớ hàm, có nghĩa là nó sẽ chỉ tạo một hàm mới nếu các giá trị nhất định thay đổi. Nếu không có gì thay đổi, chương trình sẽ sử dụng bộ nhớ đệm của hàm thay vì tính toán lại. Một số component có thể không cần mức tối ưu hóa đó, nhưng theo quy luật, một component có khả năng nằm trong cây càng cao thì nhu cầu ghi nhớ càng lớn.

Trong bước này, bạn đã cập nhật dữ liệu state với các hàm được tạo bởi useStateHook. Bạn đã tạo các hàm gộp để gọi cả hai hàm nhằm cập nhật state của một số phần dữ liệu cùng một lúc. Nhưng các hàm này bị hạn chế vì chúng thêm các giá trị tĩnh, được xác định trước thay vì sử dụng state trước đó để tạo state mới. Trong bước tiếp theo, bạn sẽ cập nhật state bằng cách sử dụng state hiện tại với cả Hook useState và Hook mới được gọi là useReducer.

Bước 4 - Thiết lập state bằng state hiện tại

Trong bước trước, bạn đã cập nhật state với một giá trị tĩnh. State trước đó không quan trọng - bạn luôn truyền cùng một giá trị. Nhưng một trang sản phẩm điển hình sẽ có nhiều mặt hàng mà bạn có thể thêm vào giỏ hàng và bạn sẽ muốn cập nhật giỏ hàng trong khi vẫn giữ các mặt hàng trước đó.

Trong bước này, bạn sẽ cập nhật state bằng state hiện tại. Bạn sẽ mở rộng trang sản phẩm của mình để bao gồm một số sản phẩm và bạn sẽ tạo các hàm cập nhật giỏ hàng và tổng tiền dựa trên các giá trị hiện tại. Để cập nhật các giá trị, bạn sẽ sử dụng cả Hook useState và một Hook mới được gọi là useReducer.

Vì React có thể tối ưu hóa mã bằng cách gọi các hành động không đồng bộ, bạn sẽ muốn đảm bảo rằng hàm của mình có quyền truy cập vào state cập nhật nhất. Cách cơ bản nhất để giải quyết vấn đề này là truyền một hàm cho hàm thiết lập state thay vì một giá trị. Nói cách khác, thay vì gọi setState(5), bạn sẽ gọi setState(previous => previous +5).

Để bắt đầu triển khai điều này, hãy thêm một số sản phẩm khác vào trang sản phẩm bằng cách tạo một mảng đối tượng products, sau đó xóa các trình xử lý sự kiện khỏi các nút Add và Remove để nhường chỗ cho việc tái cấu trúc:

import { useState } from 'react';
import './Product.css';

...

const products = [
  {
    emoji: '🍦',
    name: 'ice cream',
    price: 5
  },
  {
    emoji: '🍩',
    name: 'donuts',
    price: 2.5,
  },
  {
    emoji: '🍉',
    name: 'watermelon',
    price: 4
  }
];

export default function Product() {
  const [cart, setCart] = useState([]);
  const [total, setTotal] = useState(0);

  function add() {
    setCart(['ice cream']);
    setTotal(5);
  }

  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {getTotal(total)}</div>
        <div>
        {products.map(product => (
          <div key={product.name}>
            <div className="product">
              <span role="img" aria-label={product.name}>{product.emoji}</span>
            </div>
            <button>Add</button>
            <button>Remove</button>
          </div>
        ))}
      </div>
    </div>
  )
}

Bây giờ bạn có một số JSX sử dụng phương thức .map để lặp qua mảng và hiển thị các sản phẩm.

Lưu file lại và quay lại trang web bạn sẽ thấy kết quả giống như thế này:

Hiện tại, các nút không có hành động nào. Vì bạn chỉ muốn thêm sản phẩm cụ thể khi nhấp chuột, bạn sẽ cần truyền sản phẩm làm đối số cho hàm add. Trong hàm add, thay vì truyền trực tiếp sản phẩm mới đến các hàm setCart và setTotal, bạn sẽ truyền một hàm ẩn danh có state hiện tại và trả về giá trị cập nhật mới:

import { useState } from 'react';
import './Product.css';
...
export default function Product() {
  const [cart, setCart] = useState([]);
  const [total, setTotal] = useState(0);

  function add(product) {
    setCart(current => [...current, product.name]);
    setTotal(current => current + product.price);
  }

  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {getTotal(total)}</div>

      <div>
        {products.map(product => (
          <div key={product.name}>
            <div className="product">
              <span role="img" aria-label={product.name}>{product.emoji}</span>
            </div>
            <button onClick={() => add(product)}>Add</button>
            <button>Remove</button>
          </div>
        ))}
      </div>
    </div>
  )
}

Hàm ẩn danh sử dụng state gần đây nhất — hoặc cart hoặc total— làm đối số mà bạn có thể sử dụng để tạo giá trị mới. Tuy nhiên, hãy cẩn thận, không để state đột biến trực tiếp. Thay vào đó, khi thêm giá trị mới vào giỏ hàng, bạn có thể thêm sản phẩm mới vào state bằng cách rải (toán tử spread) giá trị hiện tại và thêm giá trị mới vào cuối.

Lưu file lại và quay lại trang web bạn sẽ thấy kết quả giống như sau:

Thêm sản phẩm

Có một Hook khác được gọi là useReducer được thiết kế đặc biệt để cập nhật state dựa trên state hiện tại, theo cách tương tự như phương thức mảng .reduce. Hook useReducer cũng tương tự như useState, nhưng khi bạn khởi tạo Hook, bạn truyền trong một hàm Hook sẽ chạy khi bạn thay đổi state cùng với các dữ liệu ban đầu. Hàm — được gọi là reducer—có hai đối số: trạng thái và một đối số khác. Đối số khác là những gì bạn sẽ cung cấp khi bạn gọi hàm cập nhật.

Cấu trúc lại state giỏ hàng để sử dụng Hook useReducer. Tạo một hàm gọi là cartReducer chứa hai tham số là state và product. Thay thế useState bằng useReducer, sau đó truyền hàm cartReducer làm đối số đầu tiên và một mảng trống làm đối số thứ hai, sẽ là dữ liệu ban đầu:

import { useReducer, useState } from 'react';

...

function cartReducer(state, product) {
  return [...state, product]
}

export default function Product() {
  const [cart, setCart] = useReducer(cartReducer, []);
  const [total, setTotal] = useState(0);

  function add(product) {
    setCart(product.name);
    setTotal(current => current + product.price);
  }

  return(
...
  )
}

Bây giờ khi bạn gọi setCart, hãy nhập tên sản phẩm thay vì một hàm. Khi bạn gọi setCart, bạn sẽ gọi hàm reducer và sản phẩm sẽ là đối số thứ hai. Bạn có thể thực hiện một thay đổi tương tự với state total.

Tạo một hàm được gọi là totalReducer có state hiện tại và cộng thêm số tiền mới. Sau đó, thay thế useState bằng useReducer và truyền giá trị mới setCart vhay vì một hàm:

import { useReducer } from 'react';

...

function totalReducer(state, price) {
  return state + price;
}

export default function Product() {
  const [cart, setCart] = useReducer(cartReducer, []);
  const [total, setTotal] = useReducer(totalReducer, 0);

  function add(product) {
    setCart(product.name);
    setTotal(product.price);
  }

  return(
    ...
  )
}

Ở đây vì bạn không còn sử dụng Hook useState nữa, bạn đã xóa nó khỏi import.

Lưu file lại và quay lại trang web bạn sẽ thấy trang web vẫn làm việc bình thường.

Bây giờ là lúc ta sẽ thêm hàm remove. Nhưng điều này dẫn đến một vấn đề: Các hàm reducer có thể xử lý việc thêm các sản phẩm và cập nhật tổng tiền, nhưng không rõ nó sẽ có thể xử lý việc xóa các sản phẩm khỏi state như thế nào. Một mẫu phổ biến trong các hàm reducer là truyền một đối tượng làm đối số thứ hai chứa tên của hành động và dữ liệu cho hành động. Sau đó bên trong hàm reducer bạn có thể cập nhật tổng tiền dựa trên hành động. Trong trường hợp này, bạn sẽ thêm các mặt hàng vào giỏ hàng với hành động add và xóa chúng với hành động remove.

Bắt đầu với totalReducer. Cập nhật hàm để nhận một action làm đối số thứ hai, sau đó thêm một điều kiện để cập nhật state dựa trên action.type:

import { useReducer } from 'react';
import './Product.css';

...

function totalReducer(state, action) {
  if(action.type === 'add') {
    return state + action.price;
  }
  return state - action.price
}

export default function Product() {
  const [cart, setCart] = useReducer(cartReducer, []);
  const [total, setTotal] = useReducer(totalReducer, 0);

  function add(product) {
    const { name, price } = product;
    setCart(name);
    setTotal({ price, type: 'add' });
  }

  return(
    ...
  )
}

Trong đoạn code trên, action là một đối tượng có hai thuộc tính: type và price, trong đó type có thể là một trong hai add hoặc remove, và price là một số. Nếu type là  add thì nó sẽ làm tăng tổng tiền, còn nếu là remove thì nó làm giảm tổng tiền. Sau khi cập nhật totalReducer, bạn gọi setTotal và truyền đi type là add và price.

Tiếp theo đây, bạn sẽ cập nhật cartReducer. Điều này phức tạp hơn một chút: Bạn có thể sử dụng điều kiện if-else, nhưng thông thường hơn thì ta sẽ sử dụng một điều kiện switch-case. Câu lệnh switch đặc biệt hữu ích nếu bạn có một reducer có thể xử lý nhiều hành động khác nhau vì nó làm cho những hành động đó dễ đọc hơn trong mã của bạn.

Như với totalReducer, bạn sẽ truyền một đối tượng với các thuộc tính type và name làm đối số thứ hai. Nếu action là remove thì ta cập nhật state bằng cách tách bỏ đi sản phẩm.

Sau khi cập nhật cartReducer, hãy tạo một hàm remove và cho nó gọi setCart và setTotal với các đối tượng chứa type: 'remove' và price hoặc là name. Sau đó, sử dụng câu lệnh switch để cập nhật dữ liệu dựa trên loại hành động:

import { useReducer } from 'react';
import './Product.css';

...

function cartReducer(state, action) {
  switch(action.type) {
    case 'add':
      return [...state, action.name];
    case 'remove':
      const update = [...state];
      update.splice(update.indexOf(action.name), 1);
      return update;
    default:
      return state;
  }
}

function totalReducer(state, action) {
  if(action.type === 'add') {
    return state + action.price;
  }
  return state - action.price
}

export default function Product() {
  const [cart, setCart] = useReducer(cartReducer, []);
  const [total, setTotal] = useReducer(totalReducer, 0);

  function add(product) {
    const { name, price } = product;
    setCart({ name, type: 'add' });
    setTotal({ price, type: 'add' });
  }

  function remove(product) {
    const { name, price } = product;
    setCart({ name, type: 'remove' });
    setTotal({ price, type: 'remove' });
  }

  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {getTotal(total)}</div>

      <div>
        {products.map(product => (
          <div key={product.name}>
            <div className="product">
              <span role="img" aria-label={product.name}>{product.emoji}</span>
            </div>
            <button onClick={() => add(product)}>Add</button>
            <button onClick={() => remove(product)}>Remove</button>
          </div>
        ))}
      </div>
    </div>
  )
}

Khi bạn làm việc trên mã của mình, hãy cẩn thận không trực tiếp thay đổi trạng thái trong các hàm reducer. Thay vào đó, hãy tạo một bản sao trước khi loại bỏ đối tượng. Cũng lưu ý rằng cách tốt nhất là thêm một action default vào câu lệnh switch để giải quyết các trường hợp không lường trước được. Trong trường hợp này, chỉ cần trả lại đối tượng. Các tùy chọn khác cho default là ném (throw) lỗi hoặc quay trở lại một hành động như add hoặc remove.

Sau khi thực hiện các thay đổi, hãy file lại sau đó quay lại trang web và refresh bạn sẽ có thể thực hiện các hành động dạng như sau:

Loại bỏ các mục

Vẫn còn một lỗi nhỏ nữa. Trong phương thức remove bạn có thể trừ vào giá ngay cả khi mặt hàng không có trong giỏ hàng. Nếu bạn nhấp vào Remove trên cây kem chẳng hạn mà trước đó nó chưa hề được thêm nó vào giỏ hàng, tổng tiền được hiển thị của bạn sẽ là -5,00.

Bạn có thể sửa lỗi này bằng cách kiểm tra xem sản phẩm định xóa có tồn tại trước khi trừ đi hay không, nhưng một cách hiệu quả hơn là giảm thiểu các phần trạng thái khác nhau bằng cách chỉ lưu dữ liệu liên quan vào một nơi. Nói cách khác, cố gắng tránh các tham chiếu kép đến cùng một dữ liệu, trong trường hợp này là sản phẩm. Thay vào đó, hãy lưu trữ dữ liệu thô trong một biến state — toàn bộ đối tượng sản phẩm — sau đó thực hiện các phép tính bằng cách sử dụng dữ liệu đó.

Cấu trúc lại component để hàm add() truyền toàn bộ sản phẩm đến reducer và hàm remove() loại bỏ toàn bộ đối tượng. Phương thức getTotal sẽ sử dụng giỏ hàng, và do đó bạn có thể xóa hàm totalReducer. Sau đó, bạn có thể truyền giỏ hàng đến getTotal(), bạn có thể cấu trúc lại để giảm mảng xuống để nó chỉ chứa một giá trị duy nhất:

import { useReducer } from 'react';
import './Product.css';

const currencyOptions = {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
}

function getTotal(cart) {
  const total = cart.reduce((totalCost, item) => totalCost + item.price, 0);
  return total.toLocaleString(undefined, currencyOptions)
}

...

function cartReducer(state, action) {
  switch(action.type) {
    case 'add':
      return [...state, action.product];
    case 'remove':
      const productIndex = state.findIndex(item => item.name === action.product.name);
      if(productIndex < 0) {
        return state;
      }
      const update = [...state];
      update.splice(productIndex, 1)
      return update
    default:
      return state;
  }
}

export default function Product() {
  const [cart, setCart] = useReducer(cartReducer, []);

  function add(product) {
    setCart({ product, type: 'add' });
  }

  function remove(product) {
    setCart({ product, type: 'remove' });
  }

  return(
    <div className="wrapper">
      <div>
        Shopping Cart: {cart.length} total items.
      </div>
      <div>Total: {getTotal(cart)}</div>

      <div>
        {products.map(product => (
          <div key={product.name}>
            <div className="product">
              <span role="img" aria-label={product.name}>{product.emoji}</span>
            </div>
            <button onClick={() => add(product)}>Add</button>
            <button onClick={() => remove(product)}>Remove</button>
          </div>
        ))}
      </div>
    </div>
  )
}

Lưu file lại và quay lại trang web ta sẽ thấy kết quả thao tác sẽ tương tự như sau:

Thêm và xóa sản phẩm

Bằng cách sử dụng Hook useReducer, bạn đã giữ cho phần thân component chính của mình được tổ chức tốt và dễ đọc, vì logic phức tạp để phân tích cú pháp và nối mảng nằm ngoài component. Bạn cũng có thể đưa reducer ra bên ngoài component nếu bạn muốn sử dụng lại nó hoặc bạn có thể tạo một Hook tùy chỉnh để sử dụng trên nhiều component. Bạn có thể làm Hook tùy chỉnh như các hàm dựa trên các Hook cơ bản chẳng hạn như useStateuseReducer hoặc useEffect.

Hook cung cấp cho bạn cơ hội để di chuyển logic state vào và ra khỏi component, trái ngược với các lớp, nơi bạn thường bị ràng buộc với component. Lợi thế này cũng có thể mở rộng sang các component khác. Vì Hook là các hàm, bạn có thể import chúng vào nhiều component thay vì sau đó sử dụng kế thừa hoặc các dạng phức tạp khác của component dạng class.

Trong bước này, bạn đã học cách thiết lập state bằng cách sử dụng state hiện tại. Bạn tạo ra một component cập nhật state sử dụng cả hai Hook là useState và useReducer, và bạn tái cơ cấu component cho các Hook khác nhau để ngăn chặn lỗi và cải thiện việc tái sử dụng.

Phần kết luận

Hook là một thay đổi lớn đối với React, tạo ra một cách mới để chia sẻ logic và cập nhật các component mà không cần sử dụng các lớp. Giờ đây, bạn có thể tạo các component bằng cách sử dụng useState và useReducer, bạn có các công cụ để tạo các dự án phức tạp đáp ứng người dùng và thông tin động. Bạn cũng có kiến ​​thức nền tảng mà bạn có thể sử dụng để khám phá các Hook phức tạp hơn hoặc để tạo các Hook tùy chỉnh.

» Tiếp: Cách chia sẻ state giữa các component với ngữ cảnh (context)
« Trước: Cách quản lý state trên các component dạng class của React
Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực
Copied !!!