Bài 22.1: CSS Modules với TypeScript

Chào mừng trở lại với chuỗi bài viết về lập trình Web Front-end! Hôm nay, chúng ta sẽ đi sâu vào một kỹ thuật cực kỳ hữu ích giúp giải quyết một trong những vấn đề nhức nhối nhất khi làm việc với CSS trong các dự án lớn: xung đột tên lớp (naming conflicts). Chúng ta sẽ tìm hiểu về CSS Modules và cách tích hợp chúng một cách mượt mà với TypeScript để có một trải nghiệm phát triển mạnh mẽ và an toàn.

Tại Sao Lại Cần CSS Modules?

Hãy tưởng tượng bạn đang xây dựng một ứng dụng web phức tạp với hàng trăm hoặc thậm chí hàng nghìn component. Mỗi component đều có CSS riêng của nó. Khi bạn sử dụng các lớp CSS truyền thống (global classes), rất dễ xảy ra tình trạng:

  1. Xung đột tên: Hai component khác nhau cùng sử dụng chung một tên lớp CSS, ví dụ button hoặc card. Style của component này có thể ghi đè lên style của component khác một cách không mong muốn.
  2. Khó bảo trì: Khi thay đổi một lớp CSS, bạn không chắc chắn liệu nó có ảnh hưởng đến những phần nào khác của ứng dụng. Việc refactor (tái cấu trúc) CSS trở nên đầy rủi ro.
  3. Phụ thuộc ngầm: CSS của một component lại phụ thuộc vào sự vắng mặt của các tên lớp cụ thể ở những nơi khác trong ứng dụng, tạo ra sự phụ thuộc ngầm định rất khó theo dõi.

CSS Modules ra đời để giải quyết triệt để vấn đề này bằng cách cung cấp CSS được phạm vi hóa (scoped CSS).

CSS Modules Hoạt Động Như Thế Nào?

Ý tưởng cốt lõi của CSS Modules rất đơn giản: mọi tên lớp và ID trong file CSS đều được mặc định là local (cục bộ) và chỉ có sẵn trong component sử dụng file đó.

Khi bạn sử dụng một file CSS Modules (thường có đuôi .module.css), hệ thống build của bạn (như Webpack, Parcel, Create React App, Next.js...) sẽ xử lý nó. Thay vì giữ nguyên tên lớp bạn viết (ví dụ: myButton), nó sẽ tạo ra một tên lớp duy nhất toàn cục (ví dụ: _myButton_abc123) bằng cách kết hợp tên lớp gốc, tên file và một hash duy nhất.

Sau đó, bạn nhập (import) file CSS này vào component JavaScript/TypeScript của mình như thể nó là một đối tượng:

import styles from './Button.module.css';

// Sử dụng trong component
<button className={styles.myButton}>Click Me</button>

Đối tượng styles chứa ánh xạ từ tên lớp gốc (myButton) sang tên lớp đã được tạo ra (_myButton_abc123). Khi component render, nó sẽ sử dụng tên lớp duy nhất đó. Bằng cách này, myButton trong Button.module.css sẽ không bao giờ xung đột với myButton trong AnotherComponent.module.css. Mỗi component có "không gian tên" CSS riêng của nó!

Hãy xem một ví dụ đơn giản:

Button.module.css

