W marcu 2026 next-themes od roku nie miał nowego wydania (0.4.6, marzec 2025). Liczby z tamtego momentu:
- pobrań tygodniowo
- 22 mln
- otwarte zgłoszenia
- 44
- pull requestów czekających na review
- 17
Nikt niczego nie mergował, a na nowszych wersjach Reacta i Next.js biblioteka zaczęła się sypać.
Co zepsuło się w React 19
Podczas migracji Hostero do Next.js 16 i React 19 next-themes zaczął wypisywać w konsoli:
Encountered a script tag while rendering React component.
Scripts inside React components are never executed when rendering on the client.Otworzyłem pull requesta, sprawdziłem aktywność w repozytorium i uznałem, że porządne przepisanie 300-linijkowej biblioteki ma więcej sensu niż utrzymywanie forka. Ostrzeżenie okazało się zresztą jednym z czterech problemów:
| Objaw | Przyczyna w next-themes | Poprawka w @wrksz/themes |
|---|---|---|
ostrzeżenie o <script> przy każdym renderze | blokujący skrypt renderuje się w Client Component | wstrzykiwany przez useServerInsertedHTML, poza drzewem komponentów |
motyw utyka na starej wartości przy włączonym cacheComponents | stan trzymany w useState | osobny store dla każdego providera, czytany przez useSyncExternalStore |
ReferenceError: __name is not defined w części buildów produkcyjnych | skrypt powstaje przez Function.toString(), więc helper __name z bundlera trafia do kodu, który działa bez niego | skrypt trafia do paczki jako gotowy string |
InvalidCharacterError przy value={{ dark: "dark high-contrast" }} | cała wartość trafia do classList jako jeden token | klasy dzielone przez flatMap przed dodaniem i usunięciem |
Bezpośredni zamiennik
@wrksz/themes zachowuje te same propsy i hooki, więc przejście to głównie instalacja i nowe importy:
npm install @wrksz/themes
npm uninstall next-themespnpm add @wrksz/themes
pnpm remove next-themesbun add @wrksz/themes
bun remove next-themesimport { ThemeProvider } from "next-themes";
import { useTheme } from "next-themes";
import { ThemeProvider } from "@wrksz/themes/next";
import { useTheme } from "@wrksz/themes/client"; Provider importujesz w layoucie, czyli w Server Component, a hooki w Client Components. Różni się jedna wartość domyślna: next-themes ustawia atrybut data-theme, a @wrksz/themes klasę. Jeśli twój CSS opiera się na data-theme, dodaj attribute="data-theme".
Co dodaje
Cookie storage bez mignięcia
Przy storage="cookie" motyw zapisuje się w cookie, a skrypt providera odczytuje je, zanim przeglądarka narysuje pierwszą klatkę, więc strona nie mignie złym motywem. Serwer w ogóle nie czyta cookie, więc layout może zostać statyczny:
<ThemeProvider storage="cookie" defaultTheme="dark">
{children}
</ThemeProvider>Typowane motywy
Podajesz własny typ z listą motywów, a setTheme odrzuci każdą inną wartość już przy kompilacji:
import { useTheme } from "@wrksz/themes/client";
type AppTheme = "light" | "dark" | "high-contrast";
const { theme, setTheme } = useTheme<AppTheme>();
setTheme("sepia");
Zagnieżdżone providery
Każdy provider ma własny store, więc dwie części jednej strony mogą jednocześnie działać na różnych motywach, o ile każda dostanie własny target i storageKey. Przydaje się to w bibliotekach komponentów, embedach i izolowanych podglądach.
ThemedImage i useThemeValue
ThemedImage wybiera obrazek dla bieżącego motywu bez hydration mismatch:
import { ThemedImage } from "@wrksz/themes/client";
<ThemedImage
src={{ light: "/logo-light.png", dark: "/logo-dark.png" }}
alt="Logo"
/>useThemeValue robi to samo dla dowolnej wartości:
import { useThemeValue } from "@wrksz/themes/client";
const label = useThemeValue({
light: "Switch to dark",
dark: "Switch to light",
});Dostęp po stronie serwera
getTheme() czyta cookie z motywem bez wciągania Reacta. W proxy albo middleware przekazujesz request:
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;
}W Server Component, layoucie albo server action wywołujesz ją bez requestu, z await: await getTheme({ defaultTheme: "dark" }). Odczyt cookie sprawia, że strona renderuje się przy każdym żądaniu, zamiast być statyczna. Wynikiem może też być "system", więc zamień go na konkretny motyw, zanim użyjesz go jako klasy.
Mniejsze dodatki
- obsługa
sessionStorage storage: "none"dla w pełni kontrolowanych motywów- meta
theme-colordla Safari i PWA - storage
hybrid: cookie plus synchronizacja między kartami przezlocalStorage
Od tamtej pory
Post powstał przy wersji 0.7.9. Tak projekt doszedł do obecnego stanu:
Pull request do next-themes
Poprawka ostrzeżenia o skrypcie w React 19. Nadal otwarty.0.1.0 na npm
Osiem wydań pierwszego dnia, aż do 0.5.0.Ten post
Napisany przy wersji 0.7.9.0.9.0
Storagehybrid, fabrykacreateThemesiuseThemeEffect.1.0.0
Paczka na npm mniejsza o 46% po rozdzieleniu deklaracji typów.1.1.0
Pierwszy zewnętrzny kontrybutor, @martijn00, z przenośnym API dla SSR i klienta.2.0.0
Koniec niejawnego czytania cookie na serwerze pod Next.js 16.3 i wymagany TypeScript 5.9.
Dokumentacja
Pełne API jest na themes.wrksz.dev. Przy przejściu z next-themes zacznij od przewodnika migracji.