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ọngthự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:

  1. 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.
  2. Dynamic Styling: Dễ dàng thay đổi kiểu dáng dựa trên props hoặc state của component.
  3. 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).
  4. 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  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àm styled.
  • styled.div: Gọi hàm styled và chỉ định rằng chúng ta muốn tạo một styled component dựa trên thẻ HTML div.
  • `...`: 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 interface ButtonProps vào hàm styled.button bằng cú pháp Generic (<...>). Điều này cho TypeScript biết rằng component DynamicButton sẽ nhận các props theo kiểu ButtonProps.
  • ${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ào DynamicButton. TypeScript biết kiểu của propsButtonProps.
  • props.primary ? '#007bff' : '#6c757d': Sử dụng giá trị của prop primary để quyết định màu nền.
  • switch (props.size): Sử dụng giá trị của prop size để 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 prop primary đá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àm css (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àm css với chuỗi CSS vào css 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 colorstring trong interface của component Box. Khi bạn sử dụng color 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ì 123number chứ không phải string.

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 selector h3p 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ẻ h3p nằm bên trong một instance của ArticleCard.
  • & 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 component ArticleCard.

TypeScript ở đây chủ yếu đảm bảo các props titlecontent 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 interface Theme. Đ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 trong props.theme. TypeScript biết cấu trúc của props.theme nhờ vào interface Theme bạn đã định nghĩa. Nếu bạn cố gắng truy cập props.theme.colors.misspelledColor hoặc props.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ờ interface Theme, biến theme 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

There are no comments at the moment.