Bài 15.4: useState và useEffect với TypeScript

Bài 15.4: useState và useEffect với TypeScript
Chào mừng các bạn trở lại! Hôm nay, chúng ta sẽ khám phá hai bộ đôi hoàn hảo trong thế giới React: useState
và useEffect
. Đây là những "bí kíp" giúp chúng ta quản lý trạng thái (state) và xử lý các tác vụ phụ (side effects) trong các component function của mình. Và tuyệt vời hơn nữa, chúng ta sẽ tích hợp sức mạnh của TypeScript để làm cho code của chúng ta an toàn, dễ bảo trì và mạnh mẽ hơn bao giờ hết!
Hãy cùng bắt đầu hành trình chinh phục useState
và useEffect
cùng với TypeScript nhé!
useState: Quản lý Trạng thái (State) trong Component của Bạn
Trong lập trình Front-end, "trạng thái" (state) đề cập đến dữ liệu có thể thay đổi theo thời gian và ảnh hưởng đến cách component của bạn hiển thị hoặc hoạt động. Ví dụ: giá trị nhập liệu của người dùng, trạng thái loading, danh sách các mục hiển thị, v.v.
Trong các component function của React, hook useState
là công cụ chính giúp chúng ta tạo và quản lý những trạng thái này một cách hiệu quả.
Cú pháp cơ bản của useState
trông như thế này:
import React, { useState } from 'react';
const [stateVariable, setStateFunction] = useState(initialValue);
stateVariable
: Biến này sẽ lưu trữ giá trị hiện tại của trạng thái.setStateFunction
: Đây là một hàm mà bạn gọi để cập nhật giá trị của trạng thái. Khi hàm này được gọi, React sẽ re-render component với giá trị trạng thái mới.initialValue
: Giá trị khởi tạo cho trạng thái. Nó có thể là bất kỳ kiểu dữ liệu nào (số, chuỗi, boolean, đối tượng, mảng, null, undefined).
TypeScript và useState: Thêm Lớp Bảo Vệ
Sức mạnh của TypeScript phát huy khi chúng ta sử dụng useState
bằng cách chỉ định rõ ràng kiểu dữ liệu của trạng thái. Điều này giúp TypeScript kiểm tra lỗi ngay trong quá trình phát triển, ngăn chặn chúng ta gán sai kiểu dữ liệu cho trạng thái.
Đây là cách chúng ta khai báo trạng thái với kiểu dữ liệu cụ thể:
import React, { useState } from 'react';
// Khai báo trạng thái kiểu number
const [count, setCount] = useState<number>(0);
// Khai báo trạng thái kiểu string
const [name, setName] = useState<string>('');
// Khai báo trạng thái kiểu boolean
const [isLoading, setIsLoading] = useState<boolean>(false);
Trong các ví dụ trên, <number>
, <string>
, <boolean>
là các generic type mà chúng ta truyền cho useState
. Điều này báo cho TypeScript biết rằng count
luôn là một số, name
luôn là một chuỗi và isLoading
luôn là một boolean.
Ví dụ với Object và Array:
Khi trạng thái là đối tượng hoặc mảng, việc định nghĩa kiểu dữ liệu càng trở nên quan trọng để đảm bảo tính nhất quán của dữ liệu.
import React, { useState } from 'react';
// Định nghĩa kiểu cho một đối tượng User
interface User {
id: number;
name: string;
age: number;
}
// Khai báo trạng thái kiểu User (có thể là null ban đầu)
const [user, setUser] = useState<User | null>(null);
// Định nghĩa kiểu cho một mảng các đối tượng Todo
interface Todo {
id: number;
text: string;
completed: boolean;
}
// Khai báo trạng thái kiểu mảng các Todo
const [todos, setTodos] = useState<Todo[]>([]);
Trong ví dụ trên:
- Chúng ta định nghĩa
interface User
vàinterface Todo
để mô tả cấu trúc dữ liệu của đối tượng và các phần tử trong mảng. useState<User | null>(null)
: Báo cho TypeScript biết rằnguser
có thể là một đối tượngUser
hoặcnull
. Giá trị khởi tạo lànull
.useState<Todo[]>([]):
Báo cho TypeScript biết rằngtodos
là một mảng các đối tượngTodo
. Giá trị khởi tạo là một mảng rỗng.
Tại sao lại dùng TypeScript ở đây?
Imagine bạn có trạng thái user
và bạn cố gắng gán một giá trị không đúng kiểu, ví dụ: setUser("Tôi là một chuỗi!")
. Nếu không có TypeScript, lỗi này có thể không xuất hiện cho đến khi runtime, gây khó khăn trong việc debug. Với TypeScript, trình biên dịch sẽ báo lỗi ngay lập tức, giúp bạn phát hiện và sửa lỗi trước khi chạy ứng dụng. Điều này cực kỳ hữu ích cho các ứng dụng lớn và phức tạp.
useEffect: Xử lý Tác vụ Phụ (Side Effects) Đỉnh Cao
Trong một ứng dụng React thuần túy, việc render UI dựa trên trạng thái và props là mục tiêu chính. Tuy nhiên, trong thực tế, chúng ta thường cần thực hiện các tác vụ "phụ" không trực tiếp liên quan đến việc render, như:
- Gọi API để lấy dữ liệu.
- Thao tác trực tiếp với DOM (ví dụ: thiết lập tiêu đề trang).
- Thiết lập hoặc xóa các listeners cho sự kiện (event listeners).
- Thiết lập hoặc xóa các bộ đếm thời gian (timers).
- Thiết lập các subscription.
Hook useEffect
là câu trả lời của React cho việc xử lý các side effects này trong các component function.
Cú pháp cơ bản của useEffect
trông như thế này:
import React, { useEffect } from 'react';
useEffect(() => {
// Code xử lý side effect của bạn
// ...
// (Tùy chọn) Hàm cleanup
return () => {
// Code dọn dẹp (hủy subscription, xóa timer, v.v.)
// ...
};
}, [dependencies]); // Mảng phụ thuộc (dependency array)
- Tham số đầu tiên: Một hàm (gọi là effect function) chứa code xử lý side effect.
- Tham số thứ hai (tùy chọn): Một mảng các giá trị (gọi là dependency array). React sẽ chạy lại effect function mỗi khi bất kỳ giá trị nào trong mảng này thay đổi.
Các trường hợp sử dụng phổ biến của Dependency Array:
Không có mảng phụ thuộc:
useEffect(() => { ... });
- Effect sẽ chạy mỗi khi component render lại. Thường ít khi sử dụng vì có thể gây vòng lặp vô hạn hoặc hiệu năng kém.
Mảng phụ thuộc rỗng:
useEffect(() => { ... }, []);
- Effect chỉ chạy một lần duy nhất sau lần render đầu tiên của component (tương tự như
componentDidMount
trong class components). Thường dùng để fetch dữ liệu ban đầu, thiết lập listeners global, v.v.
- Effect chỉ chạy một lần duy nhất sau lần render đầu tiên của component (tương tự như
Mảng phụ thuộc với các giá trị:
useEffect(() => { ... }, [prop1, state1]);
- Effect sẽ chạy sau lần render đầu tiên VÀ mỗi khi giá trị của
prop1
hoặcstate1
thay đổi. Đây là cách phổ biến nhất để đảm bảo effect chạy lại khi các dữ liệu mà nó phụ thuộc vào bị cập nhật.
- Effect sẽ chạy sau lần render đầu tiên VÀ mỗi khi giá trị của
Hàm Cleanup trong useEffect
Nhiều side effects cần được "dọn dẹp" khi component unmount hoặc khi effect chạy lại. Ví dụ: hủy bỏ một subscription, xóa một timer, loại bỏ một event listener. Để làm điều này, useEffect
cho phép bạn trả về một hàm cleanup từ effect function.
useEffect(() => {
console.log('Effect chạy!');
// Thiết lập một timer
const timerId = setInterval(() => {
console.log('Timer tick...');
}, 1000);
// Hàm cleanup
return () => {
console.log('Cleanup chạy!');
// Xóa timer khi component unmount hoặc effect chạy lại
clearInterval(timerId);
};
}, []); // Mảng rỗng để effect chỉ chạy một lần
- Hàm cleanup sẽ chạy trước khi effect chạy lại (trừ lần chạy đầu tiên) và khi component bị unmount.
TypeScript và useEffect: Đảm bảo An toàn trong Side Effects
TypeScript không thay đổi cú pháp của useEffect
một cách trực tiếp như useState
. Tuy nhiên, nó đóng vai trò quan trọng trong việc đảm bảo các hàm và dữ liệu bạn sử dụng bên trong effect function là đúng kiểu.
Ví dụ: Fetching Data với TypeScript
Khi fetch data, chúng ta thường làm việc với dữ liệu có cấu trúc. TypeScript giúp đảm bảo dữ liệu nhận được từ API khớp với kiểu dữ liệu chúng ta mong đợi.
import React, { useState, useEffect } from 'react';
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
function PostViewer({ postId }: { postId: number }) {
const [post, setPost] = useState<Post | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setIsLoading(true);
setError(null);
setPost(null); // Reset post khi postId thay đổi
const fetchPost = async () => {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: Post = await response.json(); // Ép kiểu dữ liệu nhận được
setPost(data);
} catch (err: any) { // Bắt lỗi và ép kiểu
setError(err.message);
} finally {
setIsLoading(false);
}
};
fetchPost();
}, [postId]); // Effect chạy lại khi postId thay đổi
if (isLoading) return <p>Đang tải bài viết...</p>;
if (error) return <p style={{ color: 'red' }}>Lỗi: {error}</p>;
if (!post) return <p>Không tìm thấy bài viết.</p>;
return (
<div>
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
);
}
Trong ví dụ này:
- Chúng ta định nghĩa
interface Post
để mô tả cấu trúc của dữ liệu bài viết. useState<Post | null>(null)
: Trạng tháipost
được khai báo làPost
hoặcnull
.const data: Post = await response.json();
: Khi nhận được dữ liệu từ API, chúng ta ép kiểu nó sangPost
. Nếu cấu trúc dữ liệu thực tế không khớp vớiinterface Post
, TypeScript (và các công cụ linting như ESLint với plugin TypeScript) có thể cảnh báo bạn, giúp bạn phát hiện sai sót sớm.catch (err: any)
: Lỗi trong JavaScript có thể có nhiều dạng, sử dụngany
ở đây là một cách để xử lý nhanh, nhưng trong các ứng dụng lớn, bạn có thể muốn định nghĩa kiểu lỗi cụ thể hơn hoặc kiểm tra kiểu runtime.- Mảng phụ thuộc
[postId]
: Đảm bảo effect chạy lại mỗi khi giá trị củapostId
thay đổi, cho phép chúng ta tải bài viết mới khi người dùng chọn bài khác.
Kết hợp useState và useEffect: Sức mạnh Tổng hợp
useState
và useEffect
thường hoạt động song song. useState
quản lý dữ liệu mà UI hiển thị, trong khi useEffect
thực hiện các tác vụ (như gọi API) để cập nhật dữ liệu đó dựa trên những thay đổi về trạng thái hoặc props.
Ví dụ: Một component hiển thị danh sách người dùng. Người dùng có thể nhập tên vào một ô input để tìm kiếm.
import React, { useState, useEffect } from 'react';
interface User {
id: number;
name: string;
}
function UserSearch() {
const [searchTerm, setSearchTerm] = useState<string>('');
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Bỏ qua search nếu searchTerm rỗng để tránh fetch không cần thiết
if (!searchTerm) {
setUsers([]);
return;
}
setIsLoading(true);
setError(null);
// Hàm giả lập gọi API tìm kiếm người dùng
const fetchUsers = async () => {
try {
// Thay thế bằng fetch API thực tế
console.log(`Đang tìm kiếm người dùng với từ khóa: ${searchTerm}`);
const response = await fetch(`https://jsonplaceholder.typicode.com/users?name_like=${searchTerm}`); // Sử dụng jsonplaceholder cho ví dụ
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: User[] = await response.json();
setUsers(data);
} catch (err: any) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
const debounceFetch = setTimeout(() => {
fetchUsers();
}, 500); // Debounce 500ms để tránh gọi API quá nhanh khi người dùng đang gõ
// Cleanup: Hủy bỏ timer nếu searchTerm thay đổi trước khi fetchUsers được gọi
return () => clearTimeout(debounceFetch);
}, [searchTerm]); // Effect chạy lại mỗi khi searchTerm thay đổi
return (
<div>
<h1>Tìm kiếm người dùng</h1>
<input
type="text"
placeholder="Nhập tên người dùng..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
{isLoading && <p>Đang tìm kiếm...</p>}
{error && <p style={{ color: 'red' }}>Lỗi: {error}</p>}
{!isLoading && !error && users.length > 0 && (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
{!isLoading && !error && users.length === 0 && searchTerm && (
<p>Không tìm thấy người dùng nào với từ khóa "{searchTerm}".</p>
)}
{!isLoading && !error && users.length === 0 && !searchTerm && (
<p>Nhập tên để bắt đầu tìm kiếm.</p>
)}
</div>
);
}
Trong ví dụ này:
useState<string>('')
chosearchTerm
: Quản lý giá trị nhập liệu của người dùng.useState<User[]>([]):
Quản lý danh sách người dùng tìm được.useEffect(() => { ... }, [searchTerm])
: Effect này phụ thuộc vàosearchTerm
. Mỗi khi người dùng gõ (vàsearchTerm
thay đổi), effect sẽ chạy lại.- Bên trong
useEffect
, chúng ta thực hiện logic tìm kiếm (ở đây là giả lập hoặc gọi API thật). - Chúng ta sử dụng
setTimeout
vàclearTimeout
(hàm cleanup) để thực hiện debounce. Điều này nghĩa là API tìm kiếm chỉ được gọi sau khi người dùng dừng gõ trong 500ms, giúp giảm tải cho server. - Kết quả tìm kiếm được cập nhật vào trạng thái
users
bằngsetUsers(data)
. - Component render lại dựa trên trạng thái
users
,isLoading
, vàerror
.
Ví dụ này thể hiện rõ cách useState
(quản lý trạng thái nhập liệu và kết quả) và useEffect
(thực hiện tác vụ tìm kiếm phụ thuộc vào trạng thái nhập liệu) phối hợp nhịp nhàng với sự hỗ trợ kiểu dữ liệu từ TypeScript.
Như bạn thấy, useState
và useEffect
là hai hook cực kỳ mạnh mẽ và thiết yếu khi làm việc với React function components. Khi kết hợp chúng với TypeScript, chúng ta không chỉ viết code hoạt động đúng mà còn đảm bảo tính an toàn, rõ ràng và dễ bảo trì hơn rất nhiều. Việc định nghĩa kiểu dữ liệu cho trạng thái và dữ liệu trong side effects là một bước đi quan trọng để xây dựng các ứng dụng Front-end vững chắc.
Comments