Chuyển đến nội dung chính

Trạng thái của component trong React

·7 phút· loading · loading · ·
Lập Trình Javascript Reactjs New-Reactjs-Docs React.dev Reactjs-Basic UseState
Duy Trung
Tác giả
Duy Trung
Thoughts run like the wind blows
Mục lục

Dalai Lama trong thế giới của component nói: “We don’t grow, we change”.

useState là một Hook giúp bạn thêm biến state vào một component trong React.

const [state, setState] = useState(initialState)

Cách sử dụng
#

Thêm state cho component
#

Gọi useState ở ngay đầu component sẽ khai báo các biến state.

import { useState } from 'react';

function MyComponent() {
  const [age, setAge] = useState(42);
  const [name, setName] = useState('Taylor');
  // ...

Một quy ước khi đặt tên biến là [something, setSomething] sử dụng phân rã mảng - array destructuring.

useState trả về một mảng có đúng hai phần tử:

  1. Trạng thái hiện tại của biến state, được khởi tạo với một giá trị ban đầu initialState.
  2. Hàm set dùng để gán giá trị mới cho biến state.

Để cập nhật những gì nhìn thấy trên màn hình, gọi hàm set với state mới:

function handleClick() {
  setName('Robin');
}

React sẽ lưu trữ state mới, render lại component với giá trị mới và làm tươi UI.

Sai lầm thường gặp Gọi hàm set không thay đổi ngay lập tức giá trị của state:

function handleClick() {
  setName('Robin');
  console.log(name); // Vẫn là "Taylor"!
}

Giá trị mới chỉ được cập nhật ở lần render tiếp theo.

Cập nhật state dựa trên state trước
#

Giả sử age42. Handler sau gọi setAge(age + 1) ba lần:

function handleClick() {
  setAge(age + 1); // setAge(42 + 1)
  setAge(age + 1); // setAge(42 + 1)
  setAge(age + 1); // setAge(42 + 1)
}

Tuy nhiên sau khi bấm nút, age sẽ chỉ là 43 chứ không phải 45! Như đã nói hàm set không cập nhật giá trị ngay. Nên mỗi lần gọi setAge(age + 1) chỉ thêm cộng thêm vào age tại thời điểm hiện tại một đơn vị tức là tương đương setAge(43).

Để giải quyết vấn đề, bạn cần truyền đối số là một hàm thay vì một giá trị:

function handleClick() {
  setAge(a => a + 1); // setAge(42 => 43)
  setAge(a => a + 1); // setAge(43 => 44)
  setAge(a => a + 1); // setAge(44 => 45)
}

Ở đây, a => a + 1 là hàm cập nhật, nó tính state mới dựa trên state trung gian. React gom các hàm cập nhật và đặt vào một hàng đợi. Sau đó trong lần render tiếp theo, nó sẽ gọi lần lượt theo đúng thứ tự trong code:

  1. a => a + 1 nhận 42 là giá trị trung gian và trả về state mới là 43.
  2. a => a + 1 nhận 43 là giá trị trung gian và trả về state mới là 44.
  3. a => a + 1 nhận 44 là giá trị trung gian và trả về state mới là 45.

Lúc này hàng đợi đã rỗng, React sẽ lưu trữ giá trị cuối cho state bằng 45.

Một quy ước thường gặp là đặt tên state trung gian bằng chữ cái đầu tiên trong tên biến state, như a cho age. Tuy nhiên, bạn cũng có thể đặt là prevAge gì đó miễn là bạn thấy dễ hiểu.

Ở mode development, React sẽ gọi hàm cập nhật này hai lần để đảm bảo chúng là hàm thuần khiết.

Cập nhật object và array trong state
#

State không chỉ là chuỗi hay số, mà còn có thể là object hay array. Trong React, state là read-only, do đó bạn phải thay thế chứ không thay đổi object của state. Chẳng hạn, nếu bạn có một object form, không nên cập nhật giá trị như này:

form.firstName = 'Taylor';

Thay vào đó, thay thế toàn bộ cái cũ bởi một cái mới hoàn toàn:

setForm({
  ...form,
  firstName: 'Taylor'
});

Tránh tạo lại state ban đầu
#

React lưu lại state ban đầu một lần duy nhất và bỏ qua trong những lần render sau.

function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos());
  // ...

Mặc dù kết quả createInitialTodos() chỉ được dùng cho lần render đầu tiên, nó vẫn được gọi cho mỗi lần render. Điều này sẽ tạo ra vấn đề về hiệu năng nếu hàm này tiêu tốn nhiều tài nguyên.

Để khắc phục, hãy sửa lại chút ít:

function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos);
  // ...

Bạn có nhận thấy điểm khác? Đoạn code sau truyền vào createInitialTodos không có cặp ngoặc tròn (), tức là bản thân hàm đó như một giá trị, chứ không phải truyền vào kết quả thực thi của hàm đó khi gọi createInitialTodos(). Khi truyền như vậy, React sẽ chỉ gọi nó một lần trong khi khởi tạo.

Reset state với key
#

Bạn có thể reset toàn bộ state bằng cách thay đổi prop key truyền vào một component. Đọc giữ và reset state.

