🌞

Làm tính năng Theme trong React?

Sửa bài viết này

Để hỗ trợ tính năng Theme, cho phép người dùng lựa chọn kiểu giao diện mà họ thích, trong React, bạn sẽ có thể tiếp cận 1 trong 2 cách dùng CSS-in-JS hoặc dùng CSS variable (tất nhiên không có hỗ trợ với IE)

Nếu dùng CSS-in-JS bạn sẽ có thể làm được nhiều thứ hơn, bạn có một bộ công cụ đủ các đạo cụ để mua mai trong JS. Bài viết này sẽ chỉ ra tại sao bạn nên dùng CSS variable cho nhu cầu làm theme

Nếu dùng CSS-in-JS, trong React bạn sẽ tổ chức một ThemeProvider bằng React Context, dùng hook useTheme để lấy giá trị trong ThemeProvider, sẽ như thế này

import * as React from 'react'
import styled from '@emotion/styled'
import {ThemeProvider} from 'emotion-theming'

const themes = {
  light: {
    colors: {
      primary: 'deeppink',
      background: 'white',
    },
  },
  dark: {
    colors: {
      primary: 'lightpink',
      background: 'black',
    },
  },
}

const PrimaryText = styled.div(({theme}) => ({
  padding: 20,
  color: theme.colors.primary,
  backgroundColor: theme.colors.background,
}))

function ThemeToggler({theme, onClick}) {
  const nextTheme = theme === 'light' ? 'dark' : 'light'
  return (
    <button onClick={() => onClick(nextTheme)}>
      Change to {nextTheme} mode
    </button>
  )
}

function App() {
  const [theme, setTheme] = React.useState('light')
  return (
    <ThemeProvider theme={themes[theme]}>
      <PrimaryText>This text is the primary color</PrimaryText>
      <ThemeToggler
        theme={theme}
        onClick={(nextTheme) => setTheme(nextTheme)}
      />
    </ThemeProvider>
  )
}

export default App

Nếu dùng CSS variable, chúng ta khai bao một bộ các biến cần dùng, rồi chèn thêm data-theme cho thẻ body

CSS

body[data-theme='light'] {
  --colors-primary: deeppink;
  --colors-background: white;
}
body[data-theme='dark'] {
  --colors-primary: lightpink;
  --colors-background: black;
}

Phần implement của React component lúc này sửa lại

import * as React from 'react'
import './css-vars.css'
import styled from '@emotion/styled'

const PrimaryText = styled.div({
  padding: 20,
  color: 'var(--colors-primary)',
  backgroundColor: 'var(--colors-background)',
})

function ThemeToggler() {
  const [theme, setTheme] = React.useState('light')
  const nextTheme = theme === 'light' ? 'dark' : 'light'
  React.useEffect(() => {
    document.body.dataset.theme = theme
  }, [theme])
  return (
    <button onClick={() => setTheme(nextTheme)}>
      Change to {nextTheme} mode
    </button>
  )
}

function App() {
  return (
    <div>
      <PrimaryText>This text is the primary color</PrimaryText>
      <ThemeToggler />
    </div>
  )
}

export default App

Thẳng thắn mà nói, cả 2 cách làm này đều cho kết quả như nhau về mặt trãi nghiệm sử dụng, dùng CSS-in-JS sẽ có chút cảm giác hơi quá đà kỹ thuật, từ chuyên ngành là over-engineering.

Về hiệu năng thì sao?

ThemeProvider

Profiling session showing everything rendered

CSS variable

Profiling session showing only one component rendered

Cũng không nhất thiết phải nhìn vào con số mili giây phải tốn cho việc render, vì simple này khá là bé. Bạn cứ hình dùng nếu một cây React Component với hàng trăm component con lồng ghép nhau, khi thay đổi giá trị trong ThemeProvider, tất cả những component đều bị render lại thì sẽ như thế nào? Việc dùng CSS variable sẽ mang lại hiệu quả hơn nhiều vì trình duyệt không phải làm quá nhiều thứ như cách 1.

Có một lý do mà mình cho là hơi ngụy biện khi khăng khăng đòi dùng JS-in-CSS theo mình đoán là các bạn thật sự chưa đủ tự tin cũng như "trình" để viết CSS hiện đại, bạn chuyên tâm nâng tầm JS của mình mà quên mất việc nâng tầm CSS, vón đã phát triển rất xa từ cái thời bạn dùng float

Use CSS Variables instead of React Context

Initializing...