ホームページ >ウェブフロントエンド >jsチュートリアル >SSR React アプリケーションでのテーマのセットアップ

SSR React アプリケーションでのテーマのセットアップ

Patricia Arquette
Patricia Arquetteオリジナル
2025-01-06 00:56:38577ブラウズ

Setting Up Themes in SSR React Applications

ユーザーの好みにシームレスに適応し、明るいテーマ、暗いテーマ、システムベースのテーマを簡単に切り替えることができる Web サイトにアクセスするところを想像してみてください。

この記事は、React を使用した SSR に関するシリーズの続きです。基本的な記事では、本番環境にすぐに使える構成を検討し、高度なテクニックでは、ハイドレーション エラーなどの課題に取り組みました。ここで、SSR とシームレスに統合する堅牢なテーマ サポートを実装することで、さらに一歩進めます。

目次

  • テーマとSSR
  • 実装
    • 依存関係をインストールする
    • サーバー ビルドに Cookie を追加します
    • サーバーにテーマを適用します
    • クライアントでテーマを処理する
  • 結論

テーマとSSR

主な問題は、間違ったテーマの初期フラッシュ (FOIT)です。

基本的に、テーマは CSS 変数を変更するだけです。ほとんどの場合、次の 3 つのテーマを使用します:

  • Light: CSS 変数のデフォルトのセット。
  • Dark: の場合に適用されます。タグにはダーククラスが含まれています。
  • システム: (prefers-color-scheme: dark) を使用して、ユーザーのシステム設定に基づいて自動的に切り替えます。 テーマを暗いか明るいかを決定するためのメディア クエリ。

デフォルトでは、サーバーはライトテーマで HTML をレンダリングし、ブラウザに送信します。ユーザーがダーク テーマを好む場合、最初のページの読み込み時に目に見えるテーマの変更が表示され、ユーザー エクスペリエンスが中断されます。

この問題を解決するには、主に 2 つの方法があります:

  • を追加します。サーバー上の HTML にタグを追加し、クライアント上でクラスを動的に設定します。
  • Cookie を使用してユーザーのテーマ設定を保存し、サーバー上にクラスを設定します。

最初の解決策は、次のテーマ パッケージがどのように機能するかです (2025 年 1 月)。この記事では、Cookie ベースのアプローチを実装して、SSR アプリケーションでテーマをシームレスに処理できるようにします。

実装

テーマを実装するには、2 つの Cookie を使用します:

  1. serverTheme - 正しいクラスを に適用するために使用されます。タグ。
  2. clientTheme - ハイドレーション エラーを処理するために使用されます。

クライアントは常に両方の Cookie を設定し、サーバーが次のリクエストで適切なテーマを正しくレンダリングできるようにします。

このガイドは、下部にリンクされている前の記事 本番対応 SSR React アプリケーションの構築 で紹介した概念に基づいて構築されています。わかりやすくするために、共有定数と型はここでは作成されませんが、それらの実装はサンプル リポジトリで見つけることができます。

依存関係のインストール

Cookie の処理に必要なパッケージをインストールします:

pnpm add cookie js-cookie

js-cookie のインストール タイプ:

pnpm add -D @types/js-cookie

アプリで React-router を使用していない場合は、Cookie パッケージを devDependency として使用できます。

サーバー ビルドに Cookie を追加する

tsup 構成ファイルを更新します:

// ./tsup.config.ts
import { defineConfig } from 'tsup'

export default defineConfig({
  entry: ['server'],
  outDir: 'dist/server',
  target: 'node22',
  format: ['cjs'],
  clean: true,
  minify: true,
  external: ['lightningcss', 'esbuild', 'vite'],
  noExternal: [
    'express',
    'sirv',
    'compression',
    'cookie', // Include the cookie in the server build
  ],
})

サーバーにテーマを適用する

テーマ定数の定義

// ./server/constants.ts
export const CLIENT_THEME_COOKIE_KEY = 'clientTheme'
export const SERVER_THEME_COOKIE_KEY = 'serverTheme'

export enum Theme {
  Light = 'light',
  Dark = 'dark',
  System = 'system'
}

テーマクラスをタグに適用

正しいテーマ クラスを に適用するユーティリティ関数を作成します。 serverTheme cookie に基づくタグ:

// ./server/lib/applyServerTheme.ts
import { parse } from 'cookie'
import { Request } from 'express'
import { SERVER_THEME_COOKIE_KEY, Theme } from '../constants'

