An open API service indexing awesome lists of open source software.

https://github.com/doeixd/effect-atom-jsx

Effect-native fine-grained JSX runtime with Layer-powered services, async atoms, and optimistic actions.
https://github.com/doeixd/effect-atom-jsx

dom-expressions effect effect-ts jsx optimistic-ui reactivity signals typescript

Last synced: 3 months ago
JSON representation

Effect-native fine-grained JSX runtime with Layer-powered services, async atoms, and optimistic actions.

Awesome Lists containing this project

README

          

[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/doeixd/effect-atom-jsx)

# effect-atom-jsx

Fine-grained reactive JSX runtime powered by Effect v4. Combines **effect-atom style state management**, a **dom-expressions JSX runtime**, and **Effect v4 service integration** into a single, cohesive framework.

```bash
npm i effect-atom-jsx effect@^4.0.0-beta.29
```

> Targets `effect@^4.0.0-beta.29`

## Overview

```
effect-atom-jsx = Effect v4 services + Atom/Registry state + dom-expressions JSX
```

- **Local state** via `Atom` / `AtomRef` — reactive graph primitives (`Registry` available for advanced/manual control)
- **Async state** via `defineQuery` / `atomEffect` / `Atom.fromResource` — Effect fibers with automatic cancellation
- **Mutations** via `defineMutation` / `createOptimistic` — optimistic UI with rollback
- **Testing** via `renderWithLayer` / `withTestLayer` / `mockService` — DOM-free test harness
- **Form validation** via `AtomSchema` — Schema-driven reactive fields with touched/dirty tracking
- **SSR** via `renderToString` / `hydrateRoot` — server-side rendering with hydration
- **Debug** via `AtomLogger` — structured logging for atom reads/writes

## Quick Start

### 1. Configure Babel

```json
{
"plugins": [
["babel-plugin-jsx-dom-expressions", {
"moduleName": "effect-atom-jsx",
"contextToCustomElements": true
}]
]
}
```

### 2. Write a component

Components are plain functions that run once. Reactive expressions in JSX update only the specific DOM nodes that depend on them.

```tsx
import { Atom, render } from "effect-atom-jsx";

function Counter() {
const count = Atom.make(0);
const doubled = Atom.make((get) => get(count) * 2);

return (


Count: {count()} (doubled: {doubled()})


count.update((c) => c + 1)}>+

);
}

render(() => , document.getElementById("root")!);

// Vite HMR helper (optional):
// const hot = (import.meta as ImportMeta & { hot?: ViteHotContext }).hot;
// renderWithHMR(() => , document.getElementById("root")!, hot);
```

### 3. Add Effect services

```tsx
import { Effect, Layer, ServiceMap } from "effect";
import { createMount, useService, defineQuery, Async } from "effect-atom-jsx";

const Api = ServiceMap.Service<{
readonly load: () => Effect.Effect;
}>("Api");

const ApiLive = Layer.succeed(Api, {
load: () => Effect.succeed(42),
});

function App() {
const data = defineQuery(() => useService(Api).load(), { name: "app-data" });

return (

Loading...

}
success={(value) =>

Loaded: {value}

}
/>
);
}

const mountApp = createMount(ApiLive);
mountApp(() => , document.getElementById("root")!);
```

## Core Concepts

### Term Map (In Context)

- `Atom`: core reactive unit; callable read (`count()`) plus write methods on writable atoms (`set`/`update`/`modify`).
- `derived atom`: read-only atom computed from other atoms (`Atom.make((get) => ...)` or `Atom.derived(...)`).
- `Query`: reactive async read (`defineQuery`), returns a `QueryRef` with `result`, `pending`, `latest`, `effect`, `invalidate`.
- `Mutation`: callback-style async write (`defineMutation`), returns handle with `run`, `effect`, `result`, `pending`.
- `Action`: linear runtime-bound write (`Atom.runtime(layer).action(...)`), preferred service mutation path.
- `Result`: primary async state model (`Loading` / `Refreshing` / `Success` / `Failure` / `Defect`).
- `Effect` (capital E): typed effect program from the `effect` package (`Effect`).
- `effect(...)` methods (lowercase): bridge helpers that expose state handles as `Effect` programs (`query.effect()`, `mutation.effect(input)`, `action.effect(input)`).
- `Ref` (`AtomRef`): object/collection-focused reactive state with property-level access (`ref.prop("x")`) and callable reads (`ref()`).
- `Optimistic`: temporary overlay on top of a source value (`createOptimistic`) used for immediate UI before async confirmation.
- `Store`: not a separate top-level primitive in this package; use `AtomRef` or `Atom.projection(...)` for object/draft-style state.

