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 ReactTypeScript: 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ểubả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 namestringisActiveboolean. 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:

  1. State có thể nhận nhiều kiểu (ví dụ: một kiểu dữ liệu hoặc null).
  2. State khởi tạo là null hoặc undefined.
  3. 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  null
      )}
    </div>
  );
}

Trong ví dụ này:

  • useState<User | null>(null): Chúng ta nói rõ với TypeScript rằng state user có thể là một đối tượng User hoặcnull. Điều này là cần thiết vì giá trị khởi tạo là null. Nếu bạn chỉ dùng useState(null), TypeScript sẽ suy luận kiểu là any hoặc null, làm mất đi tính an toàn kiểu khi bạn gán một đối tượng User 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ủa user (ví dụ: user.name), TypeScript sẽ yêu cầu bạn xử lý trường hợp usernull (ví dụ: dùng user ? user.name : 'N/A' hoặc optional chaining user?.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ượng Todo.
  • useState<Todo[]>([])): Chúng ta nói rõ rằng state todos 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 interface Todo. 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ận todosany[].
  • 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ểu Todo[] 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 state settings. Lưu ý cách sử dụng Union Type ('light' | 'dark') cho thuộc tính theme, giới hạn giá trị chỉ trong hai chuỗi này.
  • useState<Settings>({ ... }): Chúng ta nói rõ rằng state settings phải tuân theo cấu trúc của interface Settings.
  • 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ểu Settings 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

There are no comments at the moment.