Bài 22.1: CSS Modules với TypeScript

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:
- 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ặccard
. 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. - 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.
- 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:
- Chúng ta có hai tên lớp trong
Button.module.css
:primaryButton
vàsecondaryButton
. - File
Button.module.css.d.ts
nói với TypeScript rằngimport 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. - Trong
Button.tsx
, chúng ta nhậpstyles
. TypeScript giờ đây không báo lỗi. - Chúng ta sử dụng prop
variant
để lựa chọn tên lớp phù hợp từ đối tượngstyles
.styles.primaryButton
vàstyles.secondaryButton
sẽ cung cấp các chuỗi tên lớp đã được tạo ra duy nhất. - 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
primaryButton
vàsecondaryButton
chỉ có phạm vi trongButton.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 xemprimaryButton
có tồn tại trong CSS hay không (chỉ báo rằngstyles
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.card
và styles.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 (.
) và 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ọnmodules: 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ự động và chí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