### Type Architecture (A / E / R)

If you use Effect heavily, this is the key model:

- `A` = success value type
- `E` = typed error channel
- `R` = required services/context

`Effect` values always carry all three: `Effect`.

```tsx
import { Effect, Schedule } from "effect";
import { Atom, Async } from "effect-atom-jsx";

// Effect
const usersEffect = Effect.gen(function* () {
const api = yield* Api;
return yield* api.listUsers();
});

const apiRuntime = Atom.runtime(ApiLive);

// Runtime binding satisfies R (Api), so resulting atom is runtime-bound.
const users = apiRuntime.atom(usersEffect);

// You can annotate with public aliases when you want explicitness:
// const users: Atom.AsyncAtom = apiRuntime.atom(usersEffect);

// users() -> Result
// users.effect() -> Effect

// Dependency-aware runtime atom composition
const profile = apiRuntime.atom((get) =>
Effect.gen(function* () {
const xs = yield* get.result(users);
return xs.length;
}),
);
```

`Atom.runtime(layer)` accepts any effect whose requirements are a subset of the runtime layer output (`RReq extends R`).

How this appears in UI:

```tsx
} // e includes your typed E (e.g. HttpError)
success={(xs) => }
/>
```

Writable vs read-only state:

- Writable atoms (`Atom.make(value)`, `Atom.value(value)`) expose `set`/`update`/`modify`.
- Derived atoms (`Atom.make((get) => ...)`, `Atom.derived(...)`) are read-only.

### Golden Path (Current)

For most apps, start with this stack:

- Local state: `Atom.make` / `Atom.value` / `Atom.derived`
- Service/runtime wiring: `Atom.runtime(layer)` for service-bound atoms/actions (preferred)
- Ambient runtime alternative: `createMount(layer)` + `useService(Tag)`
- Async reads: `defineQuery(...)`
- Writes: `Atom.runtime(...).action(...)` (primary) or `defineMutation(...)` (callback alternative)
- Optimistic UX: `createOptimistic(...)`
- Async UI rendering: `Async`, `Loading`, `Errored`

For runtime-bound atom APIs, prefer:

- `Atom.runtime(layer).atom(...)` for reads
- `Atom.runtime(layer).action(...)` for writes (linear Effect flow)
- `Atom.effect(...)` for standalone async atoms

Batching uses microtask mode by default. Use `flush()` when you need immediate deterministic commit ordering.

Everything else (`scoped*` constructors, explicit registries outside components, deep runtime helpers) is advanced.

### Atom & Registry — Local State

Atoms are reactive values. Most component code uses callable atoms directly. `Registry` is for advanced/manual control.

```ts
import { Effect } from "effect";
import { Atom } from "effect-atom-jsx";
import * as Registry from "effect-atom-jsx/Registry";

const count = Atom.make(0);
const doubled = Atom.map(count, (n) => n * 2);
const callback = Atom.value((n: number) => n + 1);

// Callable atoms are the default read/write path in components
count.set(3);
console.log(doubled()); // 6

// Atom also exposes Effect-based helpers
Effect.runSync(Atom.update(count, (n) => n + 1));
```

All Effect helpers (`get`, `set`, `update`, `modify`) support both data-first and data-last (pipeable) forms.

`Registry` remains available for advanced/manual control. `Registry.useRegistry()` returns an ambient registry scoped to the current reactive owner (component/root) and auto-disposes it on cleanup. For explicit standalone usage (tests, scripts, server handlers), use `Registry.make()`.

`useService(...)` diagnostics include actionable mount/layer guidance and best-effort available-service hints when a service is missing.

`Atom.make(...)` disambiguation:

