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á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ệ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à component WelcomeMessage 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 nameage từ props. Logic hiển thị dựa trên việc prop age 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ý state count.
  • 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ằng count sẽ có kiểu là number, và hàm setCount chỉ chấp nhận giá trị kiểu number 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ặc undefined, 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 ChangeEventMouseEvent 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ố eventChangeEvent<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ập event.target, TypeScript biết rằng target là một HTMLInputElement và có thuộc tính value kiểu string.
  • Tương tự, handleButtonClick nhận tham số event kiểu MouseEvent<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ư onChangeonClick 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ặc interface) 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ột boolean 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ằng useToggle trả về một tuple theo định nghĩa UseToggleReturnType.
  • Do đó, TypeScript suy luận rằng isOn sẽ có kiểu booleanhandleToggle 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ạchan 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ểu User | null. Ban đầu là null (trước khi tải), sau đó có thể là một đối tượng User nếu tải thành công, hoặc vẫn là null nếu có lỗi.
  • State loadingbooleanerrorstring | 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ểu User. Điều này giúp TypeScript biết rằng biến data bây giờ có cấu trúc như đã định nghĩa trong interface User.
  • Sau đó, khi gọi setUser(data), TypeScript kiểm tra xem data có phù hợp với kiểu User | null của state user hay không (và nó phù hợp vì dataUser).
  • 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 trong interface 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 trong interface 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

There are no comments at the moment.