.myButton {
  background-color: blue;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

.myButton:hover {
  background-color: darkblue;
}

Button.js (hoạt động với JavaScript thuần hoặc React/JS)

import React from 'react';
import styles from './Button.module.css'; // Nhập file CSS Module

function Button() {
  // Sử dụng tên lớp thông qua đối tượng styles
  return (
    <button className={styles.myButton}>
      My Button
    </button>
  );
}

export default Button;

Khi hệ thống build xử lý, styles.myButton có thể trở thành chuỗi "Button_myButton__abc123". Element <button> sẽ được render với class="Button_myButton__abc123". Style trong Button.module.css chỉ áp dụng cho các element có class đã được đổi tên này. Đây là cốt lõi của CSS Modules.

CSS Modules và Thách Thức với TypeScript

CSS Modules hoạt động tuyệt vời với JavaScript. Tuy nhiên, khi chuyển sang sử dụng TypeScript, chúng ta gặp phải một vấn đề nhỏ. Theo mặc định, TypeScript không hiểu .module.css là gì và không biết rằng việc nhập nó sẽ trả về một đối tượng với các khóa là tên lớp CSS.

Khi bạn viết dòng code sau trong file .ts hoặc .tsx:

import styles from './Button.module.css';

TypeScript compiler sẽ báo lỗi kiểu như: "Cannot find module './Button.module.css' or its corresponding type declarations." (Không tìm thấy module './Button.module.css' hoặc khai báo kiểu tương ứng của nó). Nó không biết styles là gì!

Điều này không chỉ gây khó chịu với lỗi đỏ lòm trong editor mà còn loại bỏ lợi ích của TypeScript là kiểm tra kiểu. Bạn không nhận được gợi ý autocomplete cho tên lớp CSS và TypeScript không thể báo lỗi nếu bạn gõ sai tên lớp (ví dụ: styles.myButon thay vì styles.myButton).

Giải Quyết Thách Thức TypeScript: File Khai Báo Kiểu (.d.ts)

Để "dạy" TypeScript hiểu về CSS Modules, chúng ta cần cung cấp cho nó một file khai báo kiểu (type declaration file). File này sẽ mô tả hình dạng (shape) của đối tượng mà việc nhập file .module.css trả về.

Theo quy ước, file khai báo kiểu cho Button.module.css sẽ có tên là Button.module.css.d.ts. Nội dung của file này sẽ rất đơn giản:

Button.module.css.d.ts

declare const styles: {
  readonly [key: string]: string;
};
export default styles;

Hãy phân tích dòng code này:

  • declare const styles:: Chúng ta khai báo rằng có một hằng số tên là styles.
  • { readonly [key: string]: string; }: Đây là phần quan trọng nhất. Chúng ta mô tả kiểu của hằng số styles. Nó là một đối tượng ({...}). [key: string]: string; có nghĩa là đối tượng này có thể có bất kỳ khóa nào (tên thuộc tính) là một chuỗi (string), và giá trị tương ứng của mỗi khóa đó cũng là một chuỗi (string). Khóa ở đây chính là tên lớp CSS gốc của bạn (ví dụ: myButton), và giá trị là tên lớp đã được tạo ra (ví dụ: "Button_myButton__abc123"). readonly là một bổ sung tốt để chỉ ra rằng bạn không nên cố gắng thay đổi đối tượng này.

Sau khi thêm file .d.ts này vào dự án (đặt cùng thư mục với file .module.css tương ứng), TypeScript sẽ hiểu rằng khi bạn nhập Button.module.css, bạn sẽ nhận được một đối tượng mà bạn có thể truy cập các thuộc tính chuỗi của nó. Lỗi TypeScript sẽ biến mất!

Tích Hợp Hoàn Chỉnh: CSS Modules với React và TypeScript

Bây giờ, hãy xem một ví dụ đầy đủ về cách sử dụng CSS Modules trong một component React được viết bằng TypeScript (JSX/TSX).

Button.module.css (Giữ nguyên như cũ)

.primaryButton {
  background-color: blue;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

.primaryButton:hover {
  background-color: darkblue;
}

.secondaryButton {
  background-color: gray;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

Button.module.css.d.ts (Thêm khai báo kiểu)

declare const styles: {
  readonly [key: string]: string;
};
export default styles;

Button.tsx (Component React với TypeScript)

import React from 'react';
import styles from './Button.module.css'; // Import CSS Module

interface ButtonProps {
  variant?: 'primary' | 'secondary'; // Định nghĩa prop để chọn style
  onClick?: () => void;
  children: React.ReactNode;
}

function Button({ variant = 'primary', onClick, children }: ButtonProps) {
  // Chọn tên lớp dựa trên prop 'variant'
  const buttonClassName = variant === 'primary' ? styles.primaryButton : styles.secondaryButton;

  // Sử dụng tên lớp đã được import
  return (
    <button className={buttonClassName} onClick={onClick}>
      {children}
    </button>
  );
}

export default Button;

Trong ví dụ trên:

  1. Chúng ta có hai tên lớp trong Button.module.css: primaryButtonsecondaryButton.
  2. File Button.module.css.d.ts nói với TypeScript rằng import styles from './Button.module.css' sẽ cung cấp một đối tượng mà bạn có thể truy cập các thuộc tính chuỗi.
  3. Trong Button.tsx, chúng ta nhập styles. TypeScript giờ đây không báo lỗi.
  4. Chúng ta sử dụng prop variant để lựa chọn tên lớp phù hợp từ đối tượng styles. styles.primaryButtonstyles.secondaryButton sẽ cung cấp các chuỗi tên lớp đã được tạo ra duy nhất.
  5. Component <button> được render với tên lớp chính xác.

Lợi ích ngay lập tức:

  • Không xung đột: Các lớp primaryButtonsecondaryButton chỉ có phạm vi trong Button.module.css.
  • Bảo trì dễ dàng: Bạn có thể thay đổi style trong Button.module.css mà không sợ ảnh hưởng đến các component khác.
  • An toàn với TypeScript: Mặc dù file .d.ts cơ bản không kiểm tra xem primaryButton có tồn tại trong CSS hay không (chỉ báo rằng styles có thể có bất kỳ khóa chuỗi nào), nó loại bỏ lỗi import ban đầu. Các công cụ build hiện đại thường có khả năng tự động tạo file .d.ts này với các tên lớp cụ thể được liệt kê, mang lại kiểu an toàn cao hơn.

Thêm Ví Dụ: Áp Dụng Nhiều Lớp

Bạn thường cần áp dụng nhiều lớp CSS cho một element, ví dụ: một lớp cơ bản và một lớp modifier. Với CSS Modules và TypeScript, bạn làm điều này bằng cách kết hợp các chuỗi tên lớp từ đối tượng styles.

Card.module.css

.card {
  border: 1px solid #ccc;
  padding: 15px;
  border-radius: 8px;
  box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
}

.highlighted {
  border-color: blue;
  box-shadow: 2px 2px 5px rgba(0, 0, 255, 0.2);
}

Card.module.css.d.ts

declare const styles: {
  readonly [key: string]: string;
};
export default styles;

Card.tsx

import React from 'react';
import styles from './Card.module.css';

interface CardProps {
  isHighlighted?: boolean;
  children: React.ReactNode;
}

function Card({ isHighlighted = false, children }: CardProps) {
  // Kết hợp các tên lớp
  // Nếu isHighlighted là true, kết hợp style.card và style.highlighted
  // Nếu không, chỉ sử dụng style.card
  const cardClassName = isHighlighted
    ? `${styles.card} ${styles.highlighted}`
    : styles.card;

  return (
    <div className={cardClassName}>
      {children}
    </div>
  );
}

export default Card;

Trong ví dụ này, chúng ta sử dụng template literal (dấu backtick `) để kết hợp các tên lớp đã được generate từ styles.cardstyles.highlighted. Đây là cách phổ biến để áp dụng nhiều lớp với CSS Modules.

Lưu Ý về Tên Lớp Khác (IDs, Animations...)

CSS Modules theo mặc định chỉ phạm vi hóa các tên lớp (.)tên ID (#). Các thứ khác như @keyframes, tên font @font-face, v.v., vẫn là global. Tuy nhiên, hầu hết các trường hợp sử dụng phổ biến nhất đều liên quan đến tên lớp.

CSS Modules và Hệ Thống Build

Điều quan trọng cần biết là việc xử lý CSS Modules và tự động tạo file .d.ts thường được thực hiện bởi các hệ thống build hoặc framework bạn đang sử dụng.

  • Create React App: Hỗ trợ sẵn CSS Modules (sử dụng quy ước đặt tên .module.css) và có thể tự động xử lý phần TypeScript.
  • Next.js: Hỗ trợ sẵn CSS Modules và tích hợp tốt với TypeScript.
  • Webpack/Parcel: Cần cài đặt và cấu hình các loader/plugin tương ứng (ví dụ: css-loader với tùy chọn modules: true cho Webpack, và các plugin bổ sung để tạo file .d.ts).

Việc tự tạo file .d.ts thủ công như chúng ta đã làm là cách tốt để hiểu cách TypeScript nhận biết CSS Modules. Tuy nhiên, trong thực tế, bạn thường sẽ dựa vào công cụ build để làm việc này tự độngchính xác hơn (bằng cách liệt kê cụ thể các tên lớp có trong file CSS).

Comments

There are no comments at the moment.