@wrksz/themes - dlaczego przepisałem next-themes od zera

·3 min

next-themes ma 22 miliony pobrań tygodniowo. Ostatni release? Ponad rok temu. 44 otwarte issue. 17 niezmergowanych pull requestów. Wyszedł React 19, a maintainer zniknął.

Klasyka open source.

Problem#

Kiedy zacząłem migrować Hostero na Next.js 16 i React 19, bardzo szybko wyszło na jaw, że next-themes po prostu przestał działać poprawnie.

Pierwsze, co przywitało mnie w konsoli:

code
Encountered a script tag while rendering React component.
Scripts inside React components are never executed when rendering on the client.

React 19 przestał tolerować tagi <script> wewnątrz Client Components. next-themes robi dokładnie to. I nikt nie planował tego naprawić. Sam otworzyłem pull request, spojrzałem na aktywność w repo i tego samego dnia zdecydowałem, że zbuduję to od zera. Tak powstał @wrksz/themes.

To nie jedyny problem. Przy React 19 cacheComponents motywy mogą się „zamrażać” na przestarzałej wartości, bo next-themes używa zwykłego useState zamiast useSyncExternalStore. W produkcyjnych buildach z minifikacją nazw funkcji dostajesz ReferenceError: __name is not defined. I tak dalej.

Mógłbym sforkować, nałożyć patche i opublikować jako next-themes-maintained-fixed-... (wymyśl nazwę), ale cały kod to ~300 linii. Szkoda nie zrobić tego porządnie od zera.

Co zbudowałem#

Zamiennik drop-in dla next-themes. Migracja to jedna zmiana importu:

bash
npm install @wrksz/themes
npm uninstall next-themes
tsx
// wcześniej
import { ThemeProvider } from "next-themes";

// potem
import { ThemeProvider } from "@wrksz/themes/next";

Identyczne API. Reszta działa tak samo.

Co naprawiłem#

Każdy znany bug w next-themes:

Ostrzeżenie o skrypcie w React 19 - zamiast renderować <script> w Client Component, używam useServerInsertedHTML, żeby wstrzyknąć skrypt poza drzewem Reacta. Zero ostrzeżeń.

Przestarzały motyw przy cacheComponents - useSyncExternalStore z per-instance store zamiast globalnego singletona. Zawsze aktualna wartość, nawet gdy React wznawia zawieszone poddrzewo.

Bug minifikacji __name - naprawiony.

Motywy z wieloma klasami - next-themes zostawia stare klasy w DOM przy przełączaniu motywów typu value={{ dark: "dark high-contrast" }}. Naprawione przez flatMap + split przed usuwaniem klas.

Co nowego#

To największa zmiana. Przy storage="cookie" provider w Next.js automatycznie czyta cookie po stronie serwera. Klasa na <html> jest poprawna od pierwszego bajtu HTML, bez boilerplate:

tsx
<ThemeProvider storage="cookie" defaultTheme="dark">
  {children}
</ThemeProvider>

Koniec z migotaniem przy pierwszym renderze. Koniec z hackami.

Generyczne typy#

Pełne bezpieczeństwo TypeScript dla własnych motywów:

tsx
type AppTheme = "light" | "dark" | "high-contrast";

const { theme, setTheme } = useTheme<AppTheme>();

Zagnieżdżone providery#

Każdy provider ma niezależny store. Możesz mieć różne motywy w różnych sekcjach aplikacji jednocześnie, przydatne w bibliotekach komponentów, embedach lub izolowanych fragmentach UI.

ThemedImage#

Komponent rozwiązujący problem hydration mismatch dla obrazów zależnych od motywu:

tsx
<ThemedImage
    src={{ light: "/logo-light.png", dark: "/logo-dark.png" }}
    alt="Logo"
/>

useThemeValue#

Hook pomocniczy do mapowania motywu na dowolną wartość:

tsx
const label = useThemeValue({
    light: "Switch to dark",
    dark: "Switch to light",
});

Dostęp po stronie serwera#

getTheme() odczytuje aktualny motyw w Server Components, layoutach, server actions i middleware, bez zależności od Reacta:

tsx
import { NextResponse } from "next/server";
import { getTheme } from "@wrksz/themes/next";

export function proxy(request: Request) {
  const theme = getTheme(request, { defaultTheme: "dark" });
  const response = NextResponse.next();
  response.headers.set("x-theme", theme);
  return response;
}

Reszta#

  • obsługa sessionStorage
  • storage: "none" dla w pełni kontrolowanych motywów
  • meta theme-color dla Safari i PWA
  • integracja z Tailwind CSS v4 dark mode out of the box

Instalacja#

bash
npm install @wrksz/themes

Pełna dokumentacja i przewodnik migracji na themes.wrksz.dev. Jeśli przechodzisz z next-themes, migration guide obejmuje wszystko.