Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/cookies-solid2-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@solid-primitives/cookies": major
---

Migrate to Solid.js v2.0 (beta.13)

## Breaking Changes

**Peer dependencies**: `solid-js@^2.0.0-beta.13` and `@solidjs/web@^2.0.0-beta.13` are now required.

- `isServer` and `getRequestEvent` are now imported from `@solidjs/web` (were `solid-js/web`)
- `createEffect` follows the split compute/apply pattern required by Solid 2.0 — the internal cookie-sync effect now separates reactive tracking from the `document.cookie` write

## New

- Full test suite added: 19 browser tests and 6 SSR tests covering `parseCookie`, `getCookiesString`, `createServerCookie`, and `createUserTheme`
26 changes: 16 additions & 10 deletions packages/cookies/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@
[![version](https://img.shields.io/npm/v/@solid-primitives/cookies?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/cookies)
[![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-0.json)](https://github.com/solidjs-community/solid-primitives#contribution-process)

A set of primitives for handling cookies in solid
Reactive, signal-based cookie primitives for isomorphic use — readable and writable on both the server and the client.

- [`createServerCookie`](#createservercookie) - Provides a getter and setter for a reactive cookie, which works isomorphically.
- [`createUserTheme`](#createusertheme) - Creates a Server Cookie providing a type safe way to store a theme and access it on the server or client.
- [`getCookiesString`](#getCookiesString) - A primitive that allows for the cookie string to be accessed isomorphically on the client, or on the server
This package provides **higher-order** cookie functionality: typed reactive signals that stay in sync with `document.cookie`, with built-in SSR support via `getRequestEvent`. It is intentionally narrow in scope.

> **Looking for raw cookie read/write?** [`@solid-primitives/storage`](../storage) exposes `cookieStorage` — a `localStorage`-compatible API (`getItem`, `setItem`, `removeItem`, `key`) that works on both client and server, including full support for cookie attributes such as `domain`, `path`, `secure`, `sameSite`, and `maxAge`. Use that package when you need direct, imperative access to the cookie store.

- [`createServerCookie`](#createservercookie) - Reactive signal backed by a named cookie; isomorphic on client and server.
- [`createUserTheme`](#createusertheme) - Type-safe `"light" | "dark"` theme signal stored as a cookie.
- [`getCookiesString`](#getcookiesstring) - Returns the raw cookie string from `document.cookie` on the client or the request `Cookie` header on the server.

## Installation

Expand All @@ -24,38 +28,41 @@ yarn add @solid-primitives/cookies
pnpm add @solid-primitives/cookies
```

Requires `solid-js@^2.0.0-beta.13` and `@solidjs/web@^2.0.0-beta.13` as peer dependencies.

## How to use it

## `createServerCookie`

A primitive for creating a cookie that can be accessed isomorphically on the client, or the server.
Creates a reactive signal whose value is persisted to a named cookie. Reading the signal works on both the server (via the request `Cookie` header) and the client (via `document.cookie`). Writing the signal on the client automatically syncs the new value back to `document.cookie`.

```ts
import { createServerCookie } from "@solid-primitives/cookies";

const [cookie, setCookie] = createServerCookie("cookieName");

cookie(); // => string | undefined
setCookie("newValue");
```

### Custom serialization

Custom cookie serializers and deserializers can also be implemented
Pass `deserialize` and `serialize` functions to store non-string values.

```ts
import { createServerCookie } from "@solid-primitives/cookies";

const [serverCookie, setServerCookie] = createServerCookie("coolCookie", {
deserialize: str => (str ? str.split(" ") : []), // Deserializes cookie into a string[]
serialize: val => (val ? val.join(" ") : ""), // serializes the value back into a string
serialize: val => (val ? val.join(" ") : ""), // Serializes the value back into a string
});

serverCookie(); // => string[]
```

## `createUserTheme`

Composes `createServerCookie` to provide a type safe way to store a theme and access it on the server or client.
Composes `createServerCookie` to provide a type-safe `"light" | "dark"` theme signal. Any unrecognized cookie value is treated as `undefined` (or the provided `defaultValue`).

```ts
import { createUserTheme } from "@solid-primitives/cookies";
Expand All @@ -74,8 +81,7 @@ theme(); // => "light" | "dark"

## `getCookiesString`

A primitive that allows for the cookie string to be accessed isomorphically on the client, or on the server.
Uses `getRequestEvent` on the server and `document.cookie` on the client.
Returns the raw cookie string for the current environment — `document.cookie` in the browser, or the `Cookie` request header on the server. Use it together with `parseCookie` when you need to read a cookie value outside of a reactive context.

```ts
import { getCookiesString, parseCookie } from "@solid-primitives/cookies";
Expand Down
8 changes: 5 additions & 3 deletions packages/cookies/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@solid-primitives/cookies",
"version": "0.0.3",
"description": "A set of primitives for handling cookies in solid",
"description": "Reactive, signal-based cookie primitives for isomorphic use on client and server",
"author": "Thomas Beer (https://github.com/Tommypop2)",
"contributors": [
"Damian Tarnawski <gthetarnav@gmail.com>",
Expand Down Expand Up @@ -56,9 +56,11 @@
"test:ssr": "pnpm run vitest --mode ssr"
},
"peerDependencies": {
"solid-js": "^1.8.18"
"@solidjs/web": "^2.0.0-beta.13",
"solid-js": "^2.0.0-beta.13"
},
"devDependencies": {
"solid-js": "^1.9.7"
"@solidjs/web": "2.0.0-beta.13",
"solid-js": "2.0.0-beta.13"
}
}
17 changes: 10 additions & 7 deletions packages/cookies/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createSignal, createEffect, type Signal } from "solid-js";
import { getRequestEvent, isServer } from "solid-js/web";
import { getRequestEvent, isServer } from "@solidjs/web";

const YEAR = 365 * 24 * 60 * 60;

Expand Down Expand Up @@ -66,13 +66,16 @@ export function createServerCookie<T>(
cookieMaxAge = YEAR,
} = options ?? {};

const [cookie, setCookie] = createSignal(deserialize(parseCookie(getCookiesString(), name)));
const [cookie, setCookie] = createSignal<T | undefined>(
deserialize(parseCookie(getCookiesString(), name)) as Exclude<T | undefined, Function>,
);

createEffect(p => {
const string = serialize(cookie());
if (p !== string) document.cookie = `${name}=${string};max-age=${cookieMaxAge}`;
return string;
});
createEffect(
() => serialize(cookie()),
(string, prev) => {
if (prev !== string) document.cookie = `${name}=${string};max-age=${cookieMaxAge}`;
},
);

return [cookie, setCookie];
}
Expand Down
206 changes: 206 additions & 0 deletions packages/cookies/src/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { beforeEach, describe, expect, it } from "vitest";
import { createRoot, flush } from "solid-js";
import { parseCookie, getCookiesString, createServerCookie, createUserTheme } from "../src/index.js";

beforeEach(() => {
document.cookie.split(";").forEach(c => {
const key = c.split("=")[0]!.trim();
if (key) document.cookie = `${key}=;max-age=0`;
});
});

// ── parseCookie ───────────────────────────────────────────────────────────────

describe("parseCookie", () => {
it("extracts a value by key", () => {
expect(parseCookie("foo=bar", "foo")).toBe("bar");
});

it("extracts a value from multiple cookies", () => {
expect(parseCookie("foo=bar; baz=qux", "baz")).toBe("qux");
});

it("returns undefined for a missing key", () => {
expect(parseCookie("foo=bar", "missing")).toBeUndefined();
});

it("returns undefined for an empty cookie string", () => {
expect(parseCookie("", "foo")).toBeUndefined();
});
});

// ── getCookiesString ──────────────────────────────────────────────────────────

describe("getCookiesString", () => {
it("returns document.cookie on the client", () => {
document.cookie = "ck_test=hello";
expect(getCookiesString()).toContain("ck_test=hello");
});
});

// ── createServerCookie ────────────────────────────────────────────────────────

describe("createServerCookie", () => {
it("initializes signal from the current cookie value", () => {
document.cookie = "ck_init=world";
const dispose = createRoot(dispose => {
const [cookie] = createServerCookie("ck_init");
expect(cookie()).toBe("world");
return dispose;
});
dispose();
});

it("initializes to undefined when the cookie is absent", () => {
const dispose = createRoot(dispose => {
const [cookie] = createServerCookie("ck_absent");
expect(cookie()).toBeUndefined();
return dispose;
});
dispose();
});

it("setter updates the signal value", () => {
document.cookie = "ck_set=before";
const { setCookie, dispose } = createRoot(dispose => {
const [, setCookie] = createServerCookie("ck_set");
return { setCookie, dispose };
});
flush();
setCookie("after");
flush();
expect(parseCookie(document.cookie, "ck_set")).toBe("after");
dispose();
});

it("writes to document.cookie when value changes", () => {
document.cookie = "ck_write=old";
const { setCookie, dispose } = createRoot(dispose => {
const [, setCookie] = createServerCookie("ck_write");
return { setCookie, dispose };
});
flush();
setCookie("new");
flush();
expect(parseCookie(document.cookie, "ck_write")).toBe("new");
dispose();
});

it("does not overwrite the cookie when the serialized value is unchanged", () => {
document.cookie = "ck_same=abc";
const { cookie, setCookie, dispose } = createRoot(dispose => {
const [cookie, setCookie] = createServerCookie("ck_same");
return { cookie, setCookie, dispose };
});
flush();
setCookie("abc");
flush();
expect(cookie()).toBe("abc");
expect(parseCookie(document.cookie, "ck_same")).toBe("abc");
dispose();
});

it("applies a custom deserialize function", () => {
document.cookie = "ck_deser=42";
const dispose = createRoot(dispose => {
const [count] = createServerCookie("ck_deser", {
deserialize: str => (str !== undefined ? parseInt(str, 10) : 0),
serialize: String,
});
expect(count()).toBe(42);
return dispose;
});
dispose();
});

it("applies a custom serialize function", () => {
const { setTags, dispose } = createRoot(dispose => {
const [, setTags] = createServerCookie<string[]>("ck_ser", {
deserialize: str => (str ? str.split(",") : []),
serialize: val => val.join(","),
});
return { setTags, dispose };
});
flush();
setTags(["a", "b", "c"]);
flush();
expect(parseCookie(document.cookie, "ck_ser")).toBe("a,b,c");
dispose();
});

it("respects a custom cookieMaxAge option", () => {
const { setCookie, dispose } = createRoot(dispose => {
const [, setCookie] = createServerCookie("ck_age", { cookieMaxAge: 60 });
return { setCookie, dispose };
});
flush();
setCookie("x");
flush();
expect(parseCookie(document.cookie, "ck_age")).toBe("x");
dispose();
});
});

// ── createUserTheme ───────────────────────────────────────────────────────────

describe("createUserTheme", () => {
it("reads a stored theme from the cookie", () => {
document.cookie = "ck_theme=dark";
const dispose = createRoot(dispose => {
const [theme] = createUserTheme("ck_theme");
expect(theme()).toBe("dark");
return dispose;
});
dispose();
});

it("returns undefined for an unrecognized theme value", () => {
document.cookie = "ck_theme2=blue";
const dispose = createRoot(dispose => {
const [theme] = createUserTheme("ck_theme2");
expect(theme()).toBeUndefined();
return dispose;
});
dispose();
});

it("returns undefined when no cookie is set and no default is provided", () => {
const dispose = createRoot(dispose => {
const [theme] = createUserTheme("ck_theme3");
expect(theme()).toBeUndefined();
return dispose;
});
dispose();
});

it("uses the defaultValue when cookie is absent", () => {
const dispose = createRoot(dispose => {
const [theme] = createUserTheme("ck_theme4", { defaultValue: "light" });
expect(theme()).toBe("light");
return dispose;
});
dispose();
});

it("accepts 'light' and 'dark' as valid values", () => {
document.cookie = "ck_theme5=light";
const dispose = createRoot(dispose => {
const [theme] = createUserTheme("ck_theme5");
expect(theme()).toBe("light");
return dispose;
});
dispose();
});

it("persists a theme change to document.cookie", () => {
const { setTheme, dispose } = createRoot(dispose => {
const [, setTheme] = createUserTheme("ck_theme6");
return { setTheme, dispose };
});
flush();
setTheme("dark");
flush();
expect(parseCookie(document.cookie, "ck_theme6")).toBe("dark");
dispose();
});
});
40 changes: 40 additions & 0 deletions packages/cookies/src/test/server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import { createRoot } from "solid-js";
import { getCookiesString, createServerCookie, createUserTheme } from "../src/index.js";

describe("SSR", () => {
it("getCookiesString returns a string without a request event", () => {
expect(typeof getCookiesString()).toBe("string");
});

it("getCookiesString returns an empty string when there is no request event", () => {
expect(getCookiesString()).toBe("");
});

it("createServerCookie initializes to undefined when no cookie header is present", () =>
createRoot(dispose => {
const [cookie] = createServerCookie("ssr_test");
expect(cookie()).toBeUndefined();
dispose();
}));

it("createServerCookie does not throw on the server", () =>
createRoot(dispose => {
expect(() => createServerCookie("ssr_safe")).not.toThrow();
dispose();
}));

it("createUserTheme returns undefined on the server without a cookie header", () =>
createRoot(dispose => {
const [theme] = createUserTheme("ssr_theme");
expect(theme()).toBeUndefined();
dispose();
}));

it("createUserTheme returns defaultValue on the server when no cookie header is present", () =>
createRoot(dispose => {
const [theme] = createUserTheme("ssr_theme2", { defaultValue: "light" });
expect(theme()).toBe("light");
dispose();
}));
});
Loading