- `Atom.make(value)` -> writable atom
- `Atom.make((get) => ...)` -> derived read-only atom
- `Atom.value(value)` -> explicit writable atom (including function values)
- `Atom.derived((get) => ...)` -> explicit derived atom

### Runtime-Bound Atoms (Primary Service Pattern)

```tsx
import { Effect, Layer, ServiceMap } from "effect";
import { Atom, Async, For, isPending, latest, Show } from "effect-atom-jsx";

const Api = ServiceMap.Service<{
readonly listUsers: () => Effect.Effect>;
readonly addUser: (name: string) => Effect.Effect;
}>("Api");

const ApiLive = Layer.succeed(Api, {
listUsers: () => Effect.succeed([{ id: "1", name: "Alice" }, { id: "2", name: "Bob" }]),
addUser: (_name: string) => Effect.void,
});

const apiRuntime = Atom.runtime(ApiLive);

const users = Atom.withReactivity(
apiRuntime.atom(
Effect.gen(function* () {
const api = yield* Api;
return yield* api.listUsers();
}),
),
["users"],
);

const addUser = apiRuntime.action(
Effect.fn(function* (name: string) {
const api = yield* Api;
yield* api.addUser(name);
}),
{
name: "add-user",
reactivityKeys: ["users"],
onTransition: ({ phase }) => {
if (phase === "failure" || phase === "defect") {
console.warn("add-user failed");
}
},
},
);

// Typed composition path
const addUserProgram = addUser.effect("Charlie");
const usersProgram = users.effect();

function UsersView() {
const refreshing = isPending(users);
const latestUsers = latest(users);
return (
<>

Refreshing...




{(xs) =>

Showing {xs().length} cached users while revalidating.

}

Loading...

}
success={(xs) => (
    {(u) =>
  • {u().name}
  • }

)}
/>
addUser("Charlie")}>Add
>
);
}
```

`isPending(resultAccessor)` returns `Accessor` and is true only during `Refreshing`.
`latest(resultAccessor)` returns `Accessor
` with the last successful value.

How this flow maps to concepts:

- `users` is an async atom (query-like read) whose value is `Result`.
- `addUser` is an action (write) that runs an `Effect` and invalidates logical reactivity keys.
- `Async` handles first load; `isPending` + `latest` handle stale-while-revalidate updates.
- `users.effect()` / `addUser.effect(...)` are composition bridges when you need pure `Effect` programs.

### Atom.family with Eviction

Use `Atom.family` for keyed atom factories. Entries are cached by key until explicitly evicted.

```ts
const userAtom = Atom.family((id: string) =>
apiRuntime.atom(
Effect.gen(function* () {
const api = yield* Api;
return yield* api.findUser(id);
}),
),
);

const a = userAtom("user-1");
const b = userAtom("user-2");

userAtom.evict("user-1"); // remove one cached entry
userAtom.clear(); // remove all cached entries

// In components, evict key-scoped entries on unmount when appropriate
import { onCleanup } from "effect-atom-jsx/advanced";
function UserCard(props: { id: string }) {
const user = userAtom(props.id);
onCleanup(() => userAtom.evict(props.id));
return (

Loading user...
}
success={(u) =>
{u.name}
}
/>
);
}
```

In long-running SPAs, use `evict`/`clear` to avoid unbounded family cache growth.
`Atom.family` also supports multiple key parts (`family((a, b) => ...)` with `evict(a, b)`).
For structural keys, pass custom equality: `Atom.family(factory, { equals: (a, b) => ... })`.

### AtomRef — Object State

`AtomRef` provides per-property reactive access to objects and arrays.

```ts
import { AtomRef } from "effect-atom-jsx";

const todo = AtomRef.make({ title: "Write docs", done: false });
const title = todo.prop("title");

console.log(todo()); // { title: "Write docs", done: false }
console.log(title()); // "Write docs"
title.set("Ship release notes");
console.log(title()); // "Ship release notes"

// Collections for arrays
const list = AtomRef.collection([
{ id: 1, text: "Buy milk" },
{ id: 2, text: "Write tests" },
]);
list.push({ id: 3, text: "Deploy" });
console.log(list.toArray().length); // 3
```

