Skip to content

Commit 33d835d

Browse files
Merge pull request #44 from zeixcom/next
Version 1.3.1
2 parents 8f484d3 + 1c328df commit 33d835d

29 files changed

Lines changed: 661 additions & 104 deletions

.claude/skills/cause-effect-dev/references/api-facts.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ const value = createState<{ data: string } | never>({ data: '' })
1616
</type_constraint>
1717

1818
<core_functions>
19-
**`createScope(fn)`**
19+
**`createScope(fn, options?)`**
2020
- Returns a single `Cleanup` function
21-
- `fn` receives no arguments
22-
- `fn` may return an optional cleanup that runs when the scope is disposed
23-
- Used to group effects and control their lifetime
21+
- `fn` receives no arguments and may return an optional cleanup that runs when the scope is disposed
22+
- Used to group effects and control their shared lifetime
23+
- `options.root = true` (`ScopeOptions`) — suppresses parent-owner registration; the returned `dispose` is the sole teardown mechanism. Use for scopes whose lifecycle is controlled externally (e.g. a web component's `disconnectedCallback`)
2424

2525
**`createEffect(fn)`**
2626
- Returns a `Cleanup` function
@@ -40,7 +40,8 @@ const value = createState<{ data: string } | never>({ data: '' })
4040

4141
**`unown(fn)`**
4242
- Runs `fn` without registering cleanups in the current owner (nulls `activeOwner`)
43-
- Use in `connectedCallback` and similar DOM lifecycle hooks where the DOM — not the reactive graph — owns the element's lifetime
43+
- For creating a scope with an external lifecycle authority, prefer `createScope(fn, { root: true })` — it is equivalent to `unown(() => createScope(fn))` but more readable
44+
- Use `unown` directly when detaching non-scope computations from the current owner
4445
</core_functions>
4546

4647
<options>
@@ -91,7 +92,7 @@ Object.defineProperty(element, 'name', slot)
9192
<lifecycle_summary>
9293
| Function | Must be in owner? | Returns | Re-runs on dependency change? |
9394
|---|---|---|---|
94-
| `createScope(fn)` | No | `Cleanup` | No (fn runs once) |
95+
| `createScope(fn, options?)` | No | `Cleanup` | No (fn runs once) |
9596
| `createEffect(fn)` | **Yes** | `Cleanup` | Yes |
9697
| `createMemo(fn)` | No | `Memo<T>` | Lazily (on read) |
9798
| `createTask(fn)` | No | `Task<T>` | Yes (async) |

.claude/skills/cause-effect-dev/references/internal-types.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Two independent global pointers are maintained by `src/graph.ts`:
2929
- Set to the currently-running Effect or Scope
3030
- Any cleanup registered while `activeOwner` is set is attached to that owner
3131
- Nulled by `unown(fn)` — cleanups registered inside `fn` are not owned by the current context
32-
- Use `unown` in `connectedCallback` for nodes whose lifecycle is managed by the DOM, not by the reactive graph
32+
- For web component `connectedCallback`, prefer `createScope(fn, { root: true })` over `unown(() => createScope(fn))` — both suppress parent registration, but `{ root: true }` is clearer at the call site
3333
</global_pointers>
3434

3535
<ownership_vs_tracking>

.claude/skills/cause-effect/references/api-facts.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ const selected = createState<{ id: string } | { id: never }>({ id: '' })
1919
</type_constraint>
2020

2121
<core_functions>
22-
**`createScope(fn)`**
22+
**`createScope(fn, options?)`**
2323
- Returns a single `Cleanup` function
2424
- `fn` receives no arguments and may return an optional cleanup
2525
- Use to group effects and control their shared lifetime
26+
- `options.root = true` (`ScopeOptions`) — suppresses parent-owner registration; the returned `dispose` is the sole teardown mechanism. Use when an external authority (e.g. a web component's `disconnectedCallback`) manages the scope's lifetime
2627

2728
```typescript
2829
const dispose = createScope(() => {
@@ -67,16 +68,18 @@ createEffect(() => {
6768

6869
**`unown(fn)`**
6970
- Runs `fn` without registering cleanups in the current owner
70-
- Use in `connectedCallback` and similar DOM lifecycle methods where the DOM —
71-
not the reactive graph — manages the element's lifetime
71+
- For DOM-managed lifecycles involving a scope, prefer `createScope(fn, { root: true })` — it is equivalent to `unown(() => createScope(fn))` but more readable
72+
- Use `unown` directly when detaching a single computation (e.g. `createEffect`) rather than a full scope
7273

7374
```typescript
7475
connectedCallback() {
75-
// cleanup is tied to disconnectedCallback, not to a reactive owner
76-
this.#cleanup = unown(() => createEffect(() => this.render()))
76+
// preferred: createScope with root: true
77+
this.#dispose = createScope(() => {
78+
createEffect(() => this.render())
79+
}, { root: true })
7780
}
7881
disconnectedCallback() {
79-
this.#cleanup?.()
82+
this.#dispose?.()
8083
}
8184
```
8285
</core_functions>
@@ -194,7 +197,7 @@ Read all signals eagerly in the signals argument — not inside branches. See `n
194197
<lifecycle_summary>
195198
| Function | Requires owner? | Returns | Reactive? |
196199
|---|---|---|---|
197-
| `createScope(fn)` | No | `Cleanup` | No (fn runs once) |
200+
| `createScope(fn, options?)` | No | `Cleanup` | No (fn runs once) |
198201
| `createEffect(fn)` | **Yes** | `Cleanup` | Yes — re-runs on dependency change |
199202
| `createMemo(fn)` | No | `Memo<T>` | Lazy — recomputes on read if stale |
200203
| `createTask(fn)` | No | `Task<T>` | Yes — re-runs async on dependency change |

