0
0
Lập trình
Thaycacac
Thaycacac thaycacac

Cách hoạt động của useState và lý do cần "use client"

Đăng vào 3 tháng trước

• 5 phút đọc

Giới thiệu

Trong React, hook useState cho phép quản lý trạng thái trong các component function. Bài viết này sẽ giải thích cách useState hoạt động dưới lớp vỏ, tại sao nó lại cần sử dụng chỉ thị "use client" trong Next.js, và cung cấp các mẹo thực tiễn để sử dụng hiệu quả hơn.

1) Trạng thái thực sự tồn tại ở đâu

  • React xây dựng một cây Fibers (mỗi component có một Fiber riêng).
  • Mỗi component hàm có một danh sách hook được lưu trữ trên fiber của nó (một cấu trúc giống như danh sách liên kết/mảng).
  • Mỗi lần gọi useState(...) tương ứng với một ô hook trong danh sách đó. Thứ tự phải ổn định giữa các lần render.

2) Mount và update

  • Mount (render lần đầu):
    • React tạo một ô hook với:
      • memoizedState: giá trị hiện tại (tham số khởi tạo hoặc kết quả của hàm khởi tạo lười).
      • queue: một hàng đợi các cập nhật đang chờ xử lý (danh sách liên kết).
      • Trả về [state, dispatch].
  • Update (render lại):
    • React duyệt qua các hooks theo cùng một thứ tự và với mỗi useState:
      • Xả hàng đợi queue các cập nhật, áp dụng chúng theo thứ tự để tạo ra memoizedState mới.
      • Trả về một hàm dispatch ổn định.

Thứ tự rất quan trọng: React xác định mỗi hook theo vị trí gọi trong component. Thay đổi số lượng/thứ tự của hooks giữa các lần render sẽ phá vỡ ánh xạ này.


3) Thực tế của setState (dispatch)

  • Gọi setState(payload) không thay đổi ngay lập tức.
  • Nó tạo ra một đối tượng cập nhật và đẩy nó vào hàng đợi của hook.
  • React lên lịch làm việc trên fiber bị ảnh hưởng (với ưu tiên thích hợp).
  • Trong lần render tiếp theo, React giảm tất cả các cập nhật đã xếp hàng để tính toán trạng thái tiếp theo.

Các loại payload

  • Giá trị: setCount(3) → thay thế bằng 3.
  • Hàm updater: setCount(c => c + 1) → React gọi nó với trạng thái trước đó.
  • Khởi tạo lười (chỉ trên mount): useState(() => heavyInit()) chạy heavyInit() một lần.

4) Cảm giác “Asynchronous” và batching

  • Trong React 18+, các cập nhật trong cùng một tick được batch (sự kiện trình duyệt, promises, timeouts, v.v.).
  • Bạn có thể không thấy trạng thái mới cho đến khi render xử lý hoàn tất hàng đợi.
  • flushSync có thể buộc đồng bộ (sử dụng cẩn thận).

5) Đối tượng đồng thời & ưu tiên (React 18)

  • React có thể gián đoạn một lần render (ví dụ: các cập nhật ưu tiên thấp) và tiếp tục với dữ liệu mới hơn.
  • startTransition đánh dấu các cập nhật là chuyển tiếp (ưu tiên thấp hơn) để việc gõ vẫn mượt mà.
  • Hàng đợi trạng thái làm điều này khả thi vì React có thể chạy lại pipeline reducer một cách xác định.

6) Closures & những điều cần lưu ý

  • Các handler nắm bắt các biến từ render mà chúng được tạo ra. Nếu bạn sử dụng setX(x + 1) trong mã bất đồng bộ, bạn có thể sử dụng một x đã cũ. Ưu tiên hình thức hàm: setX(prev => prev + 1).
  • Không bao giờ gọi hooks trong vòng lặp/điều kiện. Giữ cho thứ tự gọi ổn định.

Tại sao useState cần "use client" trong Next.js App Router

Server Components (RSC) được render trên máy chủ. Chúng:

  • Phải là đầu ra có thể tuần tự (payload giống JSX/JSON).
  • Không thể giữ trạng thái client runtime, đính kèm các handler sự kiện, hoặc truy cập DOM.
  • Không gửi mã của chúng đến trình duyệt (không có JS client).

useState, useEffect, các handler sự kiện (onClick), v.v. yêu cầu một runtime client (trình duyệt hoặc React DOM trên client). Do đó:

  • Bất kỳ file nào sử dụng useState phải bắt đầu với:
javascript Copy
"use client";

Điều này đánh dấu file là một Client Component để:

  • Nó được đóng gói cho trình duyệt.
  • Nó có thể chạy hooks, giữ trạng thái giữa các lần render, xử lý các sự kiện.
  • Nó không thể nhập các mô-đun chỉ dành cho server (khách hàng DB, fs, hành động server trực tiếp, v.v.).

Mô hình tư duy

  • Server Component = lấy/biên soạn dữ liệu, không có UI hooks có trạng thái, không có JS client mặc định.
  • Client Component = UI tương tác (trạng thái, hiệu ứng, refs, handler sự kiện).

Mẫu điển hình (ranh giới server → client)

javascript Copy
// app/posts/page.tsx  (Server Component mặc định)
import PostList từ "./PostList";

export default async function Page() {
  const posts = await fetchPosts(); // dữ liệu phía server
  return <PostList initialPosts={posts} />;
}
javascript Copy
// app/posts/PostList.tsx
"use client";

import { useState } từ "react";

export default function PostList({ initialPosts }) {
  const [filter, setFilter] = useState("");
  const visible = initialPosts.filter(p => p.title.includes(filter));
  return (
    <>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      <ul>{visible.map(p => <li key={p.id}>{p.title}</li>)}</ul>
    </>
  );
}
  • Server tải dữ liệu một cách hiệu quả.
  • Client component xử lý trạng thái & tính tương tác.
  • Chỉ PostList (và các phụ thuộc chỉ client của nó) được gửi đến trình duyệt.

Mẹo thực tiễn

  • Ưu tiên cập nhật hàm khi trạng thái tiếp theo phụ thuộc vào trước đó:
javascript Copy
setCount(c => c + 1);
  • Sử dụng khởi tạo lười cho trạng thái khởi tạo tốn kém:
javascript Copy
const [value] = useState(() => computeOnce());
  • Tránh việc tính toán trạng thái mà bạn có thể tính từ props trong quá trình render; tính toán ngay hoặc lưu trữ.
  • Nếu một component chỉ cần trạng thái cho một phần nhỏ, hãy xem xét một child client nhỏ thay vì đánh dấu cả trang với "use client".
  • Nhớ rằng: mỗi cuộc gọi useState là theo từng instance. Mỗi instance component được gắn có các ô hook và hàng đợi riêng của nó.

Kết luận

Hy vọng bài viết này đã giúp bạn hiểu rõ hơn về cách hoạt động của useState và lý do tại sao nó yêu cầu chỉ thị "use client" trong Next.js. Để tìm hiểu thêm về React và các hook, hãy tham khảo tài liệu chính thức và thực hành trên các dự án của bạn. Nếu bạn có câu hỏi hoặc muốn chia sẻ kinh nghiệm của mình, hãy để lại ý kiến dưới bài viết này nhé!

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào