Bài 16.1: Typing state trong functional components với React-TypeScript

Bài 16.1: Typing state trong functional components với React-TypeScript
Chào mừng trở lại với chuỗi bài lập trình Front-end! Hôm nay, chúng ta sẽ đào sâu vào một khía cạnh cực kỳ quan trọng khi làm việc với React và TypeScript: typing state trong các functional component sử dụng hook useState
.
Việc định nghĩa kiểu dữ liệu rõ ràng cho state không chỉ giúp chúng ta tránh được nhiều lỗi tiềm ẩn ngay từ giai đoạn viết mã, mà còn cải thiện đáng kể khả năng đọc hiểu và bảo trì code. TypeScript mang lại sức mạnh kiểm tra kiểu tĩnh, và áp dụng nó cho state là một bước đi thông minh để xây dựng các ứng dụng React mạnh mẽ và đáng tin cậy hơn.
Hãy cùng khám phá cách TypeScript hoạt động với useState!
useState
và Type Inference (Suy luận kiểu)
Trong nhiều trường hợp đơn giản, TypeScript đủ thông minh để suy luận (infer) kiểu dữ liệu của state dựa trên giá trị khởi tạo mà bạn cung cấp cho useState
.
Ví dụ:
import React, { useState } from 'react';
function Counter() {
// TypeScript suy luận 'count' là kiểu 'number'
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Tăng</button>
<button onClick={decrement}>Giảm</button>
</div>
);
}
Ở đây, vì giá trị khởi tạo là 0
(một số), TypeScript tự động biết rằng count
sẽ luôn là một number
. Nếu bạn cố gắng gọi setCount('hello')
, TypeScript sẽ báo lỗi ngay lập tức.
Tương tự, nếu bạn khởi tạo state với một chuỗi hoặc boolean:
import React, { useState } from 'react';
function Greeting() {
// TypeScript suy luận 'name' là kiểu 'string'
const [name, setName] = useState('');
// TypeScript suy luận 'isActive' là kiểu 'boolean'
const [isActive, setIsActive] = useState(false);
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
<p>Hello, {name || 'Guest'}!</p>
<p>Status: {isActive ? 'Active' : 'Inactive'}</p>
<button onClick={() => setIsActive(!isActive)}>Toggle Status</button>
</div>
);
}
TypeScript sẽ suy luận name
là string
và isActive
là boolean
. Việc gán giá trị không đúng kiểu sẽ bị bắt lỗi.
Suy luận kiểu rất tiện lợi cho các kiểu dữ liệu nguyên thủy đơn giản.
Explicit Typing (Định nghĩa kiểu tường minh)
Tuy nhiên, không phải lúc nào suy luận kiểu cũng đủ hoặc chính xác như chúng ta mong muốn, đặc biệt là khi làm việc với:
- State có thể nhận nhiều kiểu (ví dụ: một kiểu dữ liệu hoặc
null
). - State khởi tạo là
null
hoặcundefined
. - State là mảng hoặc đối tượng phức tạp.
Trong những trường hợp này, chúng ta cần định nghĩa kiểu tường minh cho useState
bằng cách sử dụng cú pháp Generic <Type>
:
const [stateVariable, setStateVariable] = useState<Type>(initialValue);
Hãy xem các ví dụ:
1. State có thể là một kiểu hoặc null
Đây là trường hợp rất phổ biến khi dữ liệu có thể đang được tải hoặc chưa có sẵn.
import React, { useState, useEffect } from 'react';
// Định nghĩa interface cho dữ liệu người dùng
interface User {
id: number;
name: string;
email: string;
}
function UserProfile() {
// State có thể là User hoặc null
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Giả lập tải dữ liệu
setTimeout(() => {
setUser({ id: 1, name: 'Alice', email: 'alice@example.com' });
setIsLoading(false);
}, 1000);
}, []);
if (isLoading) {
return <p>Đang tải thông tin người dùng...</p>;
}
// Nhờ TypeScript, chúng ta biết user không null ở đây (nếu logic đúng)
// Tuy nhiên, khi truy cập user, bạn vẫn cần kiểm tra null hoặc sử dụng optional chaining (?)
return (
<div>
<h2>Thông tin người dùng</h2>
{user ? ( // Kiểm tra user không null trước khi truy cập thuộc tính
<>
<p>ID: {user.id}</p>
<p>Tên: {user.name}</p>
<p>Email: {user.email}</p>
</>
) : (
<p>Không tìm thấy người dùng.</p> // Trường hợp tải thất bại hoặc user vẫn là null
)}
</div>
);
}
Trong ví dụ này:
useState<User | null>(null)
: Chúng ta nói rõ với TypeScript rằng stateuser
có thể là một đối tượngUser
hoặc lànull
. Điều này là cần thiết vì giá trị khởi tạo lànull
. Nếu bạn chỉ dùnguseState(null)
, TypeScript sẽ suy luận kiểu làany
hoặcnull
, làm mất đi tính an toàn kiểu khi bạn gán một đối tượngUser
sau này.- Sử dụng
User | null
đảm bảo rằng khi bạn truy cập các thuộc tính củauser
(ví dụ:user.name
), TypeScript sẽ yêu cầu bạn xử lý trường hợpuser
lànull
(ví dụ: dùnguser ? user.name : 'N/A'
hoặc optional chaininguser?.name
).
2. Typing Arrays (Mảng)
Khi state là một mảng, chúng ta cần định nghĩa kiểu cho các phần tử trong mảng.
import React, { useState } from 'react';
function TodoList() {
// Định nghĩa interface cho mỗi công việc (Todo item)
interface Todo {
id: number;
text: string;
isCompleted: boolean;
}
// State là một mảng các đối tượng Todo, khởi tạo là mảng rỗng
const [todos, setTodos] = useState<Todo[]>([]);
const [newTask, setNewTask] = useState('');
const addTodo = () => {
if (newTask.trim()) {
const newTodo: Todo = { // Đảm bảo đối tượng mới đúng kiểu Todo
id: Date.now(), // Sử dụng timestamp làm ID đơn giản
text: newTask,
isCompleted: false,
};
setTodos([...todos, newTodo]); // Thêm đối tượng Todo mới vào mảng
setNewTask('');
}
};
const toggleComplete = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, isCompleted: !todo.isCompleted } : todo
));
};
return (
<div>
<h2>Danh sách công việc</h2>
<input
type="text"
value={newTask}
onChange={(e) => setNewTask(e.target.value)}
placeholder="Thêm công việc mới"
/>
<button onClick={addTodo}>Thêm</button>
<ul>
{todos.map(todo => ( // Duyệt qua mảng các đối tượng Todo
<li
key={todo.id}
style={{ textDecoration: todo.isCompleted ? 'line-through' : 'none' }}
onClick={() => toggleComplete(todo.id)}
>
{todo.text}
</li>
))}
</ul>
</div>
);
}
Trong ví dụ này:
interface Todo { ... }
: Chúng ta định nghĩa cấu trúc của một đối tượngTodo
.useState<Todo[]>([]))
: Chúng ta nói rõ rằng statetodos
là một mảng ([]
) mà mỗi phần tử bên trong nó phải tuân theo cấu trúc của interfaceTodo
. Việc khởi tạo với[]
(mảng rỗng) làm cho suy luận kiểu khó chính xác, nên việc định nghĩa tường minh<Todo[]>
là cực kỳ quan trọng. Nếu không, TypeScript có thể suy luậntodos
làany[]
.- Khi thêm
newTodo
hoặc cập nhật các mục trong mảng, TypeScript sẽ kiểm tra xem bạn có đang thao tác đúng với kiểuTodo[]
hay không.
3. Typing Objects (Đối tượng)
Tương tự như mảng, khi state là một đối tượng có cấu trúc phức tạp hơn, việc định nghĩa kiểu tường minh là cần thiết, thường kết hợp với interface
hoặc type
.
import React, { useState } from 'react';
// Định nghĩa interface cho cài đặt cấu hình
interface Settings {
theme: 'light' | 'dark'; // Sử dụng Union Type
fontSize: number;
notificationsEnabled: boolean;
}
function UserSettings() {
// State là một đối tượng Settings, khởi tạo với giá trị mặc định
const [settings, setSettings] = useState<Settings>({
theme: 'light',
fontSize: 16,
notificationsEnabled: true,
});
const toggleTheme = () => {
setSettings({
...settings, // Giữ lại các thuộc tính khác
theme: settings.theme === 'light' ? 'dark' : 'light', // Cập nhật thuộc tính 'theme'
});
};
const changeFontSize = (size: number) => {
setSettings({
...settings,
fontSize: size, // Cập nhật thuộc tính 'fontSize'
});
}
return (
<div>
<h2>Cài đặt người dùng</h2>
<p>Chủ đề: {settings.theme}</p>
<button onClick={toggleTheme}>Chuyển đổi chủ đề</button>
<p>Cỡ chữ: {settings.fontSize}</p>
<button onClick={() => changeFontSize(settings.fontSize + 2)}>Tăng cỡ chữ</button>
<p>Thông báo: {settings.notificationsEnabled ? 'Đã bật' : 'Đã tắt'}</p>
{/* Thêm nút toggle thông báo nếu cần */}
</div>
);
}
Trong ví dụ này:
interface Settings { ... }
: Chúng ta định nghĩa cấu trúc của đối tượng statesettings
. Lưu ý cách sử dụngUnion Type
('light' | 'dark'
) cho thuộc tínhtheme
, giới hạn giá trị chỉ trong hai chuỗi này.useState<Settings>({ ... })
: Chúng ta nói rõ rằng statesettings
phải tuân theo cấu trúc của interfaceSettings
.- Khi gọi
setSettings
, TypeScript sẽ kiểm tra xem đối tượng mới bạn cung cấp có khớp với kiểuSettings
hay không, bắt lỗi nếu bạn cố gắng thêm/xóa thuộc tính hoặc gán sai kiểu dữ liệu cho một thuộc tính.
Lợi ích của việc Typing State
Tóm lại, việc dành thời gian typing state với TypeScript mang lại nhiều lợi ích to lớn:
- Bắt lỗi sớm: Hầu hết các lỗi liên quan đến kiểu dữ liệu của state sẽ được phát hiện ngay trong quá trình phát triển thay vì lúc chạy (runtime).
- Code rõ ràng hơn: Việc định nghĩa kiểu giúp tài liệu hóa code của bạn. Bất kỳ ai đọc component đều có thể dễ dàng hiểu cấu trúc dữ liệu mà state đang nắm giữ.
- Hỗ trợ từ IDE tốt hơn: Các IDE (như VS Code) sử dụng thông tin kiểu để cung cấp tính năng tự động hoàn thành (autocompletion), gợi ý tham số và refactoring đáng tin cậy hơn.
- Bảo trì dễ dàng hơn: Khi cấu trúc state thay đổi, TypeScript sẽ chỉ ra tất cả những nơi trong code cần được cập nhật, giúp quá trình refactoring an toàn hơn.
Comments