.claude/skills/cause-effect/references/error-classes.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,10 @@ const dispose = createScope(() => {
117117
})
118118
```
119119

120-
**Exception:** use `unown` when the DOM manages the element's lifetime (e.g. inside
121-
`connectedCallback`/`disconnectedCallback`) and you intentionally want to bypass owner
122-
registration.
120+
**Exception:** use `createScope(fn, { root: true })` when the DOM manages the element's
121+
lifetime (e.g. inside `connectedCallback`/`disconnectedCallback`) and you intentionally
122+
want to bypass owner registration. `unown(() => createScope(fn))` achieves the same effect
123+
but is the legacy pattern.
123124
</RequiredOwnerError>
124125

125126
<CircularDependencyError>

.claude/skills/cause-effect/references/signal-types.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ const results = createTask(async (prev, signal) => {
9595
- **Must be created inside an owner** (`createScope` or another effect) — throws `RequiredOwnerError` otherwise
9696
- Runs immediately on creation, then re-runs on dependency changes
9797
- Returns a `Cleanup` function; calling it disposes the effect and all its children
98-
- Use `unown` inside `connectedCallback` / `disconnectedCallback` when the DOM manages the element's lifetime
98+
- Use `createScope(fn, { root: true })` in `connectedCallback` for DOM-managed lifetimes — the returned `dispose` goes in `disconnectedCallback`; the scope is never silently disposed by a re-running outer effect
9999

100100
```typescript
101101
const dispose = createScope(() => {

.claude/skills/cause-effect/workflows/debug.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Read the specific section in references/non-obvious-behaviors.md or references/e
3131
If the symptom does not match a known gotcha, read references/api-facts.md to confirm:
3232
- Signal generics use `T extends {}` (no `null` or `undefined`)
3333
- `createEffect` is inside an owner (`createScope` or another effect)
34-
- `unown` is used only for DOM-owned lifecycles, not to bypass ownership generally
34+
- For DOM-owned lifecycles involving a scope: use `createScope(fn, { root: true })`, not `unown(() => createScope(fn))`. `unown` is still correct for non-scope cases (e.g. detaching a single `createEffect`)
3535
- `batch` is used if multiple state writes should coalesce
3636

3737
## Step 4: Inspect library source (last resort)

.claude/skills/cause-effect/workflows/use-api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Read references/signal-types.md. Match the task to the appropriate signal(s) usi
1313

1414
Before writing any `createEffect` call, confirm there is an active owner in scope:
1515
- Top-level effects must be wrapped in `createScope`
16-
- Effects in Web Component lifecycle methods that use DOM-managed lifetime should use `unown`
16+
- Effects in Web Component lifecycle methods that use DOM-managed lifetime: use `createScope(fn, { root: true })` — the returned `dispose` goes in `disconnectedCallback`. This suppresses parent-owner registration so the scope is never disposed by a re-running outer effect
1717

1818
See references/api-facts.md → `<core_functions>` for the rules.
1919

ARCHITECTURE.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,11 @@ Cleanup functions are stored on the `cleanup` field of owner nodes. The field is
170170

171171
`registerCleanup()` promotes from `null` → function → array as needed. `runCleanup()` executes all registered cleanups and resets the field to `null`.
172172

173-
### `createScope(fn)`
173+
### `createScope(fn, options?)`
174174

175-
Creates an ownership scope without an effect. The scope becomes `activeOwner` during `fn` execution. Returns a `dispose` function. If the scope is created inside another owner, its disposal is automatically registered on the parent.
175+
Creates an ownership scope without an effect. The scope becomes `activeOwner` during `fn` execution. Returns a `dispose` function. Unless `options.root` is `true`, the scope's disposal is automatically registered on the parent owner (if any).
176+
177+
`{ root: true }` (via `ScopeOptions`) suppresses that registration, making the returned `dispose` the sole mechanism for tearing down the scope. This is the correct pattern for any owner with an external lifecycle authority (e.g. a web component whose `disconnectedCallback` is the only teardown point) — without it, a scope created inside a re-runnable effect would be disposed on the next effect re-run.
176178

177179
## Signal Types
178180

@@ -358,3 +360,4 @@ The first-subscriber path is the key to `watched` lifecycle propagation: when an
358360
| `isEqual` placement | Implementation in `graph.ts`; public preset exported as `DEEP_EQUALITY` from `graph.ts` | `util.ts` (blocked by circular import: `errors.ts``util.ts`); keep in `list.ts` | `isEqual` needs `CircularDependencyError`, which lives in `errors.ts`; `errors.ts` already imports `util.ts`, so `util.ts` cannot import back. `graph.ts` already imports `CircularDependencyError` and is the correct home for all equality constants. |
359361
| `isEqual` public export | Deprecated alias re-exported from `index.ts` pointing to the implementation in `graph.ts` | Remove immediately | No known downstream consumers, but it was part of the public API — a deprecation cycle is the correct path to removal. |
360362
| Cycle detection in `isEqual` / `DEEP_EQUALITY` | No cycle detection — plain recursion, no `WeakSet` | (a) Keep `WeakSet` per call; (b) import `fast-deep-equal` or `dequal` | `WeakSet` allocation on every `List.set()` / `Store.set()` call is unnecessary overhead for the common case (plain JSON-like signal values). Circular signal data is a user bug; a stack overflow is an acceptable outcome. Importing an external package for a 20-line function contradicts the zero-dependency policy and bundle-size constraints. `DEEP_EQUALITY` has never shipped; deprecated `isEqual` has no known consumers — no major version required. |
363+
| `createScope` root option | `ScopeOptions { root?: boolean }` as second argument to `createScope` | Standalone `createRoot(fn)` export | One-line implementation difference; extending the existing function avoids adding a new export. `ScopeOptions` follows the `*Options` pattern used by every other creation function in the library (`SignalOptions`, `ListOptions`, `SensorOptions`, etc.). Positional boolean (`createScope(fn, true)`) was rejected: readable only with IDE hover support; an options object is self-documenting in code review. |

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 1.3.1
4+
5+
### Added
6+
- `createScope` now accepts an optional `ScopeOptions` second argument; `{ root: true }` creates a root scope that is not registered on the current parent owner – the returned `dispose` is the sole teardown mechanism. Export new `ScopeOptions` type.
7+
8+
### Changed
9+
- Improved type inference for `createList` and `createCollection` when providing a custom `createItem` factory (e.g. `createStore`). The generic type of the returned item signal is now properly inferred without requiring type assertions.
10+
311
## 1.3.0
412

513
### Added

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ These are easy to get wrong and not derivable from reading the source without pr
2929

3030
**`Slot` is a valid property descriptor** with `get`, `set`, `configurable`, and `enumerable` — pass directly to `Object.defineProperty()`. `slot.replace(nextSignal)` swaps the backing signal. Slot has no `update()` — it is a forwarding layer, not a mutable cell.
3131

32-
**`unown()` is the correct fix for DOM-owned component lifecycles.** `createScope` inside `connectedCallback` registers its `dispose` on the parent effect's cleanup list — disposing the component on the next parent re-run. Wrap with `unown(() => createScope(...))` to leave `disconnectedCallback` as the sole lifecycle authority.
32+
**`createScope(fn, { root: true })` is the correct fix for DOM-owned component lifecycles.** `createScope` inside `connectedCallback` registers its `dispose` on the parent effect's cleanup list — disposing the component on the next parent re-run. Pass `{ root: true }` (via `ScopeOptions`) to suppress that registration and leave `disconnectedCallback` as the sole lifecycle authority. `unown(() => createScope(...))` achieves the same effect but is the legacy pattern; `{ root: true }` is preferred because the intent is explicit at the call site. `unown()` remains useful for detaching arbitrary computations from the current owner when no new scope is needed.

0 commit comments

Comments
 (0)