Skip to content
Open
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
17 changes: 17 additions & 0 deletions .changeset/set-solid2-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@solid-primitives/set": 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.

## New Exports

- **`union(a, b)`** — reactive `Accessor<ReadonlySet<T>>` of all elements in either set.
- **`intersection(a, b)`** — reactive `Accessor<ReadonlySet<T>>` of elements present in both sets.
- **`difference(a, b)`** — reactive `Accessor<ReadonlySet<T>>` of elements in `a` not in `b`.
- **`symmetricDifference(a, b)`** — reactive `Accessor<ReadonlySet<T>>` of elements in either set but not both.
- **`readonlySet(set)`** — casts a `ReactiveSet` to `ReadonlySet` (type-only, zero runtime cost).
136 changes: 103 additions & 33 deletions packages/set/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,17 @@
[![version](https://img.shields.io/npm/v/@solid-primitives/set?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/set)
[![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-2.json)](https://github.com/solidjs-community/solid-primitives#contribution-process)

The Javascript built-in `Set` & `WeakSet` data structures as a reactive signals.

- **[`ReactiveSet`](#reactiveset)** - A reactive version of a Javascript built-in `Set` class.
- **[`ReactiveWeakSet`](#reactiveweakset)** - A reactive version of a Javascript built-in `WeakSet` class.
Reactive `Set` and `WeakSet` primitives, plus a suite of derived set-algebra operations.

| Export | Type | Description |
|---|---|---|
| [`ReactiveSet`](#reactiveset) | `class` | Drop-in reactive replacement for `Set` |
| [`ReactiveWeakSet`](#reactiveweakset) | `class` | Drop-in reactive replacement for `WeakSet` |
| [`union`](#union) | `function` | Elements in `a` or `b` |
| [`intersection`](#intersection) | `function` | Elements in both `a` and `b` |
| [`difference`](#difference) | `function` | Elements in `a` not in `b` |
| [`symmetricDifference`](#symmetricdifference) | `function` | Elements in `a` or `b`, but not both |
| [`readonlySet`](#readonlyset) | `function` | Cast a `ReactiveSet` to `ReadonlySet` |

## Installation

Expand All @@ -25,58 +32,121 @@ pnpm add @solid-primitives/set

## `ReactiveSet`

A reactive version of a Javascript built-in `Set` class.

### How to use it

#### Import
A drop-in reactive replacement for the built-in `Set` class. All reads — `has()`, `size`, iteration — are reactive. All writes — `add()`, `delete()`, `clear()` — notify only the affected subscribers.

```ts
import { ReactiveSet } from "@solid-primitives/set";
```

#### Basic usage

```ts
const set = new ReactiveSet([1, 1, 2, 3]);
const set = new ReactiveSet([1, 2, 3]);

// listen for changes reactively
createEffect(() => {
[...set]; // => [1,2,3] (reactive on any change)
set.has(2); // => true (reactive on change to the result)
});
// reads inside a reactive context track changes automatically
createEffect(
() => [...set],
values => console.log(values), // re-runs whenever the set contents change
);
createEffect(
() => set.has(2),
exists => console.log(exists), // re-runs only when the presence of 2 changes
);

// apply like with normal Set
// mutate like a normal Set
set.add(4);
set.delete(2);
set.clear();
```

`has()` tracks at the key level — adding or removing an unrelated element will not re-run a `has()` subscriber.

## `ReactiveWeakSet`

A reactive version of a Javascript built-in `WeakSet` class.
A drop-in reactive replacement for `WeakSet`. Only `has()` is reactive; there is no size or iteration (matching the `WeakSet` contract).

```ts
import { ReactiveWeakSet } from "@solid-primitives/set";

const set = new ReactiveWeakSet<object>();

createEffect(
() => set.has(myObj),
present => console.log(present),
);

### How to use it
set.add(myObj);
set.delete(myObj);
```

#### Import
## Set algebra operations

All four operations accept any `ReadonlySet<T>` — pass a `ReactiveSet` and the derived value re-computes automatically whenever the input changes.

```ts
import { ReactiveWeakSet } from "@solid-primitives/set";
import { union, intersection, difference, symmetricDifference } from "@solid-primitives/set";

const a = new ReactiveSet([1, 2, 3]);
const b = new ReactiveSet([2, 3, 4]);
```

#### Basic usage
Each operation must be called inside a reactive owner (a component, `createRoot`, or similar) because it creates a `createMemo` internally.

### `union`

Elements that appear in `a`, `b`, or both.

```ts
const set = new ReactiveWeakSet([1, 1, 2, 3]);
const u = union(a, b);
u(); // => Set {1, 2, 3, 4}

// listen for changes reactively
createEffect(() => {
set.has(2); // => true (reactive on change to the result)
});
a.add(5);
// after flush:
u(); // => Set {1, 2, 3, 4, 5}
```

// apply changes like with normal Set
set.add(4);
set.delete(2);
### `intersection`

Elements that appear in both `a` and `b`.

```ts
const i = intersection(a, b);
i(); // => Set {2, 3}
```

### `difference`

Elements in `a` that do not appear in `b`.

```ts
const d = difference(a, b);
d(); // => Set {1}
```

### `symmetricDifference`

Elements in `a` or `b`, but not both.

```ts
const s = symmetricDifference(a, b);
s(); // => Set {1, 4}
```

## `readonlySet`

Casts a `ReactiveSet` to `ReadonlySet` at the type level. No runtime cost — returns the same instance. Useful for exposing an internal set from a primitive without allowing callers to mutate it directly.

```ts
import { readonlySet } from "@solid-primitives/set";

function createTodoList() {
const _todos = new ReactiveSet<string>();
return {
todos: readonlySet(_todos),
add(todo: string) { _todos.add(todo); },
remove(todo: string) { _todos.delete(todo); },
};
}

const list = createTodoList();
list.todos.has("buy milk"); // ok
list.todos.add("buy milk"); // TypeScript error
```

## Changelog
Expand Down
22 changes: 17 additions & 5 deletions packages/set/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@solid-primitives/set",
"version": "0.7.3",
"description": "The Set & WeakSet data structures as a reactive signals.",
"description": "Reactive Set and WeakSet primitives with set-algebra operations: union, intersection, difference, and symmetricDifference.",
"author": "Damian Tarnawski @thetarnav <gthetarnav@gmail.com>",
"license": "MIT",
"homepage": "https://primitives.solidjs.community/package/set",
Expand All @@ -17,13 +17,23 @@
"stage": 2,
"list": [
"ReactiveSet",
"ReactiveWeakSet"
"ReactiveWeakSet",
"union",
"intersection",
"difference",
"symmetricDifference",
"readonlySet"
],
"category": "Reactivity"
},
"keywords": [
"solid",
"primitives"
"primitives",
"set",
"reactive",
"union",
"intersection",
"difference"
],
"private": false,
"sideEffects": false,
Expand All @@ -49,13 +59,15 @@
"test:ssr": "pnpm run vitest --mode ssr"
},
"peerDependencies": {
"solid-js": "^1.6.12"
"@solidjs/web": "^2.0.0-beta.13",
"solid-js": "^2.0.0-beta.13"
},
"dependencies": {
"@solid-primitives/trigger": "workspace:^"
},
"typesVersions": {},
"devDependencies": {
"solid-js": "^1.9.7"
"@solidjs/web": "2.0.0-beta.13",
"solid-js": "2.0.0-beta.13"
}
}
91 changes: 75 additions & 16 deletions packages/set/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Accessor, batch } from "solid-js";
import { type Accessor, createMemo } from "solid-js";
import { TriggerCache } from "@solid-primitives/trigger";

const $KEYS = Symbol("track-keys");
Expand Down Expand Up @@ -65,10 +65,8 @@ export class ReactiveSet<T> extends Set<T> {
add(value: T): this {
if (!super.has(value)) {
super.add(value);
batch(() => {
this.#triggers.dirty(value);
this.#triggers.dirty($KEYS);
});
this.#triggers.dirty(value);
this.#triggers.dirty($KEYS);
}

return this;
Expand All @@ -78,24 +76,20 @@ export class ReactiveSet<T> extends Set<T> {
const result = super.delete(value);

if (result) {
batch(() => {
this.#triggers.dirty(value);
this.#triggers.dirty($KEYS);
});
this.#triggers.dirty(value);
this.#triggers.dirty($KEYS);
}

return result;
}

clear(): void {
if (!super.size) return;
batch(() => {
this.#triggers.dirty($KEYS);
for (const member of super.values()) {
this.#triggers.dirty(member);
}
super.clear();
});
this.#triggers.dirty($KEYS);
for (const member of super.values()) {
this.#triggers.dirty(member);
}
super.clear();
}
}

Expand Down Expand Up @@ -154,3 +148,68 @@ export function createSet<T>(initial?: T[]): SignalSet<T> {
export function createWeakSet<T extends object>(initial?: T[]): ReactiveWeakSet<T> {
return new ReactiveWeakSet(initial);
}

/**
* Reactive union — elements that appear in `a`, `b`, or both.
* Re-derives when either input changes.
*/
export function union<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): Accessor<ReadonlySet<T>> {
return createMemo(() => {
const result = new Set<T>(a);
for (const v of b) result.add(v);
return result;
});
}

/**
* Reactive intersection — elements that appear in both `a` and `b`.
* Re-derives when either input changes.
*/
export function intersection<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): Accessor<ReadonlySet<T>> {
return createMemo(() => {
const result = new Set<T>();
for (const v of a) {
if (b.has(v)) result.add(v);
}
return result;
});
}

/**
* Reactive difference — elements in `a` that do not appear in `b`.
* Re-derives when either input changes.
*/
export function difference<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): Accessor<ReadonlySet<T>> {
return createMemo(() => {
const result = new Set<T>();
for (const v of a) {
if (!b.has(v)) result.add(v);
}
return result;
});
}

/**
* Reactive symmetric difference — elements in `a` or `b`, but not both.
* Re-derives when either input changes.
*/
export function symmetricDifference<T>(
a: ReadonlySet<T>,
b: ReadonlySet<T>,
): Accessor<ReadonlySet<T>> {
return createMemo(() => {
const result = new Set<T>(a);
for (const v of b) {
if (result.has(v)) result.delete(v);
else result.add(v);
}
return result;
});
}

/**
* Casts a `ReactiveSet` to `ReadonlySet` to prevent callers from mutating it.
*/
export function readonlySet<T>(set: ReactiveSet<T>): ReadonlySet<T> {
return set;
}
Loading