Bài 22.3: Emotion với TypeScript

Trong thế giới phát triển web hiện đại, việc quản lý CSS trong các ứng dụng quy mô lớn có thể trở thành một thách thức không nhỏ. Các phương pháp truyền thống đôi khi dẫn đến xung đột CSS, khó khăn trong việc tái sử dụng, và đặc biệt là thiếu sự an toàn kiểu dữ liệu khi cần áp dụng các style động. Đây là lúc các thư viện CSS-in-JS như Emotion tỏa sáng, mang lại một cách tiếp cận linh hoạtcomponent-based hơn cho việc styling.

Tuy nhiên, ngay cả với CSS-in-JS, bạn vẫn có thể gặp phải các lỗi phổ biến như sai chính tả thuộc tính CSS, truyền nhầm kiểu dữ liệu cho giá trị, hoặc không kiểm soát được các props ảnh hưởng đến style. Đây chính là lúc TypeScript phát huy sức mạnh vượt trội. Khi kết hợp sự linh hoạt của Emotion với sự an toàn kiểu dữ liệukhả năng kiểm tra tĩnh của TypeScript, chúng ta có trong tay một bộ đôi cực kỳ mạnh mẽ để xây dựng giao diện người dùng vừa đẹp mắt vừa bền vững, dễ bảo trì hơn bao giờ hết.

Bài viết này sẽ đi sâu vào cách chúng ta có thể tận dụng tối đa Emotion cùng với TypeScript, từ những cú pháp cơ bản đến việc xử lý các tình huống nâng cao hơn.

Tại sao lại là Emotion?

Emotion là một thư viện CSS-in-JS phổ biến và hiệu quả. Những lợi ích chính mà Emotion mang lại bao gồm:

  • CSS Scope: Styles được tự động giới hạn trong phạm vi của từng component, loại bỏ gần như hoàn toàn vấn đề xung đột tên class.
  • Dynamic Styling: Dễ dàng tạo ra các styles thay đổi dựa trên props hoặc trạng thái của component.
  • Collocation: Giúp giữ logic component và styles của nó gần nhau, tăng tính tổ chức và dễ đọc.
  • Performance: Emotion có cơ chế rất hiệu quả để inject styles vào DOM.

Tại sao cần TypeScript khi dùng Emotion?

Emotion tự thân nó đã tuyệt vời, nhưng TypeScript thêm một lớp an toàntrải nghiệm phát triển vượt trội:

  1. Kiểm tra kiểu dữ liệu cho CSS: TypeScript có thể kiểm tra xem bạn có đang sử dụng đúng tên thuộc tính CSS (như backgroundColor thay vì backgrondColor) và đúng kiểu dữ liệu cho giá trị của chúng hay không. Điều này giúp bắt lỗi ngay trong quá trình viết mã thay vì lúc chạy.
  2. An toàn kiểu dữ liệu cho Props: Khi styles của bạn phụ thuộc vào các props được truyền vào component, TypeScript đảm bảo rằng bạn đang truyền đúng tên props và đúng kiểu dữ liệu mà styles mong đợi.
  3. Intellisense và Gợi ý mã: Trong trình soạn thảo mã (IDE) hỗ trợ TypeScript, bạn sẽ nhận được gợi ý thông minh (intellisense) cho cả thuộc tính CSS lẫn các props ảnh hưởng đến style. Điều này tăng tốc độ codegiảm thiểu sai sót.
  4. Refactoring dễ dàng hơn: Với kiểu dữ liệu rõ ràng, việc thay đổi tên props hoặc cấu trúc style trở nên an toàn hơn vì TypeScript sẽ báo lỗi ở những nơi cần cập nhật.

Bắt đầu với Emotion và TypeScript

Để sử dụng Emotion trong dự án TypeScript/React, bạn cần cài đặt các gói sau (ví dụ với npm):

npm install @emotion/react @emotion/styled @emotion/css

Nếu bạn đang dùng React (mà khả năng cao là vậy), bạn cũng cần cài đặt các type definition tương ứng (thường thì @emotion/react đã bao gồm, nhưng đôi khi vẫn cần cài thêm @types cho các phiên bản cũ hoặc các gói phụ trợ khác):

