Bài 22.2: Styled-components với TypeScript

Bài 22.2: Styled-components với TypeScript
Chào mừng trở lại series Lập trình Web Front-end! Hôm nay chúng ta sẽ đi sâu vào một sự kết hợp cực kỳ hiệu quả trong thế giới Front-end hiện đại: sử dụng Styled-components cùng với TypeScript. Nếu bạn đã quen thuộc với React và TypeScript, việc tích hợp Styled-components sẽ mang lại một cấp độ mới về khả năng quản lý CSS, đặc biệt là trong các dự án lớn, phức tạp.
Styled-components cho phép chúng ta viết CSS trực tiếp trong các file JavaScript/TypeScript của mình, tạo ra các component React đã được "style" sẵn. Khi kết hợp với TypeScript, chúng ta không chỉ nhận được lợi ích của CSS-in-JS (component-based styling, dynamic styling, theming) mà còn có thêm sự an toàn và khả năng bảo trì mà hệ thống kiểu dữ liệu của TypeScript mang lại.
Hãy cùng tìm hiểu cách chúng hoạt động cùng nhau!
Bắt Đầu Với Styled-components & TypeScript
Việc cài đặt rất đơn giản. Bạn chỉ cần thêm hai package chính: styled-components
và định nghĩa kiểu TypeScript cho nó @types/styled-components
.
npm install styled-components
npm install -D @types/styled-components
# hoặc
yarn add styled-components
yarn add -D @types/styled-components
Bây giờ, bạn có thể bắt đầu viết styled components với cú pháp quen thuộc:
import styled from 'styled-components';
// Tạo một styled component dựa trên thẻ div
const Container = styled.div`
padding: 20px;
background-color: #f0f0f0;
border-radius: 8px;
margin-bottom: 15px;
`;
// Tạo một styled component dựa trên thẻ button
const PrimaryButton = styled.button`
background-color: #007bff;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
&:hover {
background-color: #0056b3;
}
`;
// Sử dụng trong component React của bạn
function MyComponent() {
return (
<Container>
<p>Đây là nội dung bên trong container.</p>
<PrimaryButton>Nhấn vào đây</PrimaryButton>
</Container>
);
}
Giải thích:
- Chúng ta import
styled
từ thư việnstyled-components
. styled.div
hoặcstyled.button
là cách chúng ta nói với Styled-components rằng chúng ta muốn tạo một component React mà cuối cùng sẽ render ra thẻdiv
hoặcbutton
trong DOM.- Phần CSS được viết bên trong cặp dấu backticks (
) sử dụng cú pháp Tagged Template Literals của JavaScript.
- TypeScript tự động nhận diện kiểu của
Container
vàPrimaryButton
là các component React với các props HTML tương ứng (ví dụ:onClick
,className
,children
,...). Nhờ có@types/styled-components
, mọi thứ hoạt động mượt mà.
Style Các React Component Khác
Styled-components không chỉ dùng để style các thẻ HTML thông thường. Bạn hoàn toàn có thể style chính các component React mà bạn đã tạo ra hoặc từ thư viện bên ngoài. Cú pháp rất đơn giản, thay vì dùng styled.div
, bạn dùng styled(YourComponent)
.
import styled from 'styled-components';
import AnotherComponent from './AnotherComponent'; // Giả sử bạn có component này
interface FancyProps {
borderColor: string;
}
// Giả sử AnotherComponent nhận props thông thường
// interface AnotherComponentProps { text: string }
const StyledAnotherComponent = styled(AnotherComponent)<FancyProps>`
border: 2px solid ${(props) => props.borderColor};
padding: 10px;
margin: 10px 0;
`;
// Sử dụng
function App() {
return (
<StyledAnotherComponent
text="Nội dung của component khác"
borderColor="#ff00ff" // Truyền prop cho styled component
/>
);
}
Giải thích:
- Chúng ta sử dụng
styled(AnotherComponent)
để tạo một styled component dựa trênAnotherComponent
. - Bây giờ,
StyledAnotherComponent
sẽ nhận cả props màAnotherComponent
gốc cần (text
) và các props mới mà chúng ta định nghĩa cho mục đích styling (borderColor
). - Chúng ta dùng cú pháp
<FancyProps>
saustyled(AnotherComponent)
để nói với TypeScript về các props riêng mà chúng ta sẽ sử dụng trong phần style (borderColor
). Điều này giúp TypeScript kiểm tra kiểu cho các prop này khi bạn sử dụngStyledAnotherComponent
. - Bên trong phần CSS, chúng ta truy cập prop
borderColor
thông quaprops.borderColor
.
Truyền Props và Định Kiểu (Typing Props)
Đây là lúc TypeScript thực sự tỏa sáng khi làm việc với Styled-components. Khả năng style động dựa trên props là một sức mạnh lớn, và việc định kiểu cho các prop này giúp chúng ta tránh lỗi và có được gợi ý code tuyệt vời.
Cách phổ biến nhất là định nghĩa một interface
cho các props bạn muốn sử dụng trong CSS và truyền interface đó vào kiểu của styled component:
import styled from 'styled-components';
interface ButtonProps {
primary?: boolean; // Prop tùy chọn, kiểu boolean
size?: 'small' | 'medium' | 'large'; // Prop với các giá trị cụ thể
disabled?: boolean; // Prop tùy chọn, kiểu boolean
}
const DynamicButton = styled.button<ButtonProps>`
padding: ${(props) => {
switch (props.size) {
case 'small':
return '5px 10px';
case 'large':
return '15px 30px';
default: // medium
return '10px 20px';
}
}};
background-color: ${(props) => (props.primary ? '#007bff' : '#6c757d')};
color: white;
border: none;
border-radius: 4px;
cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
opacity: ${(props) => (props.disabled ? 0.6 : 1)};
&:hover {
background-color: ${(props) =>
props.primary ? '#0056b3' : '#5a6268'};
opacity: ${(props) => (props.disabled ? 0.6 : 0.9)};
}
`;
// Sử dụng
function ButtonExamples() {
return (
<div>
<DynamicButton primary size="large">
Primary Large
</DynamicButton>
<DynamicButton size="medium">Secondary Medium</DynamicButton>
<DynamicButton primary size="small" disabled>
Disabled Small
</DynamicButton>
</div>
);
}
Giải thích:
- Chúng ta định nghĩa interface
ButtonProps
với các prop mà component này sẽ nhận và sử dụng trong CSS. TypeScript đảm bảo rằng khi bạn dùng<DynamicButton>
, bạn phải truyền các prop đúng kiểu (ví dụ:size
chỉ có thể là 'small', 'medium', hoặc 'large'). - Cú pháp
<ButtonProps>
saustyled.button
là cách chúng ta liên kết interfaceButtonProps
với styled component này. Styled-components sẽ làm cho các prop này có sẵn trong hàm callback của template literal ((props) => ...
), và TypeScript sẽ biết kiểu củaprops
bên trong hàm đó làButtonProps
. Điều này mang lại tính năng tự động gợi ý (autocompletion) và kiểm tra lỗi kiểu dữ liệu ngay trong CSS. - Bên trong template literal, chúng ta truy cập các prop như
props.size
,props.primary
,props.disabled
để điều khiển style động.
Kế Thừa Styles (Extending Styles)
Đôi khi bạn muốn tạo một component mới chỉ khác một chút so với component đã có. Styled-components cho phép bạn dễ dàng kế thừa styles. Cách làm hiện đại và được khuyến khích là sử dụng cú pháp styled(ExistingStyledComponent)
. TypeScript xử lý việc này rất tốt.
import styled from 'styled-components';
const BaseButton = styled.button`
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
`;
const SuccessButton = styled(BaseButton)`
background-color: #28a745; /* Green */
color: white;
&:hover {
background-color: #218838;
}
`;
const DangerButton = styled(BaseButton)`
background-color: #dc3545; /* Red */
color: white;
&:hover {
background-color: #c82333;
}
`;
// Sử dụng
function InheritExamples() {
return (
<div>
<BaseButton>Base Button</BaseButton>
<SuccessButton>Success Action</SuccessButton>
<DangerButton>Danger Action</DangerButton>
</div>
);
}
Giải thích:
BaseButton
là styled component cơ bản.SuccessButton
vàDangerButton
được tạo bằng cách gọistyled(BaseButton)
. Điều này nghĩa là chúng sẽ kế thừa tất cả các styles từBaseButton
và bạn có thể thêm hoặc ghi đè (override) các styles khác.- TypeScript hiểu rằng
SuccessButton
vàDangerButton
cũng là các component React và sẽ có các props tương tự nhưBaseButton
(nếuBaseButton
có props động) cùng với các props mới bạn định nghĩa (nếu có).
Theming Với TypeScript
Theming là một tính năng mạnh mẽ của Styled-components, cho phép bạn định nghĩa các giá trị thiết kế (màu sắc, font, spacing,...) tập trung và sử dụng chúng trong toàn bộ ứng dụng. Khi kết hợp với TypeScript, bạn sẽ có toàn bộ lợi ích của theme cùng với kiểm tra kiểu dữ liệu và tự động gợi ý cho các giá trị trong theme object của bạn.
Để làm việc này, bạn cần định nghĩa kiểu dữ liệu cho theme object của mình và sau đó "nói" cho Styled-components (và TypeScript) biết về kiểu này.
Bước 1: Định nghĩa kiểu Theme của bạn
Tạo một file riêng, ví dụ src/theme.ts
hoặc src/types/theme.d.ts
:
// src/types/theme.d.ts
// Đây là interface định nghĩa cấu trúc theme của bạn
export interface MyTheme {
colors: {
primary: string;
secondary: string;
success: string;
danger: string;
background: string;
text: string;
};
spacing: {
small: string;
medium: string;
large: string;
};
fonts: {
body: string;
heading: string;
};
// ... thêm các thuộc tính theme khác
}
// Bạn cũng có thể xuất ra theme object cụ thể của mình
export const defaultTheme: MyTheme = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545',
background: '#ffffff',
text: '#212529',
},
spacing: {
small: '8px',
medium: '16px',
large: '24px',
},
fonts: {
body: "'Arial', sans-serif",
heading: "'Georgia', serif",
},
};
Bước 2: "Nói" cho Styled-components về kiểu Theme của bạn
TypeScript cần biết rằng khi Styled-components sử dụng một theme, theme đó sẽ tuân theo MyTheme
. Bạn làm điều này bằng cách sử dụng "Module Augmentation". Tạo một file định nghĩa kiểu riêng, ví dụ src/styled.d.ts
. Đảm bảo file này nằm trong phạm vi của tsconfig.json
của bạn.
// src/styled.d.ts
// Import styled-components để mở rộng namespace của nó
import 'styled-components';
// Import kiểu theme của bạn
import type { MyTheme } from './types/theme.d'; // Điều chỉnh đường dẫn nếu cần
// Khai báo lại module 'styled-components'
declare module 'styled-components' {
// Mở rộng interface DefaultTheme
// Bằng cách này, DefaultTheme của styled-components
// sẽ chứa tất cả các thuộc tính của MyTheme của bạn
export interface DefaultTheme extends MyTheme {}
}
Giải thích:
- File
styled.d.ts
này không chứa code chạy, nó chỉ cung cấp thông tin kiểu cho TypeScript. declare module 'styled-components'
nói rằng chúng ta đang thêm hoặc sửa đổi các định nghĩa kiểu cho modulestyled-components
.export interface DefaultTheme extends MyTheme {}
là mấu chốt. InterfaceDefaultTheme
là interface mà Styled-components sử dụng nội bộ để định nghĩa kiểu của theme context. Bằng cách mở rộng nó vớiMyTheme
, chúng ta nói với TypeScript rằng bất cứ khi nào Styled-components cung cấp một theme, nó sẽ có kiểu làMyTheme
.
Bước 3: Sử dụng ThemeProvider
và Theme trong Component
Bây giờ, bạn có thể bọc ứng dụng của mình bằng ThemeProvider
và dễ dàng truy cập theme object với đầy đủ kiểm tra kiểu và tự động gợi ý bên trong các styled component của bạn.
// src/App.tsx (hoặc root component của bạn)
import { ThemeProvider } from 'styled-components';
import { defaultTheme } from './types/theme.d'; // Import theme object
// Không cần import kiểu MyTheme ở đây nữa, vì đã augment ở styled.d.ts
import MyThemedComponent from './MyThemedComponent';
function App() {
return (
<ThemeProvider theme={defaultTheme}>
{/* Các component con của bạn */}
<MyThemedComponent />
</ThemeProvider>
);
}
export default App;
// src/MyThemedComponent.tsx
import styled from 'styled-components';
// Không cần import kiểu theme ở đây
const ThemedBox = styled.div`
padding: ${(props) => props.theme.spacing.medium};
background-color: ${(props) => props.theme.colors.background};
color: ${(props) => props.theme.colors.text};
border: 1px solid ${(props) => props.theme.colors.primary};
font-family: ${(props) => props.theme.fonts.body};
border-radius: ${(props) => props.theme.spacing.small};
`;
const ThemedHeading = styled.h2`
color: ${(props) => props.theme.colors.success};
font-family: ${(props) => props.theme.fonts.heading};
`;
// Sử dụng trong component khác (bên dưới ThemeProvider)
function MyThemedComponent() {
return (
<ThemedBox>
<ThemedHeading>Tiêu đề có Theme</ThemedHeading>
<p>Nội dung sử dụng các giá trị từ theme.</p>
</ThemedBox>
);
}
export default MyThemedComponent;
Giải thích:
ThemeProvider
được sử dụng để cung cấp theme object (defaultTheme
) cho toàn bộ cây component con bên dưới nó thông qua React Context.- Bên trong
ThemedBox
vàThemedHeading
, chúng ta truy cập theme object thông quaprops.theme
. - Nhờ có file
styled.d.ts
, khi bạn gõprops.theme.
, TypeScript sẽ hiển thị gợi ý các thuộc tính củaMyTheme
(nhưcolors
,spacing
,fonts
), và nó sẽ báo lỗi nếu bạn cố gắng truy cập một thuộc tính không tồn tại hoặc sai kiểu. Đây là lợi ích to lớn của việc kết hợp TypeScript với theme trong Styled-components.
Xử Lý Prop 'as' (Polymorphic Props)
Styled-components có một prop đặc biệt là as
, cho phép bạn thay đổi thẻ HTML hoặc component mà styled component đó render ra, trong khi vẫn giữ nguyên styles. TypeScript và Styled-components xử lý điều này khá thông minh.
Ví dụ, bạn có thể tạo một styled component trông giống một nút, nhưng muốn render nó dưới dạng thẻ <a>
hoặc một component Link
từ thư viện routing.
import styled from 'styled-components';
// import { Link } from 'react-router-dom'; // Giả sử bạn dùng React Router
const StyledButtonLike = styled.button`
display: inline-block; /* Quan trọng nếu đổi từ button sang a hoặc div */
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none; /* Để đảm bảo không có gạch chân khi dùng as="a" */
text-align: center;
&:hover {
background-color: #0056b3;
}
`;
// Sử dụng prop 'as'
function PolymorphicExamples() {
return (
<div>
{/* Render mặc định là button */}
<StyledButtonLike onClick={() => alert('Clicked!')}>
Đây là Button
</StyledButtonLike>
{/* Render như một thẻ link */}
<StyledButtonLike as="a" href="/about">
Đây là Link (trông như Button)
</StyledButtonLike>
{/* Render như một React Router Link component */}
{/* TypeScript hiểu các props của component Link (như to, replace) */}
{/* <StyledButtonLike as={Link} to="/dashboard">
Đi tới Dashboard
</StyledButtonLike> */}
{/* Render như một thẻ div */}
<StyledButtonLike as="div">
Đây là Div (trông như Button)
</StyledButtonLike>
</div>
);
}
Giải thích:
- Styled-components và TypeScript tự động kết hợp các props của styled component gốc (
StyledButtonLike
) với các props intrinsic của thẻ/component được chỉ định bởi propas
. - Ví dụ, khi bạn dùng
as="a"
, TypeScript biết rằng bạn đang render một thẻ<a>
và sẽ cho phép bạn sử dụng các thuộc tính của thẻ<a>
nhưhref
mà không báo lỗi. Tương tự vớias="button"
(các thuộc tính của button nhưonClick
,disabled
) hoặc khi sử dụng một component React khác. - Đây là một cách linh hoạt để sử dụng lại styles trên các loại phần tử khác nhau trong DOM.
Comments