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:
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:
npm install @wrksz/themes
npm uninstall next-themes// 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#
Cookie storage z zero-flash SSR#
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:
<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:
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:
<ThemedImage
src={{ light: "/logo-light.png", dark: "/logo-dark.png" }}
alt="Logo"
/>useThemeValue#
Hook pomocniczy do mapowania motywu na dowolną wartość:
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:
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-colordla Safari i PWA - integracja z Tailwind CSS v4 dark mode out of the box
Instalacja#
npm install @wrksz/themesPełna dokumentacja i przewodnik migracji na themes.wrksz.dev. Jeśli przechodzisz z next-themes, migration guide obejmuje wszystko.