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

next-themes notuje 22 miliony pobrań tygodniowo i od ponad roku nie dostał nowej wersji. Napisałem bezpośredni zamiennik, który naprawia błędy, dodaje cookie SSR i typowane motywy.

Opublikowano 2026-03-30, zaktualizowano 2026-09-24 · https://wrksz.dev/pl/blog/wrksz-themes

---

W marcu 2026 next-themes od roku nie miał nowego wydania (0.4.6, marzec 2025). Liczby z tamtego momentu:

- **22 mln** pobrań tygodniowo
- **44** otwarte zgłoszenia
- **17** pull requestów czekających na review

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](https://hostero.gg/pl) do Next.js 16 i React 19 next-themes zaczął wypisywać w konsoli:

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

[Otworzyłem pull requesta](https://github.com/pacocoursey/next-themes/pull/386), 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:

[@wrksz/themes](https://github.com/jakubwarkusz/themes)

**npm**

```bash
npm install @wrksz/themes
npm uninstall next-themes
```

**pnpm**

```bash
pnpm add @wrksz/themes
pnpm remove next-themes
```

**bun**

```bash
bun add @wrksz/themes
bun remove next-themes
```

```diff
-import { 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:

```tsx
<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:

```tsx
import { useTheme } from "@wrksz/themes/client";

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

const { theme, setTheme } = useTheme<AppTheme>();
setTheme("sepia");
//       ^^^^^^^ Argument of type '"sepia"' is not assignable to parameter of type '((current: ThemeSelection<AppTheme> | undefined) => ThemeSelection<AppTheme>) | ThemeSelection<AppTheme>'.
```

### 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:

```tsx
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:

```tsx
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:

```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;
}
```

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.

<details>
<summary>Mniejsze dodatki</summary>

- obsługa `sessionStorage`
- `storage: "none"` dla w pełni kontrolowanych motywów
- meta `theme-color` dla Safari i PWA
- storage `hybrid`: cookie plus synchronizacja między kartami przez `localStorage`

</details>

## Od tamtej pory

Post powstał przy wersji 0.7.9. Tak projekt doszedł do obecnego stanu:

- **2026-03-20** Pull request do next-themes: Poprawka ostrzeżenia o skrypcie w React 19. Nadal otwarty.
- **2026-03-21** 0.1.0 na npm: Osiem wydań pierwszego dnia, aż do 0.5.0.
- **2026-03-30** Ten post: Napisany przy wersji 0.7.9.
- **2026-04-23** 0.9.0: Storage `hybrid`, fabryka `createThemes` i `useThemeEffect`.
- **2026-07-07** 1.0.0: Paczka na npm mniejsza o 46% po rozdzieleniu deklaracji typów.
- **2026-08-03** 1.1.0: Pierwszy zewnętrzny kontrybutor, [@martijn00](https://github.com/martijn00), z przenośnym API dla SSR i klienta.
- **2026-09-20** 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](https://themes.wrksz.dev/docs). Przy przejściu z next-themes zacznij od [przewodnika migracji](https://themes.wrksz.dev/docs/migration).