`todo.prop("title")` returns an `AtomRef` (not an `Atom` directly). Primary read style is callable (`title()`).
For atom-graph interop (`Atom.map`, etc.), use `AtomRef.toAtom(title)`.

```ts
const titleAtom = AtomRef.toAtom(title);
const upper = Atom.map(titleAtom, (s) => s.toUpperCase());

const titleQuery = defineQuery(() => Effect.succeed(titleAtom()), { name: "title" });
```

`get.result(...)` expects an atom carrying `Result`/`FetchResult`; use `AtomRef.toAtom(...)` for value-level interop first.

### Advanced: defineQuery / atomEffect / Result

Both create reactive async computations backed by Effect fibers. When tracked dependencies change, the previous fiber is interrupted and a new one starts.

```tsx
import { Effect } from "effect";
import { atomEffect, defineQuery, useService } from "effect-atom-jsx";
import { Result, Async } from "effect-atom-jsx/advanced";

// atomEffect — standalone, no runtime needed
const time = atomEffect(() =>
Effect.succeed(new Date().toISOString()).pipe(Effect.delay("1 second"))
);

// defineQuery — uses ambient Layer runtime from mount()
const data = defineQuery(() => useService(Api).load(), { name: "data" });

const users = defineQuery(() => useService(Api).listUsers(), {
name: "users",
retrySchedule: Schedule.exponential("1 second").pipe(Schedule.compose(Schedule.recurs(3))),
pollSchedule: Schedule.spaced("30 seconds"),
});

// Pattern-match on the result in JSX

Loading...

}
error={(e) =>

Error: {e.message}

}
success={(value) =>

{value}

}
/>
```

**Key difference:** `defineQuery` uses the ambient runtime injected by `mount()`, while `atomEffect` runs Effects directly (or accepts an explicit runtime parameter).

`defineQuery` supports Phase E scheduling/observability options:

- `retrySchedule`: retry typed failures before settling
- `pollSchedule`: periodic invalidation/polling via Effect schedule
- `onTransition` and `observe`: lightweight execution hooks for tracing/metrics

For ergonomic key + invalidation wiring, pass `query.key` into `defineMutation({ invalidates })`.

#### `Result` state mapping defaults

`Async` supports all `Result` states:

- `Loading` -> `loading()`
- `Refreshing(previous)` -> `refreshing(previous)` if provided, otherwise reuses the settled previous renderer
- `Success(value)` -> `success(value)`
- `Failure(error)` -> `error(error)` if provided, otherwise `null`
- `Defect(cause)` -> `defect(cause)` if provided, otherwise `null`

If you want defects or typed failures to escalate globally, leave local handlers undefined and use boundaries at higher levels.

### Advanced Compatibility: FetchResult

Use `Result` as the default async model. `FetchResult` is an advanced compatibility model.

| Type | Module | Used by | Purpose |
|------|--------|---------|---------|
| `Result
` | `effect-ts.ts` | Default | Unified async state (Loading / Refreshing / Success / Failure / Defect) |
| `FetchResult
` | `Result.ts` | Advanced compat | Data-fetching state (Initial / Success / Failure) with waiting flag |

Convert between them with `FetchResult.fromResult()` and `FetchResult.toResult()`.

Important: conversion is useful but not semantically identical in every state. `Result` carries explicit fiber-lifecycle states (`Loading`, `Refreshing`, `Defect`) while `FetchResult` models data-centric waiting semantics. Treat conversion as an interop bridge, not a one-to-one state machine equivalence.

For explicit non-suspense rendering, use `FetchResult.builder(...)`:

