Bài 15.5: Bài tập thực hành ứng dụng React-TypeScript

Bài 15.5: Bài tập thực hành ứng dụng React-TypeScript
Chào mừng trở lại chuỗi bài về Lập trình Web Front-end! Nếu bạn đã theo dõi từ đầu, chắc hẳn bạn đã có nền tảng vững chắc về HTML, CSS, JavaScript cơ bản, và giờ đây là React cùng TypeScript. Việc học lý thuyết là quan trọng, nhưng để thực sự nắm vững và áp dụng vào các dự án thực tế, không có gì hiệu quả hơn việc thực hành!
Bài viết này không chỉ đơn thuần là lý thuyết, mà là sân chơi để bạn áp dụng ngay những gì đã học thông qua các bài tập và ví dụ cụ thể. Chúng ta sẽ cùng nhau xây dựng những đoạn code nhỏ nhưng mạnh mẽ, kết hợp sự linh hoạt của React với sự bảo vệ và minh bạch của TypeScript.
Sự kết hợp giữa React - thư viện xây dựng giao diện người dùng phổ biến nhất hiện nay, và TypeScript - ngôn ngữ siêu tập hợp của JavaScript mang lại khả năng kiểm soát kiểu dữ liệu mạnh mẽ, là một cặp đôi hoàn hảo cho các ứng dụng quy mô lớn và phức tạp. Việc sử dụng TypeScript trong React giúp bạn bắt lỗi sớm, dễ dàng tái cấu trúc code, và làm cho codebase của bạn trở nên dễ đọc, dễ hiểu hơn rất nhiều.
Hãy cùng bắt đầu luyện tập nào!
1. Xây dựng Component với Props và Type An Toàn
Bài tập cơ bản đầu tiên là tạo một component đơn giản nhận dữ liệu qua props và hiển thị chúng. TypeScript sẽ giúp chúng ta định nghĩa rõ ràng kiểu dữ liệu mà component này mong đợi.
// src/components/WelcomeMessage.tsx
import React from 'react';
// Định nghĩa kiểu dữ liệu cho props
interface WelcomeMessageProps {
name: string;
age?: number; // Dấu '?' báo hiệu prop này là tùy chọn (optional)
}
const WelcomeMessage: React.FC<WelcomeMessageProps> = ({ name, age }) => {
return (
<div>
<h2>Xin chào, **{name}**!</h2>
{age && <p>Bạn năm nay **{age}** tuổi.</p>}
{!age && <p>*Chúng tôi chưa biết tuổi của bạn.*</p>}
</div>
);
};
export default WelcomeMessage;
Giải thích:
- Chúng ta định nghĩa một
interface
tên làWelcomeMessageProps
để mô tả cấu trúc và kiểu dữ liệu của các props mà componentWelcomeMessage
sẽ nhận. name
được khai báo làstring
(chuỗi).age
được khai báo lànumber
(số) và có thêm dấu?
để chỉ ra rằng prop này có thể có hoặc không.React.FC<WelcomeMessageProps>
là cách khai báo component hàm trong React với TypeScript, nơi<WelcomeMessageProps>
chỉ định kiểu dữ liệu của props. Điều này giúp TypeScript kiểm tra xem bạn có truyền đúng các props cần thiết với đúng kiểu dữ liệu khi sử dụng component này hay không.- Bên trong component, chúng ta sử dụng cú pháp destructuring để lấy
name
vàage
từ props. Logic hiển thị dựa trên việc propage
có tồn tại hay không.
Cách sử dụng:
import WelcomeMessage from './components/WelcomeMessage';
function App() {
return (
<div>
<WelcomeMessage name="FullhouseDev" age={30} /> {/* Đúng kiểu dữ liệu */}
<WelcomeMessage name="Độc giả thân mến" /> {/* age là optional nên không truyền cũng được */}
{/* <WelcomeMessage ten="FullhouseDev" tuoi={30} /> // Lỗi TypeScript: prop ten không tồn tại, prop name bị thiếu */}
{/* <WelcomeMessage name="FullhouseDev" age="ba muoi" /> // Lỗi TypeScript: age mong đợi number nhưng nhận string */}
</div>
);
}
- Khi bạn truyền đúng props như đã định nghĩa trong
interface
, code sẽ chạy bình thường. - Nếu bạn cố gắng truyền prop sai tên hoặc sai kiểu dữ liệu (như các dòng code bị comment ở trên), trình biên dịch TypeScript sẽ báo lỗi ngay lập tức, trước cả khi bạn chạy ứng dụng. Đây chính là sức mạnh của TypeScript!
2. Làm việc với State trong Component
Hầu hết các component tương tác đều cần quản lý state (trạng thái) nội bộ. TypeScript giúp chúng ta khai báo và làm việc với state một cách an toàn.
// src/components/Counter.tsx
import React, { useState } from 'react';
const Counter: React.FC = () => {
// Khai báo state với kiểu dữ liệu là number
const [count, setCount] = useState<number>(0);
// Hoặc TypeScript có thể tự suy luận kiểu nếu bạn cung cấp giá trị khởi tạo (0 là number)
// const [count, setCount] = useState(0); // TypeScript hiểu count là number
const increment = () => {
setCount(prevCount => prevCount + 1);
};
const decrement = () => {
setCount(prevCount => prevCount - 1);
};
return (
<div>
<p>Giá trị hiện tại: **{count}**</p>
<button onClick={increment}>Tăng</button>
<button onClick={decrement}>Giảm</button>
{/* <button onClick={() => setCount("hello")}>Set sai kiểu dữ liệu</button> // Lỗi TypeScript: setCount mong đợi number */}
</div>
);
};
export default Counter;
Giải thích:
- Chúng ta sử dụng hook
useState
để quản lý statecount
. - TypeScript rất giỏi trong việc suy luận kiểu dữ liệu. Khi chúng ta khởi tạo
useState
với giá trị0
, TypeScript tự động hiểu rằngcount
sẽ có kiểu lànumber
, và hàmsetCount
chỉ chấp nhận giá trị kiểunumber
hoặc một hàm trả vềnumber
. - Chúng ta cũng có thể tường minh khai báo kiểu dữ liệu cho state bằng cách viết
useState<number>(0)
. Điều này hữu ích khi giá trị khởi tạo lànull
hoặcundefined
, và bạn muốn state sau này có một kiểu cụ thể (ví dụ:useState<string | null>(null)
). - Khi bạn gọi
setCount
, TypeScript sẽ kiểm tra xem giá trị bạn truyền vào có phù hợp với kiểu dữ liệu của state hay không. Nếu bạn cố gắng truyền một giá trị không phải lànumber
(như dòng code bị comment), bạn sẽ nhận được lỗi.
3. Xử lý Sự kiện (Events) An Toàn
Làm việc với các sự kiện DOM như click
, change
, submit
là phần không thể thiếu của UI tương tác. TypeScript cung cấp các kiểu dữ liệu được định nghĩa sẵn cho các loại sự kiện, giúp chúng ta xử lý chúng một cách chính xác.
// src/components/InputBox.tsx
import React, { useState, ChangeEvent, MouseEvent } from 'react';
const InputBox: React.FC = () => {
const [text, setText] = useState('');
// Định nghĩa kiểu cho hàm xử lý sự kiện ChangeEvent trên input element
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
// event.target.value sẽ có kiểu string, nhờ TypeScript
setText(event.target.value);
};
// Định nghĩa kiểu cho hàm xử lý sự kiện MouseEvent trên button element
const handleButtonClick = (event: MouseEvent<HTMLButtonElement>) => {
// event.currentTarget có kiểu HTMLButtonElement
console.log('Button clicked!', event.currentTarget);
// event.preventDefault() // Có thể gọi các phương thức chuẩn của Event
};
return (
<div>
<input
type="text"
value={text}
onChange={handleInputChange} // TypeScript kiểm tra hàm này nhận đúng kiểu Event
placeholder="Nhập gì đó..."
/>
<p>Bạn đang nhập: **{text}**</p>
<button onClick={handleButtonClick}>Log Button</button> {/* TypeScript kiểm tra hàm này nhận đúng kiểu Event */}
</div>
);
};
export default InputBox;
Giải thích:
- React cung cấp các kiểu sự kiện tổng hợp như
ChangeEvent
(cho các sự kiện thay đổi giá trị input),MouseEvent
(cho các sự kiện chuột),FormEvent
, v.v. - Chúng ta import
ChangeEvent
vàMouseEvent
từreact
. - Khi định nghĩa hàm xử lý sự kiện
handleInputChange
, chúng ta chỉ định kiểu của tham sốevent
làChangeEvent<HTMLInputElement>
. Điều này cho TypeScript biết rằng đây là một sự kiện thay đổi giá trị xảy ra trên một phần tử<input>
. Nhờ đó, khi truy cậpevent.target
, TypeScript biết rằngtarget
là mộtHTMLInputElement
và có thuộc tínhvalue
kiểustring
. - Tương tự,
handleButtonClick
nhận tham sốevent
kiểuMouseEvent<HTMLButtonElement>
, đảm bảo rằng bạn chỉ có thể sử dụng các thuộc tính và phương thức phù hợp với sự kiện chuột trên một nút bấm. - Việc gán các hàm này vào các prop sự kiện như
onChange
vàonClick
trên các phần tử JSX cũng được TypeScript kiểm tra. Nếu bạn gán một hàm xử lý sự kiện sai kiểu, TypeScript sẽ báo lỗi.
4. Tạo và Sử dụng Custom Hooks An Toàn
Custom Hooks cho phép chúng ta tái sử dụng logic có state giữa các component. TypeScript là công cụ tuyệt vời để đảm bảo các custom hook của bạn có chữ ký (signature) rõ ràng và trả về kiểu dữ liệu mong đợi.
// src/hooks/useToggle.ts
import { useState } from 'react';
// Định nghĩa kiểu trả về của custom hook
type UseToggleReturnType = [boolean, () => void];
const useToggle = (initialValue: boolean = false): UseToggleReturnType => {
const [isOn, setIsOn] = useState<boolean>(initialValue);
const toggle = () => {
setIsOn(prevIsOn => !prevIsOn);
};
// Trả về một mảng (tuple) với giá trị state hiện tại và hàm toggle
return [isOn, toggle];
};
export default useToggle;
Giải thích:
- Chúng ta tạo một custom hook đơn giản tên là
useToggle
để quản lý state boolean và cung cấp một hàm để đảo ngược giá trị đó. - Chúng ta định nghĩa một
type
(hoặcinterface
) tên làUseToggleReturnType
để mô tả chính xác kiểu dữ liệu trả về của hook: một mảng (trong TypeScript gọi là tuple) chứa mộtboolean
và một hàm không nhận tham số nào (()
) và không trả về gì (void
). - Hook
useToggle
được chú thích kiểu trả về làUseToggleReturnType
. Điều này đảm bảo rằng hook này luôn luôn trả về một mảng với đúng cấu trúc và kiểu dữ liệu như đã hứa hẹn. - Bên trong hook, chúng ta sử dụng
useState<boolean>
để quản lý state boolean.
Cách sử dụng custom hook:
// src/components/ToggleComponent.tsx
import React from 'react';
import useToggle from '../hooks/useToggle'; // Import custom hook
const ToggleComponent: React.FC = () => {
// Sử dụng custom hook
// TypeScript biết rằng isOn là boolean và handleToggle là hàm void
const [isOn, handleToggle] = useToggle(false);
return (
<div>
<p>Trạng thái: **{isOn ? 'Bật' : 'Tắt'}**</p>
<button onClick={handleToggle}>
Chuyển đổi trạng thái
</button>
</div>
);
};
export default ToggleComponent;
Giải thích:
- Khi sử dụng
const [isOn, handleToggle] = useToggle(false);
, TypeScript biết rằnguseToggle
trả về một tuple theo định nghĩaUseToggleReturnType
. - Do đó, TypeScript suy luận rằng
isOn
sẽ có kiểuboolean
vàhandleToggle
sẽ là một hàm không nhận tham số và không trả về gì. - Điều này mang lại sự minh bạch và an toàn khi sử dụng các custom hook, bạn biết chính xác những gì bạn sẽ nhận được từ hook đó.
5. Tích hợp Dữ liệu với Kiểu Dữ liệu Định Nghĩa
Trong các ứng dụng thực tế, bạn thường cần lấy dữ liệu từ API. TypeScript giúp bạn định nghĩa cấu trúc dữ liệu mong đợi và làm việc với nó một cách an toàn.
// src/components/UserData.tsx
import React, { useState, useEffect } from 'react';
// Định nghĩa cấu trúc dữ liệu mong đợi từ API
interface User {
id: number;
name: string;
username: string;
email: string;
// ... có thể có nhiều thuộc tính khác
}
const UserData: React.FC = () => {
// Khai báo state để lưu trữ dữ liệu người dùng
// Ban đầu là null (chưa tải), sau đó sẽ là kiểu User hoặc null
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchUserData = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: User = await response.json(); // Ép kiểu dữ liệu nhận được thành kiểu User
setUser(data);
} catch (err) {
// err có thể là Error, cần kiểm tra hoặc ép kiểu nếu muốn truy cập thuộc tính cụ thể
setError(err instanceof Error ? err.message : 'An unknown error occurred');
} finally {
setLoading(false);
}
};
fetchUserData();
}, []); // Mảng dependencies rỗng, chỉ chạy một lần khi component mount
if (loading) {
return <p>*Đang tải dữ liệu người dùng...*</p>;
}
if (error) {
return <p style={{ color: 'red' }}>*Lỗi khi tải dữ liệu: {error}*</p>;
}
// Hiển thị dữ liệu nếu có và không có lỗi
return (
<div>
<h3>Thông tin Người dùng:</h3>
{user && ( // Chỉ hiển thị nếu user không phải là null
<div>
<p>ID: **{user.id}**</p>
<p>Tên: **{user.name}**</p>
<p>Username: **{user.username}**</p>
<p>Email: **{user.email}**</p>
</div>
)}
</div>
);
};
export default UserData;
Giải thích:
- Chúng ta định nghĩa một
interface User
để mô tả cấu trúc dữ liệu mà chúng ta mong đợi nhận được từ API (trong ví dụ này là một API công cộng trả về dữ liệu người dùng giả). - State
user
được khai báo với kiểuUser | null
. Ban đầu lànull
(trước khi tải), sau đó có thể là một đối tượngUser
nếu tải thành công, hoặc vẫn lànull
nếu có lỗi. - State
loading
làboolean
vàerror
làstring | null
để quản lý trạng thái tải và thông báo lỗi. - Trong
useEffect
, chúng ta thực hiện việc gọi API. - Quan trọng là dòng
const data: User = await response.json();
. Chúng ta ép kiểu dữ liệu JSON nhận được thành kiểuUser
. Điều này giúp TypeScript biết rằng biếndata
bây giờ có cấu trúc như đã định nghĩa tronginterface User
. - Sau đó, khi gọi
setUser(data)
, TypeScript kiểm tra xemdata
có phù hợp với kiểuUser | null
của stateuser
hay không (và nó phù hợp vìdata
làUser
). - Khi hiển thị dữ liệu, việc truy cập
user.id
,user.name
, v.v., được TypeScript kiểm tra. Bạn chỉ có thể truy cập các thuộc tính đã được định nghĩa tronginterface User
. Nếu API trả về dữ liệu có cấu trúc khác và bạn cố gắng truy cập một thuộc tính không tồn tại tronginterface User
, TypeScript sẽ báo lỗi ngay lập tức, giúp bạn nhận ra sự sai lệch giữa cấu trúc dữ liệu mong đợi và thực tế.
Comments