export function applyServerTheme(req: Request, html: string): string {
  const cookies = parse(req.headers.cookie || '')
  const theme = cookies?.[SERVER_THEME_COOKIE_KEY]

  if (theme === Theme.Dark) {
    return html.replace('<html lang="en">', `<html lang="en">



<h4>
  
  
  Retrieve the Client Theme Cookie
</h4>

<p>Create a utility function to retrieve the clientTheme cookie<br>
</p>

<pre class="brush:php;toolbar:false">// ./server/getClientTheme.ts
import { parse } from 'cookie'
import { Request } from 'express'
import { CLIENT_THEME_COOKIE_KEY, Theme } from '../constants'

export function getClientTheme(req: Request) {
  const cookies = parse(req.headers.cookie || '')

  return cookies?.[CLIENT_THEME_COOKIE_KEY] as Theme | undefined
}

テーマ設定用のサーバー構成を更新する

開発構成:

// ./server/dev.ts
import fs from 'fs'
import path from 'path'
import { Application } from 'express'
import { HTML_KEY } from './constants'
import { applyServerTheme } from './lib/applyServerTheme'
import { getClientTheme } from './lib/getClientTheme'

const HTML_PATH = path.resolve(process.cwd(), 'index.html')
const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'src/entry-server.tsx')

export async function setupDev(app: Application) {
  const vite = await (
    await import('vite')
  ).createServer({
    root: process.cwd(),
    server: { middlewareMode: true },
    appType: 'custom',
  })

  app.use(vite.middlewares)

  app.get('*', async (req, res, next) => {
    try {
      let html = fs.readFileSync(HTML_PATH, 'utf-8')
      html = await vite.transformIndexHtml(req.originalUrl, html)

      const { render } = await vite.ssrLoadModule(ENTRY_SERVER_PATH)
      // send Client Theme from cookie to render
      const appHtml = await render(getClientTheme(req))

      // Apply Server theme on template html
      html = applyServerTheme(req, html)
      html = html.replace(HTML_KEY, appHtml)

      res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
    } catch (e) {
      vite.ssrFixStacktrace(e as Error)
      console.error((e as Error).stack)
      next(e)
    }
  })
}

実稼働構成:

// ./server/prod.ts
import fs from 'fs'
import path from 'path'
import compression from 'compression'
import { Application } from 'express'
import sirv from 'sirv'
import { HTML_KEY } from './constants'
import { applyServerTheme } from './lib/applyServerTheme'
import { getClientTheme } from './lib/getClientTheme'

const CLIENT_PATH = path.resolve(process.cwd(), 'dist/client')
const HTML_PATH = path.resolve(process.cwd(), 'dist/client/index.html')
const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'dist/ssr/entry-server.js')

export async function setupProd(app: Application) {
  app.use(compression())
  app.use(sirv(CLIENT_PATH, { extensions: [] }))

  app.get('*', async (req, res, next) => {
    try {
      let html = fs.readFileSync(HTML_PATH, 'utf-8')

      const { render } = await import(ENTRY_SERVER_PATH)
      // send Client Theme from cookie to render
      const appHtml = await render(getClientTheme(req))

      // Apply Server theme on template html
      html = applyServerTheme(req, html)
      html = html.replace(HTML_KEY, appHtml)

      res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
    } catch (e) {
      console.error((e as Error).stack)
      next(e)
    }
  })
}

クライアント上でテーマを処理する

定数の定義

クライアントが使用する定数を複製するか、共有フォルダーに移動します

// ./src/constants.ts
export const SSR = import.meta.env.SSR

export const CLIENT_THEME_COOKIE_KEY = 'clientTheme'
export const SERVER_THEME_COOKIE_KEY = 'serverTheme'

export enum Theme {
  Light = 'light',
  Dark = 'dark',
  System = 'system',
}

テーマコンテキストの作成

React コンテキストを設定してテーマの状態を管理し、テーマ管理メソッドを提供します。

// ./src/theme/context.ts
import { createContext, useContext } from 'react'
import { Theme } from '../constants'

export type ThemeContextState = {
  theme: Theme
  setTheme: (theme: Theme) => void
}

export const ThemeContext = createContext<ThemeContextState>({
  theme: Theme.System,
  setTheme: () => null,
})

export const useThemeContext = () => useContext(ThemeContext)

テーマユーティリティの実装

// ./src/theme/lib.ts
import Cookies from 'js-cookie'
import { CLIENT_THEME_COOKIE_KEY, SERVER_THEME_COOKIE_KEY, SSR, Theme } from '../constants'

// Resolve the system theme using the `prefers-color-scheme` media query
export function resolveSystemTheme() {
  if (SSR) return Theme.Light
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.Dark : Theme.Light
}

// Update the theme cookies and set appropriate class to <html>
export function updateTheme(theme: Theme) {
  if (SSR) return

  const resolvedTheme = theme === Theme.System ? resolveSystemTheme() : theme

  Cookies.set(CLIENT_THEME_COOKIE_KEY, theme)
  Cookies.set(SERVER_THEME_COOKIE_KEY, resolvedTheme)

  window.document.documentElement.classList.toggle('dark', resolvedTheme === Theme.Dark)
}

// Get the default theme from cookies
export function getDefaultTheme(): Theme {
  if (SSR) return Theme.System
  const theme = (Cookies.get(CLIENT_THEME_COOKIE_KEY) as Theme) || Theme.System

  updateTheme(theme)
  return theme
}

テーマプロバイダーを作成する

// ./src/theme/Provider.tsx
import { PropsWithChildren, useState } from 'react'
import { Theme } from '../constants'
import { ThemeContext } from './context'
import { getDefaultTheme, updateTheme } from './lib'

type Props = PropsWithChildren & {
  defaultTheme?: Theme // Handle theme for SSR
}

export function ThemeProvider({ children, defaultTheme }: Props) {
  const [theme, setTheme] = useState<Theme>(defaultTheme || getDefaultTheme())

  const handleSetTheme = (theme: Theme) => {
    setTheme(theme)
    updateTheme(theme)
  }

  return <ThemeContext value={{ theme, setTheme: handleSetTheme }}>{children}</ThemeContext>
}
// ./src/theme/index.ts
export { ThemeProvider } from './Provider'
export { useThemeContext } from './context'

コンポーネントでテーマコンテキストを使用する

// ./src/App.tsx
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import Card from './Card'
import { Theme } from './constants'
import { ThemeProvider } from './theme'

import './App.css'

// Theme from Server Entry
type AppProps = {
  theme?: Theme
}

function App({ theme }: AppProps) {
  return (
    <ThemeProvider defaultTheme={theme}>
      <div>
        <a href="https://vite.dev" target="_blank" rel="noreferrer">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank" rel="noreferrer">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <Card />
      <p className="read-the-docs">Click on the Vite and React logos to learn more</p>
    </ThemeProvider>
  )
}

export default App

カードコンポーネントの作成

// ./src/Card.tsx
import { useState } from 'react'
import { Theme } from './constants'
import { useThemeContext } from './theme'

function Card() {
  const { theme, setTheme } = useThemeContext()
  const [count, setCount] = useState(0)

  return (
    <div className='card'>
      <button onClick={() => setCount((count) => count + 1)}>count is {count}</button>
      <p>
        Edit <code>src/App.tsx</code> and save to test HMR
      </p>
      <div>
        Themes:{' '}
        <select value={theme} onChange={(event) => setTheme(event.target.value as Theme)}>
          <option value={Theme.System}>System</option>
          <option value={Theme.Light}>Light</option>
          <option value={Theme.Dark}>Dark</option>
        </select>
      </div>
    </div>
  )
}

export default Card

ハイドレーションエラーを解決する

テーマをサーバーのレンダリング メソッドに渡し、サーバーで生成された HTML がクライアント側のレンダリングと一致することを確認します。

import { renderToString } from 'react-dom/server'
import App from './App'
import { Theme } from './constants'

export function render(theme: Theme) {
  return renderToString(<App theme={theme} />)
}

スタイルの追加

:root {
    color: #242424;
    background-color: rgba(255, 255, 255, 0.87);
}

:root.dark {
    color: rgba(255, 255, 255, 0.87);
    background-color: #242424;
}

結論

この記事では、SSR React アプリケーションにシームレスなテーマを実装するという課題に取り組みました。 Cookie を使用し、クライアント側とサーバー側の両方のロジックを統合することにより、ハイドレーション エラーやユーザー エクスペリエンスを中断することなく、明るいテーマ、暗いテーマ、およびシステムベースのテーマをサポートする堅牢なシステムを作成しました。

コードを探索する

  • : 反応-ssr-テーマ-例
  • SSR で着陸: プロフェッショナル着陸

関連記事

これは、React を使用した SSR に関するシリーズの一部です。他の記事もお楽しみに!

  • 本番環境に対応した SSR React アプリケーションを構築する
  • ストリーミングおよび動的データを使用した高度な React SSR テクニック
  • SSR React アプリケーションでのテーマのセットアップ

つながりを保つ

フィードバック、コラボレーション、技術的なアイデアについての議論はいつでも受け付けています。お気軽にご連絡ください。

  • ポートフォリオ: maxh1t.xyz
  • メール: m4xh17@gmail.com

以上がSSR React アプリケーションでのテーマのセットアップの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。