npm install -D @types/react # Nếu chưa có

Sau khi cài đặt, bạn đã sẵn sàng để tích hợp Emotion vào các component TypeScript của mình.

Các Khái Niệm Chính với Hỗ trợ của TypeScript

1. Styled Components

Đây là cách phổ biến nhất để sử dụng Emotion. Bạn tạo ra một component mới bằng cách gọi styled với tên thẻ HTML hoặc component React khác.

import styled from '@emotion/styled';

// Tạo một Button cơ bản
const BasicButton = styled.button`
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
`;

// Sử dụng component
function App() {
  return (
    <BasicButton>Click Me</BasicButton>
  );
}

Giải thích:

  • Chúng ta import styled từ @emotion/styled.
  • Gọi styled.button và dùng cú pháp template literals (dấu backtick `) để viết CSS thông thường.
  • Kết quả là một component React mới (BasicButton) mà bạn có thể sử dụng như thẻ <button> bình thường, nhưng đã được áp dụng style.

TypeScript hỗ trợ ở đây như thế nào?

Khi bạn viết CSS bên trong template literal, TypeScript (với sự hỗ trợ của các plugin ngôn ngữ trong IDE) sẽ kiểm tra các thuộc tính và giá trị CSS. Nếu bạn gõ sai (backgrond-color thay vì background-color), trình soạn thảo sẽ ngay lập tức báo lỗi.

import styled from '@emotion/styled';

const ButtonWithTypeError = styled.button`
  padding: 10px 20px;
  border: none;
  backgrond-color: blue; /* <- TypeScript sẽ báo lỗi ở đây! */
  color: white;
`;
2. Styled Components với Props

Đây là nơi TypeScript thực sự tỏa sáng. Bạn có thể tạo ra các styles động dựa trên props được truyền vào component.

import styled from '@emotion/styled';

// Định nghĩa kiểu dữ liệu cho props
interface ButtonProps {
  primary?: boolean;
  backgroundColor?: string; // Ví dụ prop có kiểu string
}

// Tạo Button với props, sử dụng Generic để chỉ định kiểu props
const StyledButton = styled.button<ButtonProps>`
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;

  /* Sử dụng props để thay đổi style */
  background-color: ${props => props.primary ? 'royalblue' : (props.backgroundColor || '#f0f0f0')};
  color: ${props => props.primary ? 'white' : 'black'};

  &:hover {
    opacity: 0.9;
  }
`;

// Sử dụng component với props
function App() {
  return (
    <>
      <StyledButton primary>Primary Button</StyledButton>
      <StyledButton>Secondary Button</StyledButton>
      <StyledButton backgroundColor="orange">Orange Button</StyledButton>
      {/* <StyledButton isActive={true}>Error</StyledButton> /* <- TypeScript sẽ báo lỗi vì isActive không tồn tại trong ButtonProps */ }
    </>
  );
}

Giải thích:

  • Chúng ta định nghĩa một interface ButtonProps để mô tả các props mà component styled này sẽ nhận.
  • Khi gọi styled.button, chúng ta truyền ButtonProps vào trong dấu ngoặc nhọn (<>) như một generic type. Điều này nói với TypeScript rằng component này sẽ nhận các props theo cấu trúc của ButtonProps.
  • Bên trong template literal, chúng ta sử dụng cú pháp ${props => ...} để truy cập các props. TypeScript biết kiểu của propsButtonProps, nên nó sẽ cung cấp autocompletion và kiểm tra kiểu khi bạn truy cập props.primary hay props.backgroundColor.
  • Khi sử dụng <StyledButton>, TypeScript sẽ kiểm tra xem bạn có truyền đúng props hay không. Nếu bạn cố gắng truyền một prop không có trong ButtonProps (như isActive trong ví dụ comment), TypeScript sẽ ngăn bạn lại ngay lập tức.
3. css Prop

Emotion cung cấp một cách siêu linh hoạt để thêm styles trực tiếp vào bất kỳ element hoặc component nào bằng cách sử dụng css prop. Để sử dụng nó, bạn cần thêm pragma @jsx jsx hoặc @jsxRuntime automatic (với React 17+) và import jsx (hoặc không cần import với runtime automatic) từ @emotion/react.

/** @jsxImportSource @emotion/react */ // Dành cho runtime automatic
import { jsx, css } from '@emotion/react';

interface BoxProps {
  isHighlighted?: boolean;
}

function Box({ isHighlighted }: BoxProps) {
  const boxStyles = css`
    padding: 20px;
    border: 1px solid gray;
    margin: 10px;
    border-radius: 8px;

    /* Style động dựa trên prop */
    background-color: ${isHighlighted ? 'yellow' : 'white'};
    font-weight: ${isHighlighted ? 'bold' : 'normal'};
  `;

  return (
    <div css={boxStyles}>
      Đây  một hộp nội dung.
    </div>
  );
}

// Sử dụng component
function App() {
  return (
    <>
      <Box />
      <Box isHighlighted={true} />
      {/* <Box color="red" /> /* <- TypeScript sẽ báo lỗi vì color không có trong BoxProps */ }
    </>
  );
}

Giải thích:

  • Chúng ta sử dụng css function từ @emotion/react để tạo ra một object style.
  • Object style này được truyền vào css prop của thẻ <div>.
  • Chúng ta vẫn có thể sử dụng props (isHighlighted) để tạo style động bên trong css template literal.
  • Giống như styled components, TypeScript kiểm tra các thuộc tính CSS bên trong template literal của css.

TypeScript hỗ trợ ở đây như thế nào?

Mặc dù css prop nằm trên thẻ <div> thông thường, nhưng khi bạn tạo object style bằng css function, TypeScript vẫn kiểm tra các thuộc tính và giá trị CSS bên trong template literal. Quan trọng hơn, khi bạn tạo một hàm trả về object style dựa trên props (như cách bạn có thể làm với css function, xem phần tiếp theo), TypeScript sẽ kiểm tra kiểu dữ liệu của các props đó.

4. css Function để Tái Sử Dụng Styles

css function cực kỳ hữu ích khi bạn muốn định nghĩa các đoạn style có thể tái sử dụng hoặc kết hợp nhiều bộ style với nhau.

/** @jsxImportSource @emotion/react */
import { jsx, css } from '@emotion/react';

// Định nghĩa một bộ style cơ bản
const baseStyles = css`
  margin: 10px;
  padding: 15px;
  border-radius: 5px;
`;

// Định nghĩa style cho cảnh báo
const warningStyles = css`
  background-color: #fff3cd;
  border: 1px solid #ffeeba;
  color: #856404;
`;

// Định nghĩa style cho lỗi
const errorStyles = css`
  background-color: #f8d7da;
  border: 1px solid #f5c6cb;
  color: #721c24;
`;

// Kết hợp các style sử dụng array trong css prop
function Message({ type, children }: { type: 'warning' | 'error'; children: React.ReactNode }) {
  return (
    <div
      css={[
        baseStyles, // Style cơ bản
        type === 'warning' && warningStyles, // Áp dụng warningStyles nếu type là warning
        type === 'error' && errorStyles,     // Áp dụng errorStyles nếu type là error
      ]}
    >
      {children}
    </div>
  );
}

// Sử dụng component
function App() {
  return (
    <>
      <Message type="warning">Đây  một cảnh báo!</Message>
      <Message type="error">Đã xảy ra lỗi nghiêm trọng!</Message>
      {/* <Message type="info">Không có kiểu info! /* <- TypeScript báo lỗi */ }
    </>
  );
}

Giải thích:

  • Chúng ta tạo các hằng số (baseStyles, warningStyles, errorStyles) bằng cách gọi css function. Mỗi hằng số này chứa một đối tượng mô tả các styles.
  • Trong css prop, chúng ta truyền một mảng các đối tượng style. Emotion sẽ hợp nhất chúng lại.
  • Chúng ta sử dụng toán tử logic && để áp dụng có điều kiện các style (warningStyles hoặc errorStyles) dựa trên prop type.

TypeScript hỗ trợ ở đây như thế nào?

TypeScript kiểm tra cú pháp CSS bên trong từng lần gọi css function. Quan trọng hơn, trong ví dụ Message, chúng ta đã định nghĩa rõ ràng kiểu của prop type'warning' | 'error'. Khi sử dụng component <Message>, TypeScript sẽ đảm bảo rằng giá trị truyền cho type phải là một trong hai chuỗi đó, giúp ngăn ngừa lỗi chính tả hoặc truyền giá trị không hợp lệ.

5. Thao tác với Theme

Emotion hỗ trợ theming để quản lý các giá trị thiết kế toàn cục như màu sắc, font, spacing, v.v. TypeScript giúp đảm bảo rằng bạn truy cập các giá trị trong theme một cách an toàn và có autocompletion.

Đầu tiên, bạn cần định nghĩa kiểu cho đối tượng theme của mình. Bạn làm điều này bằng cách mở rộng (extend) interface Theme của Emotion.

// Ví dụ: src/types/emotion.d.ts (Đặt file này ở nơi TypeScript có thể thấy)

import '@emotion/react'; // Quan trọng: import gốc để mở rộng interface

declare module '@emotion/react' {
  export interface Theme {
    colors: {
      primary: string;
      secondary: string;
      background: string;
      text: string;
    };
    spacing: {
      small: string; // ví dụ: '8px'
      medium: string; // ví dụ: '16px'
      large: string; // ví dụ: '24px'
    };
    // Thêm các thuộc tính theme khác tại đây (fontSizes, breakpoints, v.v.)
  }
}

Giải thích:

  • Chúng ta tạo một file định nghĩa kiểu (.d.ts).
  • Sử dụng declare module '@emotion/react' để nói rằng chúng ta đang mở rộng module @emotion/react.
  • Export một interface tên là Theme (viết hoa chữ T) và định nghĩa cấu trúc của theme object mà bạn sẽ sử dụng.

Sau khi định nghĩa kiểu theme, bạn có thể sử dụng theme trong styled components hoặc css function, và TypeScript sẽ biết cấu trúc của theme.

import styled from '@emotion/styled';
// Không cần import Theme ở đây, TypeScript tự nhận
// import { Theme } from '@emotion/react'; // Đôi khi bạn cần import type này nếu dùng trực tiếp

const ThemedButton = styled.button`
  padding: ${props => props.theme.spacing.medium}; // <- TypeScript kiểm tra 'spacing' và 'medium'
  background-color: ${props => props.theme.colors.primary}; // <- TypeScript kiểm tra 'colors' và 'primary'
  color: ${props => props.theme.colors.background};
  border: none;
  border-radius: 4px;
  cursor: pointer;

  &:hover {
    background-color: ${props => props.theme.colors.secondary};
  }
`;

// Để sử dụng theme, bạn cần bọc ứng dụng của mình trong <ThemeProvider>
// import { ThemeProvider } from '@emotion/react';
// import { Theme } from '@emotion/react'; // Đôi khi cần import type

// const myTheme: Theme = { // Áp dụng kiểu đã định nghĩa
//   colors: {
//     primary: '#007bff',
//     secondary: '#6c757d',
//     background: '#ffffff',
//     text: '#212529',
//   },
//   spacing: {
//     small: '8px',
//     medium: '16px',
//     large: '24px',
//   },
// };

// function AppWithTheme() {
//   return (
//     <ThemeProvider theme={myTheme}>
//       <ThemedButton>Themed Button</ThemedButton>
//     </ThemeProvider>
//   );
// }

Giải thích:

  • Trong styled component, bạn truy cập theme thông qua props.theme.
  • Vì chúng ta đã mở rộng interface Theme, khi bạn gõ props.theme., TypeScript sẽ gợi ý các thuộc tính như colorsspacing.
  • Khi bạn tiếp tục gõ props.theme.colors., TypeScript sẽ gợi ý primary, secondary, v.v.
  • Nếu bạn cố gắng truy cập một thuộc tính không có trong theme đã định nghĩa (ví dụ: props.theme.fonts), TypeScript sẽ báo lỗi.

TypeScript hỗ trợ ở đây như thế nào?

Việc định nghĩa kiểu cho theme object cung cấp sự an toàn kiểu dữ liệu tuyệt vời. Nó không chỉ giúp bạn tránh lỗi chính tả khi truy cập các giá trị theme mà còn giúp tất cả các thành viên trong nhóm tuân thủ một cấu trúc theme nhất quán.

Comments

There are no comments at the moment.