Lỗi Gặp Phải
Mở console của trình duyệt lên và bạn sẽ thấy thông báo như thế này:
Warning: Prop `className` did not match. Server: "sc-bdVTJa" Client: "sc-fzoLsD"
Trang web trông vẫn bình thường. React bắn ra cảnh báo hydration mismatch này, và đây không chỉ là thông báo vô nghĩa. Trong môi trường dev, nó làm ngập console của bạn. Trong production, React bỏ đi toàn bộ HTML đã render phía server và render lại mọi thứ ở phía client — first paint chậm hơn, SEO kém hơn, và điểm Core Web Vitals bị ảnh hưởng.
Nguyên Nhân
styled-components không dùng tên class cố định. Nó tạo ra chúng lúc runtime bằng một bộ đếm nội bộ được khởi tạo lại từ đầu mỗi lần chạy.
Trên server, bộ đếm bắt đầu từ zero và tạo ra thứ gì đó như sc-bdVTJa. Sau đó trình duyệt tải JavaScript, React bắt đầu hydrate, và bộ đếm reset lại — xuất ra một chuỗi hoàn toàn khác: sc-fzoLsD.
HTML phía server ghi className="sc-bdVTJa". Còn render phía client của React lại ghi className="sc-fzoLsD". Chúng không khớp nhau. Cảnh báo được kích hoạt.
CSS Modules cũng có thể gây ra vấn đề tương tự, nhưng vì lý do khác: chuỗi tên class được ghép động, hoặc pattern hash của localIdentName không khớp giữa cấu hình webpack phía server và phía client.
Chẩn Đoán Nhanh
Trước khi tìm cách sửa, hãy xác nhận xem chính xác vấn đề nằm ở đâu:
- Chuột phải → Xem Nguồn Trang. Bạn có thấy thẻ
<style>trong<head>không? Nếu thiếu styles nghĩa là SSR collection không chạy. - Kiểm tra hàm render SSR của bạn xem có
ServerStyleSheetkhông. Không có? Đó chính là thủ phạm. - Trên Next.js, xác nhận rằng tùy chọn compiler cho styled-components đã được bật trong
next.config.js.
Cách Sửa 1: Cài babel-plugin-styled-components (React Thông thường / Express SSR)
Nguyên nhân gốc rễ là tên class không xác định trước. Plugin babel này giải quyết vấn đề bằng cách tạo tên class dựa trên đường dẫn file và display name của mỗi component thay vì dùng bộ đếm — nhờ đó sc-bdVTJa sẽ trở thành cùng một hash ổn định trên cả server lẫn client, trong mọi lần chạy.
npm install --save-dev babel-plugin-styled-components
# hoặc
yarn add -D babel-plugin-styled-components
Thêm vào .babelrc hoặc babel.config.js:
{
"plugins": [
[
"babel-plugin-styled-components",
{
"ssr": true,
"displayName": true,
"preprocess": false
}
]
]
}
"ssr": true là tùy chọn quan trọng nhất — nó chuyển cơ chế tạo tên class từ dựa trên bộ đếm sang tất định. "displayName": true là phần thưởng thêm: nó đặt tên component làm tiền tố cho mỗi class, giúp việc debug trên DevTools dễ dàng hơn nhiều.
Cách Sửa 2: Kết Nối ServerStyleSheet Vào Hàm Render SSR
Plugin babel thôi chưa đủ. styled-components còn cần thu thập styles phía server và inject chúng vào HTML response trước khi gửi về trình duyệt. Bỏ qua bước này và client sẽ bắt đầu từ đầu, tạo lại mọi thứ.
import { ServerStyleSheet } from 'styled-components'
import { renderToString } from 'react-dom/server'
import App from './App'
function handleRequest(req, res) {
const sheet = new ServerStyleSheet()
try {
const html = renderToString(
sheet.collectStyles(<App />)
)
const styleTags = sheet.getStyleTags()
res.send(`
<!DOCTYPE html>
<html>
<head>${styleTags}</head>
<body><div id="root">${html}</div></body>
</html>
`)
} finally {
sheet.seal()
}
}
sheet.collectStyles() bọc app của bạn và chặn mọi style của styled-components khi các component render. sheet.getStyleTags() trả về các thẻ <style> sẵn sàng để inject.
Khối finally là bắt buộc, không thể bỏ qua. Nếu không gọi sheet.seal(), memory leak sẽ tích lũy qua từng request — mỗi server response để lại một style sheet ảo không bao giờ được giải phóng.
Cách Sửa 3: Next.js — Bật Compiler Styled-Components Tích Hợp Sẵn
Next.js 12 ra mắt compiler styled-components viết bằng Rust, khiến plugin babel trở nên lỗi thời. Một dòng config trong next.config.js là đủ để bật nó:
/** @type {import('next').NextConfig} */
const nextConfig = {
compiler: {
styledComponents: true,
},
}
module.exports = nextConfig
Cần kiểm soát chi tiết hơn trong quá trình phát triển? Mở rộng object tùy chọn:
compiler: {
styledComponents: {
displayName: true,
ssr: true,
fileName: true,
meaninglessFileNames: ['index', 'styles'],
minify: true,
transpileTemplateLiterals: true,
pure: true,
},
},
Sau khi bật tùy chọn này, hãy xóa babel-plugin-styled-components khỏi cấu hình babel của bạn. Chạy cả hai cùng lúc sẽ gây xung đột — và đó là loại xung đột âm thầm, khó chẩn đoán.
Pages Router cũng cần một file _document.tsx tùy chỉnh để xử lý việc thu thập styles phía server:
import Document, { DocumentContext, DocumentInitialProps } from 'next/document'
import { ServerStyleSheet } from 'styled-components'
export default class MyDocument extends Document {
static async getInitialProps(
ctx: DocumentContext
): Promise<DocumentInitialProps> {
const sheet = new ServerStyleSheet()
const originalRenderPage = ctx.renderPage
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
})
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
styles: [initialProps.styles, sheet.getStyleElement()],
}
} finally {
sheet.seal()
}
}
}
App Router (Next.js 13+) là câu chuyện khác. React Server Components không có React context, vì vậy ServerStyleSheet đơn giản là không hoạt động ở đó. Các lựa chọn của bạn: chuyển sang Tailwind CSS, next/font, hoặc thư viện khác tương thích với RSC — hoặc giữ styled-components chỉ trong các component 'use client' bằng cách dùng style registry:
'use client'
// lib/registry.tsx
import React, { useState } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { ServerStyleSheet, StyleSheetManager } from 'styled-components'
export default function StyledComponentsRegistry({
children,
}: {
children: React.ReactNode
}) {
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet())
useServerInsertedHTML(() => {
const styles = styledComponentsStyleSheet.getStyleElement()
styledComponentsStyleSheet.instance.clearTag()
return <>{styles}</>
})
if (typeof window !== 'undefined') return <>{children}</>
return (
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
{children}
</StyleSheetManager>
)
}
Bọc root layout của bạn với registry này và styled-components sẽ đẩy styles vào server response một cách chính xác.
Cách Sửa 4: CSS Modules — Tránh Ghép Tên Class Động
Tên class của CSS Modules vốn dĩ ổn định theo thiết kế. Chúng bị hỏng khi bạn ghép chúng bằng string concatenation, hoặc khi pattern hash của localIdentName trong webpack khác nhau giữa bundle server và client — dù chỉ một ký tự.
Chuyển sang dùng clsx thay vì ghép chuỗi:
import styles from './Button.module.css'
import clsx from 'clsx'
function Button({ primary, disabled }) {
// Không tốt — ghép chuỗi dễ bị lỗi:
// const cls = `${styles.button} ${primary ? styles.primary : ''}`
// Tốt hơn — clsx xử lý undefined/false một cách duyên dáng:
const cls = clsx(styles.button, {
[styles.primary]: primary,
[styles.disabled]: disabled,
})
return <button className={cls}>Click me</button>
}
Với cấu hình webpack tùy chỉnh, hãy đặt localIdentName theo cùng một pattern chính xác trong cả hai cấu hình server và client:
// webpack.config.js — giống nhau trên CẢ server lẫn client
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[path][name]__[local]--[hash:base64:5]',
},
},
}
Kiểm Tra Sau Khi Sửa
- Kiểm tra cảnh báo đã biến mất — khởi động lại hoàn toàn dev server (
Ctrl+C, sau đónpm run dev). Hot reload không nhận các thay đổi cấu hình babel. Cảnh báoProp className did not matchphải biến mất hoàn toàn. - Kiểm tra View Source — HTML được render phía server phải đã chứa thẻ
<style>trong<head>ngay từ lần tải đầu tiên, trước khi JavaScript nào chạy. - So sánh tên class — inspect một element styled trong DevTools và ghi lại class của nó. Tìm element đó trong View Source. Chúng phải giống nhau từng ký tự một.
- Chạy production build:
# Next.js
npm run build && npm run start
# Mở trình duyệt → F12 → tab Console
# Lọc theo "className" — kết quả bằng không là bạn đã xong
Các Lỗi Thường Gặp Sau Khi Sửa
- Cả babel plugin lẫn Next.js compiler đều đang hoạt động cùng lúc — chúng xung đột nhau. Chọn một cái và xóa cái còn lại khỏi cấu hình của bạn.
- Quên khởi động lại dev server — các thay đổi babel không được hot reload nhận ra. Phải khởi động lại hoàn toàn.
- Thiếu
sheet.seal()— style sheet chồng chất qua từng request và cuối cùng gây memory leak. Luôn seal trong khốifinally. - Dùng
createGlobalStylemà không có SSR collection — global styles chạy qua cùng một hệ thống bộ đếm và cũng cần được đẩy quaServerStyleSheet. - App Router + styled-components mà không có registry — không có React context đồng nghĩa không inject được styles. Pattern registry là bắt buộc trong trường hợp này.