```tsx
const view = FetchResult.builder(FetchResult.fromResult(users()))
.onInitial(() => )
.onFailure((cause) => )
.onSuccess((data, { waiting }) => (
<>
{waiting && }
{(u) =>

  • {u().name}
  • }
    >
    ))
    .render();
    ```

    `Result` is **Exit-first internally** — each settled state (`Success`, `Failure`, `Defect`) carries a `.exit` field holding the canonical Effect `Exit`. This enables lossless round-trips and integration with Effect's error model. Combinators `Result.match`, `.map`, `.flatMap`, `.getOrElse`, and `.getOrThrow` are available for ergonomic pattern matching and transformation.

    ### Mutations: Linear First

    Prefer linear write flows with `Atom.runtime(layer).action(...)` when working with services.

    ```ts
    import { Effect } from "effect";
    import { Atom, createOptimistic } from "effect-atom-jsx";

    const optimisticUsers = createOptimistic(users);

    const addUser = apiRuntime.action(
    Effect.fn(function* (name: string) {
    optimisticUsers.set((prev) => [...prev, { id: "optimistic", name }]);
    const api = yield* Api;
    yield* api.addUser(name);
    }),
    {
    reactivityKeys: ["users"],
    onError: () => optimisticUsers.clear(),
    onSuccess: () => optimisticUsers.clear(),
    },
    );
    ```

    Use `defineMutation(...)` when you want callback-style lifecycle hooks.

    ```ts
    import { Effect } from "effect";
    import { Atom, createOptimistic, defineMutation } from "effect-atom-jsx";

    const savedCount = Atom.make(0);
    const optimistic = createOptimistic(savedCount);

    const save = defineMutation(
    (next: number) => Effect.succeed(next).pipe(Effect.delay("250 millis")),
    {
    optimistic: (next) => optimistic.set(next),
    rollback: () => optimistic.clear(),
    onSuccess: (next) => {
    optimistic.clear();
    savedCount.set(next);
    },
    },
    );

    // Typed composition path
    const saveProgram = save.effect(10);

    save.run(10);
    console.log(optimistic()); // 10 immediately
    ```

    ### defineMutation — Callback Alternative

    Composition summary:

    - `defineQuery(...).effect()` returns `Effect`
    - `defineMutation(...).effect(input)` returns `Effect`
    - `Atom.runtime(...).action(...).effect(input)` returns `Effect`
    - `Atom.runtime(...).action(...).runEffect(input)` returns `Effect
    ` (preserves action success value)
    - `Atom.result(atom)` converts result-like atoms into typed `Effect` values for pipelines

    `BridgeError` is tagged (`ResultLoadingError` | `ResultDefectError`) so composition errors stay explicit in the Effect error channel.

    ### AtomSchema — Form Validation

    Wraps atoms with Effect Schema for reactive validation with form state tracking.

    ```ts
    import { Schema, Effect, Option } from "effect";
    import { Atom, AtomSchema } from "effect-atom-jsx";

    const ageField = AtomSchema.makeInitial(Schema.Int, 25);

    // Each field provides reactive accessors
    ageField.value; // Atom> — parsed value
    ageField.error; // Atom> — validation error
    ageField.isValid; // Atom
    ageField.touched; // Atom — modified since creation?
    ageField.dirty; // Atom — differs from initial?

    // Write invalid input
    Effect.runSync(Atom.set(ageField.input, 1.5));
    Effect.runSync(Atom.get(ageField.isValid)); // false

    // Reset everything
    ageField.reset(); // restores initial value, clears touched

    const profile = AtomSchema.struct({
    age: AtomSchema.makeInitial(Schema.Int, 25),
    score: AtomSchema.makeInitial(Schema.Int, 10),
    });
    profile.isValid();
    profile.touch();
    profile.input.set({ age: 30, score: 11 });
    profile.values(); // Accessor>

    const address = AtomSchema.struct({
    city: AtomSchema.makeInitial(Schema.String, ""),
    zip: AtomSchema.makeInitial(Schema.Int, 12345),
    });
    const userForm = AtomSchema.struct({
    profile,
    address,
    });
    userForm.reset();
    ```

    ### AtomLogger — Debug Tracking

    Structured logging for atom reads and writes using Effect's Logger.

    ```ts
    import { Effect } from "effect";
    import { Atom, AtomLogger } from "effect-atom-jsx";

    const count = Atom.make(0);

    // Wrap to automatically log all reads/writes
    const traced = AtomLogger.tracedWritable(count, "count");
    // logs: atom:read { atom: "count", op: "read", value: "0" }
    // logs: atom:write { atom: "count", op: "write", value: "5" }

    // Effect-based logging
    Effect.runSync(AtomLogger.logGet(count, "count"));

    // Capture state snapshot
    const snap = Effect.runSync(
    AtomLogger.snapshot([["count", count], ["other", otherAtom]])
    );
    // { count: 0, other: "hello" }
    ```

    ### fromStream / fromQueue / fromSchedule — Streaming Atoms

    Create atoms whose values are continuously updated from Effect Streams or Queues.

    ```ts
    import { Stream, Queue, Effect, Schedule } from "effect";
    import { Atom } from "effect-atom-jsx";

    // Atom fed by a Stream — starts a fiber on first read
    const prices = Atom.fromStream(
    Stream.fromIterable([10, 20, 30]),
    0, // initial value
    );

    // Atom fed by a Queue
    const queue = Effect.runSync(Queue.unbounded());
    const messages = Atom.fromQueue(queue, "");

    // Atom fed by a Schedule (via Stream.fromSchedule)
    const ticks = Atom.fromSchedule(Schedule.recurs(3), 0 as any);

    // Stream recipe for UI text inputs (trim + length filtering)
    const rawInput = Stream.make(" hello ", " ", "x", " world ");
    const queryInput = Atom.Stream.textInput(rawInput, { minLength: 2 });

    // Search-box recipe (text normalization + optional dedupe)
    const searchTerms = Atom.Stream.searchInput(rawInput, {
    minLength: 2,
    lowercase: true,
    });

    // Both helpers return Effect Streams, so compose them into atoms.
    function SearchBox() {
    const [input, setInput] = createSignal("");
    const results = Atom.fromStream(
    Atom.Stream.searchInput(inputToStream(input), { minLength: 2, lowercase: true }),
    [] as ReadonlyArray,
    );
    return setInput((e.currentTarget as HTMLInputElement).value)} />;
    }
    ```

    ### Server-Side Rendering

    Render components to HTML strings on the server and hydrate on the client.

    ```ts
    import {
    renderToString, hydrateRoot, isServer,
    setRequestEvent, getRequestEvent,
    } from "effect-atom-jsx";
    import { Hydration, Atom } from "effect-atom-jsx";
    import * as Registry from "effect-atom-jsx/Registry";

    // ─── Server ─────────────────────────────────────────────────────
    setRequestEvent({ url: req.url, headers: req.headers });

    const html = renderToString(() => );

    // Serialize atom state for the client
    const registry = Registry.make();
    const state = Hydration.dehydrate(registry, [
    ["count", countAtom],
    ["user", userAtom],
    ]);

    res.send(`

    ${html}

    window.__STATE__ = ${JSON.stringify(state)}
    `);

    // ─── Client ─────────────────────────────────────────────────────
    // Restore atom state from server
    Hydration.hydrate(registry, window.__STATE__, {
    count: countAtom,
    user: userAtom,
    });

    // Optional validation hooks for development diagnostics:
    Hydration.hydrate(registry, window.__STATE__, { count: countAtom, user: userAtom }, {
    onUnknownKey: (key) => console.warn("Unknown hydration key:", key),
    onMissingKey: (key) => console.warn("Missing hydration key:", key),
    });

    // Attach reactivity to existing DOM
    const dispose = hydrateRoot(() => , document.getElementById("root")!);
    ```

    ## Control-Flow Components

    JSX components for declarative conditional and list rendering:

    | Component | Purpose | Example |
    |-----------|---------|---------|
    | `Show` | Conditional rendering | `

    Visible

    ` |
    | `For` | List rendering with keying | `{(item) =>
  • {item}
  • }` |
    | `Async` | Result pattern matching | `` |
    | `Loading` | Show content while loading | `` |
    | `Errored` | Show content on error | `{(e) =>

    {e}

    }` |
    | `Switch` / `Match` | Multi-case matching | `A...` |
    | `MatchTag` | Type-safe `_tag` matching | `` |
    | `Optional` | Render when value is truthy | `{(v) =>

    {v}

    }` |
    | `MatchOption` | Match Effect Option | ` ...} />` |
    | `Dynamic` | Dynamic component selection | `` |
    | `WithLayer` | Provide a Layer boundary | `...` |
    | `Frame` | Animation frame loop | `{() => }` |

    ## API Reference

    ### Namespace Modules

    Primary modules are available as top-level namespace imports; advanced modules like `Registry` are deep-imported:

    ```ts
    // Namespace import
    import { Atom, AtomRef, Result, Hydration } from "effect-atom-jsx";
    import { FetchResult } from "effect-atom-jsx"; // optional advanced compatibility
    import { AtomSchema, AtomLogger, AtomRpc, AtomHttpApi } from "effect-atom-jsx";

    // Deep imports
    import * as Atom from "effect-atom-jsx/Atom";
    import * as AtomSchema from "effect-atom-jsx/AtomSchema";
    import * as Registry from "effect-atom-jsx/Registry";
    ```

    | Module | Key Exports |
    |--------|-------------|
    | `Atom` | `make`, `readable`, `writable`, `family`, `map`, `withFallback`, `projection`, `projectionAsync`, `withReactivity`, `invalidateReactivity`, `keepAlive`, `runtime`, `action`, `effect`, `pull`, `Stream.*` (advanced OOO helpers), `searchParam`, `kvs`, `flush`, `get`, `set`, `update`, `modify`, `refresh`, `subscribe`, `fromStream`, `fromQueue`, `query` |
    | `AtomRef` | `make`, `collection` |
    | `Registry` | `make` (returns instance with `get`, `set`, `update`, `modify`, `mount`, `refresh`, `subscribe`, `reset`, `dispose`) |
    | `Result` | `loading`, `refreshing`, `success`, `failure`, `defect`, `match`, `map`, `flatMap`, `getOrElse`, `getOrThrow` |
    | `FetchResult` | `initial`, `success`, `failure`, `isInitial`, `isSuccess`, `isFailure`, `isWaiting`, `fromResult`, `toResult`, `map`, `flatMap`, `match`, `all` |
    | `Hydration` | `dehydrate`, `hydrate`, `toValues` |
    | `AtomSchema` | `make`, `makeInitial`, `path`, `HtmlInput` |
    | `AtomLogger` | `traced`, `tracedWritable`, `logGet`, `logSet`, `snapshot` |
    | `AtomRpc` | `Tag()` factory with `query`, `mutation`, `refresh` |
    | `AtomHttpApi` | `Tag()` factory with grouped `query`, `mutation`, `refresh` |

    ### Effect Integration

    ```ts
    import {
    defineQuery, createQueryKey, invalidate,
    isPending, latest,
    createOptimistic, defineMutation,
    useService, useServices, createMount, mount,
    } from "effect-atom-jsx";

    import {
    atomEffect,
    layerContext,
    scopedRootEffect,
    scopedQueryEffect,
    scopedMutationEffect,
    Result, Async,
    } from "effect-atom-jsx/advanced";
    ```

    ### Reactive Core (Internals / Advanced)

    ```ts
    import {
    createSignal, createEffect, createMemo, createRoot,
    createContext, useContext,
    onCleanup, onMount,
    untrack, sample, flush,
    mergeProps, splitProps,
    getOwner, runWithOwner,
    } from "effect-atom-jsx/advanced";
    ```

    `batch(...)` remains available for low-level runtime internals, but app code should rely on default microtask batching and use `flush()` only when deterministic sync ordering is required.

    Full API reference: [`docs/API.md`](docs/API.md)

    Dedicated Effect integration guide: [`docs/ACTION_EFFECT_USE_RESOURCE.md`](docs/ACTION_EFFECT_USE_RESOURCE.md)

    Effect-atom migration/equivalents guide: [`docs/EFFECT_ATOM_EQUIVALENTS.md`](docs/EFFECT_ATOM_EQUIVALENTS.md)

    Architecture decisions (in progress): `docs/adr/`

    ## Examples

    | Example | Location | What it shows |
    |---------|----------|---------------|
    | Counter | `examples/counter/` | Signals, atoms, Registry, async data with `atomEffect` |
    | Projection | `examples/projection/` | `Atom.projection` + `Atom.projectionAsync` with `Async` rendering |
    | OOO Async | `examples/ooo-async/` | `Atom.pull` + OOO chunk merge, rendered via `Async`, `Loading`, and `Errored` |
    | TodoMVC | `examples/todomvc/` | Full app with `defineQuery`, `defineMutation`, optimistic UI, service injection |
    | RPC & HTTP API | `examples/rpc-httpapi/` | `AtomRpc.Tag()`, `AtomHttpApi.Tag()`, `MatchTag` component |
    | Schema Form | `examples/schema-form/` | `AtomSchema` validation, touched/dirty/reset, `AtomLogger.snapshot` |
    | SSR | `examples/ssr/` | `renderToString`, `hydrateRoot`, `Hydration.dehydrate/hydrate` |

    ## How It Works

    1. **`Atom.runtime(layer)`** creates a runtime-bound API for reads (`runtime.atom`) and writes (`runtime.action`)
    2. Effects inside runtime-bound atoms/actions resolve services via Effect context (`yield* Api`) with requirements satisfied by the bound layer
    3. **`defineQuery()` / `atomEffect()`** run async effects reactively, exposing `Result` state
    4. **`defineMutation()`** remains the callback-style mutation alternative (optimistic/rollback hooks)
    5. Component lifetimes are scope-backed: mount/root and component boundaries map to Effect scopes so parent disposal interrupts descendant fibers transitively
    6. **`createMount(layer)` + `useService(Tag)`** remain the ambient-runtime alternative for simpler trees
    7. **`scopedRootEffect()` / `scopedQueryEffect()` / `scopedMutationEffect()`** are advanced Effect-first lifetime constructors
    8. Babel compiles JSX to **dom-expressions** helpers — reactivity updates only the affected DOM nodes

    ## Testing

    DOM-free test harness via `effect-atom-jsx/testing`:

    ```ts
    import { Effect } from "effect";
    import { Atom, defineQuery, defineMutation, useService } from "effect-atom-jsx";
    import { withTestLayer, renderWithLayer, mockService } from "effect-atom-jsx/testing";

    const ApiMock = mockService(Api, {
    load: () => Effect.succeed(42),
    save: (_n: number) => Effect.void,
    });

    // Option 1: runtime-first testing (primary)
    const testRuntime = Atom.runtime(ApiMock);
    const users = testRuntime.atom(
    Effect.gen(function* () {
    const api = yield* Api;
    return yield* api.load();
    }),
    );
    await Effect.runPromise(Atom.result(users));
    await testRuntime.dispose();

    // Option 2: withTestLayer — manual ambient runtime execution
    const harness = withTestLayer(ApiMock);
    const result = harness.run(() => defineQuery(() => useService(Api).load(), { name: "load" }));
    await harness.tick();
    await harness.dispose();

    // Option 3: renderWithLayer — runs UI immediately
    const harness2 = renderWithLayer(ApiMock, () => {
    const save = defineMutation((n: number) => useService(Api).save(n));
    save.run(42);
    });
    await harness2.tick();
    await harness2.dispose();
    ```

    > See [`docs/TESTING.md`](docs/TESTING.md) for the full testing guide.

    ## `flush()` Escape Hatch

    Microtask batching is the default. Use `flush()` only when imperative DOM work needs synchronous commit ordering.

    ```tsx
    import { Atom } from "effect-atom-jsx";

    function handleSubmit(button: HTMLButtonElement) {
    const submitted = Atom.make(false);
    submitted.set(true);
    Atom.flush();
    button.focus();
    }
    ```

    ## Relationship to `@effect-atom/atom`

    This project provides an effect-atom-like ergonomic surface, implemented natively for Effect v4.

    - **Same:** namespace-style API (`Atom`, `Result`, `Registry`, `AtomRef`), atom graph patterns, waiting/revalidation async model
    - **Different:** native implementation tuned for JSX + dom-expressions, targets Effect v4 beta (vs v3)
    - **Guidance:** if you already think in effect-atom terms, this API should feel familiar. Prefer `defineQuery` / `defineMutation` / `createMount` for Effect service integration.

    ## Compatibility

    - Runtime: Effect v4 beta (`effect@^4.0.0-beta.29`)
    - JSX: `dom-expressions` via `effect-atom-jsx/runtime`
    - Test: `npm test` / Typecheck: `npm run typecheck` / Build: `npm run build`