Bài 30.2: Styled JSX với TypeScript

Bài 30.2: Styled JSX với TypeScript
Chào mừng bạn đến với Bài 30.2 trong chuỗi series Lập trình Web Front-end của chúng ta! Hôm nay, chúng ta sẽ khám phá một chủ đề quan trọng và thực tế trong thế giới React/Next.js hiện đại: kết hợp Styled JSX với TypeScript.
Trong những bài học trước, chúng ta đã làm quen với React/Next.js và sức mạnh của TypeScript trong việc quản lý state, props và logic component. Nhưng còn việc tạo kiểu (styling) thì sao? Làm thế nào để chúng ta viết CSS một cách linh hoạt, có thể tái sử dụng, và quan trọng nhất là được bảo vệ bởi sự an toàn kiểu dữ liệu của TypeScript?
Đó chính là lúc Styled JSX (thường được hiểu trong ngữ cảnh của các thư viện CSS-in-JS như Emotion hoặc Styled Components) kết hợp với TypeScript phát huy tối đa sức mạnh.
Styled JSX Là Gì? (Trong Ngữ Cảnh Này)
Khái niệm "Styled JSX" ban đầu có thể hơi mơ hồ, nhưng trong ngữ cảnh hiện đại của React/Next.js, nó thường đề cập đến khả năng viết CSS trực tiếp bên trong các file JavaScript/TypeScript của component. Đây là cốt lõi của phương pháp CSS-in-JS.
Thay vì tạo các file .css
hoặc .module.css
riêng biệt, bạn định nghĩa kiểu dáng cùng với component sử dụng chúng. Điều này mang lại một số lợi ích rõ rệt:
- Colocation: Logic component và styling của nó nằm cùng một nơi, giúp bạn dễ dàng tìm kiếm và sửa đổi.
- Dynamic Styling: Dễ dàng thay đổi kiểu dáng dựa trên props hoặc state của component.
- Scoped Styles: Kiểu dáng chỉ áp dụng cho component đó, tránh xung đột CSS toàn cục (như khi dùng BEM hoặc các quy ước đặt tên khác).
- Reusability: Dễ dàng tạo ra các styled component có thể tái sử dụng.
Các thư viện phổ biến nhất hỗ trợ CSS-in-JS bao gồm Emotion và Styled Components. Cả hai đều cung cấp cú pháp tương tự dựa trên tagged template literals trong JavaScript để định nghĩa CSS.
Tại Sao Lại Là TypeScript?
Bạn đã quen thuộc với lợi ích của TypeScript: an toàn kiểu dữ liệu, autocompletion mạnh mẽ, và bắt lỗi sớm ngay trong quá trình phát triển. Khi kết hợp với CSS-in-JS, TypeScript nâng cao trải nghiệm và độ tin cậy lên một tầm cao mới.
TypeScript giúp chúng ta:
- Định nghĩa kiểu dữ liệu cho các props ảnh hưởng đến styling.
- Kiểm tra kiểu khi truyền props xuống styled component, tránh lỗi runtime do truyền sai kiểu hoặc thiếu prop bắt buộc.
- Autocompletion cho tên props và đôi khi cả tên thuộc tính CSS (tùy thuộc vào thư viện và setup).
- Dễ dàng refactor code mà không sợ làm hỏng styling liên quan.
Hãy cùng đi sâu vào cách sử dụng và xem các ví dụ cụ thể với Emotion (một thư viện phổ biến hỗ trợ cú pháp rất gần với "Styled JSX" ban đầu và có tích hợp TypeScript tốt).
Sử Dụng Styled JSX (với Emotion) và TypeScript
Để bắt đầu, bạn cần cài đặt Emotion và các dependency liên quan cho TypeScript:
npm install @emotion/react @emotion/styled
npm install --save-dev @emotion/babel-plugin # Cần cho Next.js hoặc Babel setup
npm install --save-dev @types/react @types/node # Đảm bảo đã có
npm install --save-dev @emotion/babel-plugin @emotion/core # Cần cho cấu hình cụ thể, Emotion v11+ dùng @emotion/react
Trong Next.js, bạn có thể cần cấu hình babel.config.js
hoặc next.config.js
để bật Emotion's CSS prop hoặc Server-Side Rendering (SSR) nếu cần. Với các phiên bản Next.js mới, việc tích hợp Emotion trở nên dễ dàng hơn.
1. Tạo Styled Component Cơ Bản
Cách phổ biến nhất là sử dụng hàm styled
từ @emotion/styled
.
import styled from '@emotion/styled';
// Tạo một styled component cho thẻ div
const Container = styled.div`
padding: 20px;
background-color: #f0f0f0;
border-radius: 8px;
margin-bottom: 15px;
`;
// Sử dụng nó trong một component React/Next.js
function BasicExample() {
return (
<Container>
<p>Đây là nội dung bên trong styled container.</p>
</Container>
);
}
export default BasicExample;
Giải thích:
import styled from '@emotion/styled';
: Chúng ta import hàmstyled
.styled.div
: Gọi hàmstyled
và chỉ định rằng chúng ta muốn tạo một styled component dựa trên thẻ HTMLdiv
.`...`
: Sử dụng tagged template literal (chuỗi được bao bởi dấu backticks`
) để viết CSS. Mọi thuộc tính CSS hợp lệ đều có thể đặt ở đây.Container
: Biến này bây giờ là một React component mà bạn có thể sử dụng trong JSX như bất kỳ component nào khác.
Với TypeScript, việc này hoạt động ngay lập tức. Container
component sẽ tự động có kiểu dữ liệu chính xác cho các thuộc tính HTML div
tiêu chuẩn (className
, onClick
, v.v.).
2. Styling Dựa Trên Props (Sức Mạnh Của TypeScript)
Đây là lúc TypeScript tỏa sáng. Chúng ta thường muốn kiểu dáng thay đổi dựa trên các props được truyền vào component (ví dụ: màu sắc khác nhau cho nút chính/phụ, kích thước khác nhau cho avatar).
import styled from '@emotion/styled';
// 1. Định nghĩa kiểu dữ liệu cho props
interface ButtonProps {
primary?: boolean; // Prop tùy chọn, kiểu boolean
size?: 'small' | 'medium' | 'large'; // Prop tùy chọn, các giá trị cố định
}
// 2. Tạo styled component và truyền kiểu props
const DynamicButton = styled.button<ButtonProps>`
padding: ${props => { // Truy cập props bên trong CSS
switch (props.size) {
case 'small': return '5px 10px';
case 'large': return '15px 30px';
default: return '10px 20px'; // medium hoặc không truyền
}
}};
border: none;
border-radius: 5px;
cursor: pointer;
font-size: ${props => props.size === 'large' ? '1.2em' : '1em'};
background-color: ${props => props.primary ? '#007bff' : '#6c757d'};
color: ${props => props.primary ? 'white' : 'black'};
&:hover {
opacity: 0.9;
}
`;
// 3. Sử dụng component với props được type-checked
function DynamicButtonExample() {
return (
<div>
<DynamicButton primary size="medium">Nút Chính</DynamicButton> {/* OK */}
<DynamicButton size="large">Nút Phụ Lớn</DynamicButton> {/* OK */}
<DynamicButton primary size="small">Nút Chính Nhỏ</DynamicButton> {/* OK */}
{/* <DynamicButton primary="yes" size="xlarge">Lỗi Kiểu!</DynamicButton> */} {/* 🚨 TypeScript sẽ báo lỗi tại đây! */}
</div>
);
}
export default DynamicButtonExample;
Giải thích:
interface ButtonProps { ... }
: Chúng ta định nghĩa cấu trúc của các props màDynamicButton
sẽ nhận và ảnh hưởng đến styling.primary
là boolean,size
chỉ được là 'small', 'medium', hoặc 'large'.styled.button<ButtonProps>
: Chúng ta truyền interfaceButtonProps
vào hàmstyled.button
bằng cú pháp Generic (<...>
). Điều này cho TypeScript biết rằng componentDynamicButton
sẽ nhận các props theo kiểuButtonProps
.${props => ...}
: Bên trong chuỗi CSS backticks, chúng ta có thể nhúng các biểu thức JavaScript bằng${}
. Biểu thức này nhận một đối sốprops
, đó chính là object chứa tất cả các props được truyền vàoDynamicButton
. TypeScript biết kiểu củaprops
làButtonProps
.props.primary ? '#007bff' : '#6c757d'
: Sử dụng giá trị của propprimary
để quyết định màu nền.switch (props.size)
: Sử dụng giá trị của propsize
để quyết định padding.- Type Safety: Khi bạn sử dụng
<DynamicButton primary="yes">
, TypeScript sẽ phát hiện ra rằng bạn đang truyền một string ("yes"
) cho propprimary
đáng lẽ phải là boolean, và báo lỗi ngay lập tức trong editor hoặc lúc build. Tương tự,size="xlarge"
sẽ bị bắt lỗi vì"xlarge"
không phải là một trong các giá trị hợp lệ ('small' | 'medium' | 'large') đã định nghĩa.
Đây là lợi ích khổng lồ của việc kết hợp TypeScript với CSS-in-JS: bạn có được sự linh hoạt của styling động mà không mất đi sự an toàn và khả năng bảo trì.
3. Sử Dụng CSS Prop (Emotion)
Emotion cũng hỗ trợ một cách khác để áp dụng CSS trực tiếp trong JSX bằng cách sử dụng css
prop. Cách này thường dùng cho các styling ad-hoc, không cần tạo component riêng biệt hoặc khi bạn muốn style trực tiếp một element trong JSX.
Để sử dụng css
prop, bạn cần cấu hình Babel/SWC hoặc thêm magic comment vào file. Với Next.js, thường chỉ cần cài @emotion/babel-plugin
và cấu hình trong babel.config.js
. Hoặc bạn có thể dùng magic comment:
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
// Component nhận prop color
function Box({ color }: { color: string }) {
return (
<div
// Sử dụng css prop với tagged template literal
css={css`
width: 100px;
height: 100px;
background-color: ${color}; /* Truy cập prop color */
margin: 10px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
`}
>
Box {color}
</div>
);
}
// Sử dụng component
function CssPropExample() {
return (
<div css={css`display: flex;`}>
<Box color="teal" /> {/* OK */}
<Box color="purple" /> {/* OK */}
{/* <Box color={123} /> */} {/* 🚨 TypeScript báo lỗi: number không gán được cho string */}
</div>
);
}
export default CssPropExample;
Giải thích:
/** @jsxImportSource @emotion/react */
: Magic comment này hướng dẫn trình biên dịch (Babel/SWC) xử lý cú pháp JSX để hỗ trợcss
prop.import { css } from '@emotion/react';
: Import hàmcss
(thực chất là một tagged template literal function).css={css
...}
: Truyền kết quả của việc gọi hàmcss
với chuỗi CSS vàocss
prop của thẻdiv
. Emotion sẽ xử lý điều này để tạo và áp dụng style.${color}
: Tương tự nhưstyled
, bạn có thể nhúng biểu thức JavaScript trong chuỗi CSS. Bên trong hàm được gọi bởi${}
, bạn có quyền truy cập vào các props của component chứa element đó.- Type Safety: TypeScript đã định nghĩa kiểu của prop
color
làstring
trong interface của componentBox
. Khi bạn sử dụngcolor
trong CSS, TypeScript biết nó là một string. Quan trọng hơn, khi bạn sử dụng<Box color={123} />
, TypeScript sẽ kiểm tra kiểu và báo lỗi vì123
lànumber
chứ không phảistring
.
css
prop rất tiện lợi cho các styling nhỏ, chỉ dùng một lần hoặc khi bạn muốn áp dụng style động cho một element cụ thể mà không cần bọc nó trong một styled component mới.
4. Styling Phần Tử Con
Bạn có thể dễ dàng style các phần tử con bên trong một styled component bằng cách sử dụng các selector CSS thông thường.
import styled from '@emotion/styled';
const ArticleCard = styled.div`
border: 1px solid #ddd;
padding: 15px;
margin: 10px;
border-radius: 8px;
max-width: 300px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
/* Style thẻ h3 BÊN TRONG ArticleCard */
h3 {
color: #333;
margin-top: 0;
margin-bottom: 10px;
}
/* Style thẻ p BÊN TRONG ArticleCard */
p {
color: #666;
line-height: 1.5;
}
/* Style khi hover lên ArticleCard */
&:hover {
border-color: #007bff;
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.2);
}
`;
// Component sử dụng ArticleCard
function ArticleCardExample({ title, content }: { title: string; content: string }) {
return (
<ArticleCard>
<h3>{title}</h3>
<p>{content}</p>
</ArticleCard>
);
}
export default ArticleCardExample;
Giải thích:
- Bên trong chuỗi CSS của
ArticleCard
, chúng ta sử dụng các selectorh3
vàp
như CSS thông thường. Emotion (hoặc thư viện CSS-in-JS khác) sẽ xử lý để đảm bảo các style này chỉ áp dụng cho các thẻh3
vàp
nằm bên trong một instance củaArticleCard
. &
trong CSS-in-JS thường đại diện cho chính styled component đó.&:hover
nghĩa là áp dụng style khi con trỏ chuột di chuột qua componentArticleCard
.
TypeScript ở đây chủ yếu đảm bảo các props title
và content
của component wrapper (ArticleCardExample
) đúng kiểu dữ liệu. Bản thân các selector CSS là cú pháp của CSS, không phải TypeScript, nhưng việc kết hợp chúng trong một component được type-check giúp giữ mọi thứ ngăn nắp và dễ hiểu.
5. Sử Dụng Theme Với TypeScript (Khái niệm)
Nhiều ứng dụng lớn sử dụng theme để quản lý các giá trị styling chung như màu sắc, spacing, typography. CSS-in-JS tích hợp rất tốt với theme. Quan trọng hơn, TypeScript giúp bạn có kiểu dữ liệu cho theme object, đảm bảo bạn truy cập đúng tên thuộc tính trong theme.
Để làm điều này, bạn thường cần định nghĩa kiểu cho theme và cấu hình thư viện CSS-in-JS để nhận theme object.
// Bước 1: Định nghĩa kiểu cho Theme (thường ở một file riêng như types/emotion.d.ts)
import '@emotion/react'; // Quan trọng: import từ @emotion/react để mở rộng module
declare module '@emotion/react' {
export interface Theme {
colors: {
primary: string;
secondary: string;
text: string;
background: string;
};
spacing: {
small: string;
medium: string;
large: string;
};
typography: {
fontFamily: string;
fontSize: string;
};
// Thêm các thuộc tính theme khác tại đây
}
}
// Bước 2: Sử dụng Theme trong Styled Component
import styled from '@emotion/styled';
import { useTheme } from '@emotion/react'; // Hook để truy cập theme trong function component
const ThemedContainer = styled.div`
background-color: ${props => props.theme.colors.background}; /* Truy cập theme qua props */
color: ${props => props.theme.colors.text};
padding: ${props => props.theme.spacing.large};
font-family: ${props => props.theme.typography.fontFamily};
border-radius: ${props => props.theme.spacing.small}; /* Ví dụ dùng spacing cho border-radius */
`;
// Component sử dụng ThemedContainer
function ThemedExample() {
// Với styled components, theme tự động có trong props
return (
<ThemedContainer>
<p>Nội dung được style theo theme.</p>
</ThemedContainer>
);
}
// Hoặc sử dụng useTheme hook trong functional component
function ThemedFunctionalExample() {
const theme = useTheme(); // theme ở đây có kiểu Theme đã định nghĩa
return (
<div css={css`
background-color: ${theme.colors.secondary};
padding: ${theme.spacing.medium};
color: ${theme.colors.primary};
`}>
<p>Nội dung khác cũng theo theme.</p>
</div>
);
}
// Lưu ý: Để theme hoạt động, bạn cần bọc ứng dụng của mình trong <ThemeProvider theme={yourThemeObject}>
// import { ThemeProvider } from '@emotion/react';
// const myTheme = { colors: { primary: 'blue', secondary: 'pink', ... }, spacing: { ... }, ... };
// <ThemeProvider theme={myTheme}> <App /> </ThemeProvider>
export default ThemedExample; // Hoặc ThemedFunctionalExample
Giải thích:
declare module '@emotion/react' { ... }
: Đoạn code này mở rộng kiểu dữ liệu của module@emotion/react
để định nghĩa interfaceTheme
. Điều này cho TypeScript biết cấu trúc của theme object khi bạn sử dụng Emotion.props.theme.colors.background
: Bên trong styled component, theme object có sẵn trongprops.theme
. TypeScript biết cấu trúc củaprops.theme
nhờ vào interfaceTheme
bạn đã định nghĩa. Nếu bạn cố gắng truy cậpprops.theme.colors.misspelledColor
hoặcprops.theme.nonExistentCategory
, TypeScript sẽ báo lỗi.useTheme()
: Hook này cung cấp quyền truy cập vào theme object trong bất kỳ functional component nào. Nhờ interfaceTheme
, biếntheme
này cũng được type-check đầy đủ.- An toàn với Theme: TypeScript đảm bảo bạn chỉ sử dụng các giá trị theme đã được định nghĩa, tránh lỗi gõ sai hoặc quên cập nhật khi thay đổi theme.
Việc sử dụng theme kết hợp TypeScript là một pattern rất mạnh để xây dựng hệ thống thiết kế (design system) nhất quán và dễ bảo trì.
Lợi Ích Tổng Kết
Qua các ví dụ trên, có thể thấy sự kết hợp của Styled JSX (CSS-in-JS) và TypeScript mang lại nhiều lợi ích:
- Colocation: Đặt logic và styling cùng nhau giúp component dễ hiểu và bảo trì.
- Styling Động Mạnh Mẽ: Dễ dàng thay đổi giao diện dựa trên props và state của component.
- Phạm Vi Hạn Chế (Scoped): Tránh xung đột CSS toàn cục không mong muốn.
- Tái Sử Dụng Cao: Dễ dàng tạo ra các styled component và sử dụng lại chúng khắp ứng dụng.
- An Toàn Kiểu Dữ Liệu (TypeScript): Đây là điểm đặc biệt quan trọng. TypeScript đảm bảo props ảnh hưởng đến styling được sử dụng đúng cách, bắt lỗi sớm, cải thiện độ tin cậy và tăng tốc độ phát triển nhờ autocompletion.
- Hỗ Trợ Theme Tốt: Xây dựng hệ thống thiết kế nhất quán trở nên an toàn hơn với kiểu dữ liệu cho theme.
Một Vài Điều Cần Lưu Ý
Mặc dù mạnh mẽ, phương pháp này cũng có một số điểm cần cân nhắc:
- Độ Phức Tạp: Việc kết hợp CSS, JS và TypeScript trong cùng một file có thể cảm thấy phức tạp lúc đầu.
- Hiệu Năng Runtime: Một số thư viện CSS-in-JS có thể có một chút chi phí runtime để sinh ra style, mặc dù các thư viện hiện đại như Emotion và Styled Components đã được tối ưu hóa rất nhiều.
- Kích Thước Bundle: Code CSS của bạn được thêm vào bundle JavaScript/TypeScript, có thể làm tăng kích thước bundle ban đầu (dù thường không đáng kể với code splitting).
- Server-Side Rendering (SSR): Cần cấu hình đặc biệt để CSS được render đúng cách trên server với các framework như Next.js.
Tuy nhiên, với lợi ích về khả năng bảo trì, an toàn kiểu dữ liệu và trải nghiệm phát triển (developer experience), Styled JSX với TypeScript vẫn là một lựa chọn tuyệt vời và phổ biến cho các dự án React/Next.js hiện đại.
Hy vọng bài viết này đã cho bạn cái nhìn rõ ràng về cách kết hợp sức mạnh của Styled JSX và TypeScript để viết code styling hiệu quả hơn, an toàn hơn và dễ bảo trì hơn trong các ứng dụng Front-end của bạn!
Comments