Giữ lại dữ liệu từ lần render trước
#

Thông thường, bạn sẽ update state trong event handler. Tuy nhiên, một vài trường hợp ít gặp sẽ yêu cầu bạn điều chỉnh state ngay khi render - chẳng hạn, bạn sẽ thay đổi state theo thay đổi của props.

Component CountLabel hiển thị giá trị count được truyền từ ngoài vào:

export default function CountLabel({ count }) {
  return <h1>{count}</h1>
}

Bạn muốn hiển thị thêm thông tin là bộ đếm này đang tăng hay giảm. Để làm được vậy bạn cần theo dõi cả giá trị cũ của count, ta gọi là prevCount. Tiếp đến ta thêm một state trend để lưu trữ thông tin xu hướng của bộ đếm. So sánh prevCountcount, nếu chúng khác nhau thì cập nhật cả prevCounttrend.

import { useState } from 'react';

export default function CountLabel({ count }) {
  const [prevCount, setPrevCount] = useState(count);
  const [trend, setTrend] = useState(null);
  if (prevCount !== count) {
    setPrevCount(count);
    setTrend(count > prevCount ? 'increasing' : 'decreasing');
  }
  return (
    <>
      <h1>{count}</h1>
      {trend && <p>The count is {trend}</p>}
    </>
  );
}

Lưu ý khi bạn gọi hàm set trong render, nó phải được đặt trong điều kiện rẽ nhánh như prevCount !== count, nếu không component sẽ render vô hạn và boom! Thêm nữa, bạn chỉ có thể cập nhật state của component đang render: lỗi sẽ xảy ra nếu bạn cố gọi hàm set của một component khác.

Pattern này nói chung khó hiểu và không nên dùng nhiều. Tuy vậy vẫn tốt hơn là dùng Effect. Khi bạn gọi hàm set trong render, React sẽ render lại component ngay sau khi return, trước khi render các con. Bằng cách này, component sẽ không phải render hai lần. Phần còn lại của component vấn được thực hiện (dù bị vứt đi ngay sau đó), nên nếu có thể, bạn hãy return sớm ngay trong điều kiện rẽ nhánh để kích hoạt việc render lại sớm hơn.

Câu hỏi thường gặp
#

Tôi muốn đặt một hàm làm giá trị cho state, nhưng hàm lại bị gọi liên tục
#

Bạn không thể đặt một hàm vào state như này:

const [fn, setFn] = useState(someFunction);

function handleClick() {
  setFn(someOtherFunction);
}

Vì React sẽ hiểu someFunction là hàm khởi tạo, và someOtherFunction là hàm cập nhật, nên nó sẽ gọi hàm mỗi lần render và lưu lại kết quả. Để thực sự lưu một hàm, bạn cần thêm () => trước cả hai trường hợp. React sẽ lưu hàm như một giá trị vào state thay vì gọi nó.

const [fn, setFn] = useState(someFunction);

function handleClick() {
  setFn(someOtherFunction);
}

Tôi gặp lỗi “Too many re-renders. React limits the number of renders to prevent an infinite loop”
#

Bạn đang đặt hàm set trong render mà không có điều kiện dừng, nên component rơi vào vòng lặp vô hạn. Ngoài ra, một lỗi phổ biến là bạn đặt sai event handler:

// 🚩 Sai: Gọi handler mỗi lần render
return <button onClick={handleClick()}>Click me</button>

// ✅ Đúng: Truyền handler như giá trị, không gọi
return <button onClick={handleClick}>Click me</button>

// ✅ Đúng: Sử dụng hàm mũi tên, không gọi hàm
return <button onClick={(e) => handleClick(e)}>Click me</button>

Tài liệu tham khảo
#

Reactjs đang viết lại trang tài liệu chính thức. Một vài điểm đáng chú ý về nội dung mới:

  • Tất cả diễn giải sử dụng Hook thay cho class.
  • Tăng graphic trực quan và ví dụ tương tác.
  • Có câu hỏi (kèm lời giải!) để kiểm tra mức độ hiểu bài của độc giả.

Ở thời điểm năm 2022, bản beta của tài liệu mới tại đây.

Bài viết này dựa phần lớn trên bản gốc tại đây.

Bài liên quan

Tree-shake Lodash trong Next.js
·5 phút· loading · loading
Lập Trình Javascript Node.js Next.js
Lý Tiểu Long nói đại ý: không sợ kẻ biết 1 vạn kiểu đá, chỉ sợ kẻ tập 1 kiểu 1 vạn lần.
Ưu tiên undefined hơn null!
·4 phút· loading · loading
Lập Trình Javascript Best-Practices Eslint Linting

Lão Tử nói: “Đạo mà diễn tả được thì đó không còn là đạo bất biến nữa, tên mà gọi ra được thì đó không còn là tên bất biến nữa.”

Number trong Javascript: Dấu chấm động và IEEE 754
·10 phút· loading · loading
Lập Trình Javascript Floating-Point Ieee-754 Arithmetic
0.1 + 0.2 !== 0.3 (╯ ͡• ͜ʖ ͡•)╯┻━┻