{"id":47213610,"url":"https://github.com/doeixd/effect-atom-jsx","last_synced_at":"2026-03-13T15:50:08.353Z","repository":{"id":343102854,"uuid":"1176239723","full_name":"doeixd/effect-atom-jsx","owner":"doeixd","description":"Effect-native fine-grained JSX runtime with Layer-powered services, async atoms, and optimistic actions.","archived":false,"fork":false,"pushed_at":"2026-03-09T01:15:07.000Z","size":308,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-09T02:32:21.780Z","etag":null,"topics":["dom-expressions","effect","effect-ts","jsx","optimistic-ui","reactivity","signals","typescript"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/doeixd.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-08T19:59:26.000Z","updated_at":"2026-03-09T01:15:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/doeixd/effect-atom-jsx","commit_stats":null,"previous_names":["doeixd/effect-atom-jsx"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/doeixd/effect-atom-jsx","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doeixd%2Feffect-atom-jsx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doeixd%2Feffect-atom-jsx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doeixd%2Feffect-atom-jsx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doeixd%2Feffect-atom-jsx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/doeixd","download_url":"https://codeload.github.com/doeixd/effect-atom-jsx/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doeixd%2Feffect-atom-jsx/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30469498,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-13T11:00:43.441Z","status":"ssl_error","status_checked_at":"2026-03-13T11:00:23.173Z","response_time":60,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dom-expressions","effect","effect-ts","jsx","optimistic-ui","reactivity","signals","typescript"],"created_at":"2026-03-13T15:50:07.634Z","updated_at":"2026-03-13T15:50:08.344Z","avatar_url":"https://github.com/doeixd.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/doeixd/effect-atom-jsx)\n\n# effect-atom-jsx\n\nFine-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.\n\n```bash\nnpm i effect-atom-jsx effect@^4.0.0-beta.29\n```\n\n\u003e Targets `effect@^4.0.0-beta.29`\n\n## Overview\n\n```\neffect-atom-jsx = Effect v4 services + Atom/Registry state + dom-expressions JSX\n```\n\n- **Local state** via `Atom` / `AtomRef` — reactive graph primitives (`Registry` available for advanced/manual control)\n- **Async state** via `defineQuery` / `atomEffect` / `Atom.fromResource` — Effect fibers with automatic cancellation\n- **Mutations** via `defineMutation` / `createOptimistic` — optimistic UI with rollback\n- **Testing** via `renderWithLayer` / `withTestLayer` / `mockService` — DOM-free test harness\n- **Form validation** via `AtomSchema` — Schema-driven reactive fields with touched/dirty tracking\n- **SSR** via `renderToString` / `hydrateRoot` — server-side rendering with hydration\n- **Debug** via `AtomLogger` — structured logging for atom reads/writes\n\n## Quick Start\n\n### 1. Configure Babel\n\n```json\n{\n  \"plugins\": [\n    [\"babel-plugin-jsx-dom-expressions\", {\n      \"moduleName\": \"effect-atom-jsx\",\n      \"contextToCustomElements\": true\n    }]\n  ]\n}\n```\n\n### 2. Write a component\n\nComponents are plain functions that run once. Reactive expressions in JSX update only the specific DOM nodes that depend on them.\n\n```tsx\nimport { Atom, render } from \"effect-atom-jsx\";\n\nfunction Counter() {\n  const count = Atom.make(0);\n  const doubled = Atom.make((get) =\u003e get(count) * 2);\n\n  return (\n    \u003cdiv\u003e\n      \u003cp\u003eCount: {count()} (doubled: {doubled()})\u003c/p\u003e\n      \u003cbutton onClick={() =\u003e count.update((c) =\u003e c + 1)}\u003e+\u003c/button\u003e\n    \u003c/div\u003e\n  );\n}\n\nrender(() =\u003e \u003cCounter /\u003e, document.getElementById(\"root\")!);\n\n// Vite HMR helper (optional):\n// const hot = (import.meta as ImportMeta \u0026 { hot?: ViteHotContext }).hot;\n// renderWithHMR(() =\u003e \u003cCounter /\u003e, document.getElementById(\"root\")!, hot);\n```\n\n### 3. Add Effect services\n\n```tsx\nimport { Effect, Layer, ServiceMap } from \"effect\";\nimport { createMount, useService, defineQuery, Async } from \"effect-atom-jsx\";\n\nconst Api = ServiceMap.Service\u003c{\n  readonly load: () =\u003e Effect.Effect\u003cnumber\u003e;\n}\u003e(\"Api\");\n\nconst ApiLive = Layer.succeed(Api, {\n  load: () =\u003e Effect.succeed(42),\n});\n\nfunction App() {\n  const data = defineQuery(() =\u003e useService(Api).load(), { name: \"app-data\" });\n\n  return (\n    \u003cAsync\n      result={data.result()}\n      loading={() =\u003e \u003cp\u003eLoading...\u003c/p\u003e}\n      success={(value) =\u003e \u003cp\u003eLoaded: {value}\u003c/p\u003e}\n    /\u003e\n  );\n}\n\nconst mountApp = createMount(ApiLive);\nmountApp(() =\u003e \u003cApp /\u003e, document.getElementById(\"root\")!);\n```\n\n## Core Concepts\n\n### Term Map (In Context)\n\n- `Atom`: core reactive unit; callable read (`count()`) plus write methods on writable atoms (`set`/`update`/`modify`).\n- `derived atom`: read-only atom computed from other atoms (`Atom.make((get) =\u003e ...)` or `Atom.derived(...)`).\n- `Query`: reactive async read (`defineQuery`), returns a `QueryRef` with `result`, `pending`, `latest`, `effect`, `invalidate`.\n- `Mutation`: callback-style async write (`defineMutation`), returns handle with `run`, `effect`, `result`, `pending`.\n- `Action`: linear runtime-bound write (`Atom.runtime(layer).action(...)`), preferred service mutation path.\n- `Result`: primary async state model (`Loading` / `Refreshing` / `Success` / `Failure` / `Defect`).\n- `Effect` (capital E): typed effect program from the `effect` package (`Effect\u003cA, E, R\u003e`).\n- `effect(...)` methods (lowercase): bridge helpers that expose state handles as `Effect` programs (`query.effect()`, `mutation.effect(input)`, `action.effect(input)`).\n- `Ref` (`AtomRef`): object/collection-focused reactive state with property-level access (`ref.prop(\"x\")`) and callable reads (`ref()`).\n- `Optimistic`: temporary overlay on top of a source value (`createOptimistic`) used for immediate UI before async confirmation.\n- `Store`: not a separate top-level primitive in this package; use `AtomRef` or `Atom.projection(...)` for object/draft-style state.\n\n### Type Architecture (A / E / R)\n\nIf you use Effect heavily, this is the key model:\n\n- `A` = success value type\n- `E` = typed error channel\n- `R` = required services/context\n\n`Effect` values always carry all three: `Effect\u003cA, E, R\u003e`.\n\n```tsx\nimport { Effect, Schedule } from \"effect\";\nimport { Atom, Async } from \"effect-atom-jsx\";\n\n// Effect\u003cUser[], HttpError, Api\u003e\nconst usersEffect = Effect.gen(function* () {\n  const api = yield* Api;\n  return yield* api.listUsers();\n});\n\nconst apiRuntime = Atom.runtime(ApiLive);\n\n// Runtime binding satisfies R (Api), so resulting atom is runtime-bound.\nconst users = apiRuntime.atom(usersEffect);\n\n// You can annotate with public aliases when you want explicitness:\n// const users: Atom.AsyncAtom\u003cUser[], HttpError\u003e = apiRuntime.atom(usersEffect);\n\n// users() -\u003e Result\u003cUser[], HttpError\u003e\n// users.effect() -\u003e Effect\u003cUser[], HttpError | BridgeError\u003e\n\n// Dependency-aware runtime atom composition\nconst profile = apiRuntime.atom((get) =\u003e\n  Effect.gen(function* () {\n    const xs = yield* get.result(users);\n    return xs.length;\n  }),\n);\n```\n\n`Atom.runtime(layer)` accepts any effect whose requirements are a subset of the runtime layer output (`RReq extends R`).\n\nHow this appears in UI:\n\n```tsx\n\u003cAsync\n  result={users()}\n  error={(e) =\u003e \u003cErrorView error={e} /\u003e} // e includes your typed E (e.g. HttpError)\n  success={(xs) =\u003e \u003cUserList users={xs} /\u003e}\n/\u003e\n```\n\nWritable vs read-only state:\n\n- Writable atoms (`Atom.make(value)`, `Atom.value(value)`) expose `set`/`update`/`modify`.\n- Derived atoms (`Atom.make((get) =\u003e ...)`, `Atom.derived(...)`) are read-only.\n\n### Golden Path (Current)\n\nFor most apps, start with this stack:\n\n- Local state: `Atom.make` / `Atom.value` / `Atom.derived`\n- Service/runtime wiring: `Atom.runtime(layer)` for service-bound atoms/actions (preferred)\n- Ambient runtime alternative: `createMount(layer)` + `useService(Tag)`\n- Async reads: `defineQuery(...)`\n- Writes: `Atom.runtime(...).action(...)` (primary) or `defineMutation(...)` (callback alternative)\n- Optimistic UX: `createOptimistic(...)`\n- Async UI rendering: `Async`, `Loading`, `Errored`\n\nFor runtime-bound atom APIs, prefer:\n\n- `Atom.runtime(layer).atom(...)` for reads\n- `Atom.runtime(layer).action(...)` for writes (linear Effect flow)\n- `Atom.effect(...)` for standalone async atoms\n\nBatching uses microtask mode by default. Use `flush()` when you need immediate deterministic commit ordering.\n\nEverything else (`scoped*` constructors, explicit registries outside components, deep runtime helpers) is advanced.\n\n### Atom \u0026 Registry — Local State\n\nAtoms are reactive values. Most component code uses callable atoms directly. `Registry` is for advanced/manual control.\n\n```ts\nimport { Effect } from \"effect\";\nimport { Atom } from \"effect-atom-jsx\";\nimport * as Registry from \"effect-atom-jsx/Registry\";\n\nconst count = Atom.make(0);\nconst doubled = Atom.map(count, (n) =\u003e n * 2);\nconst callback = Atom.value((n: number) =\u003e n + 1);\n\n// Callable atoms are the default read/write path in components\ncount.set(3);\nconsole.log(doubled()); // 6\n\n// Atom also exposes Effect-based helpers\nEffect.runSync(Atom.update(count, (n) =\u003e n + 1));\n```\n\nAll Effect helpers (`get`, `set`, `update`, `modify`) support both data-first and data-last (pipeable) forms.\n\n`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()`.\n\n`useService(...)` diagnostics include actionable mount/layer guidance and best-effort available-service hints when a service is missing.\n\n`Atom.make(...)` disambiguation:\n\n- `Atom.make(value)` -\u003e writable atom\n- `Atom.make((get) =\u003e ...)` -\u003e derived read-only atom\n- `Atom.value(value)` -\u003e explicit writable atom (including function values)\n- `Atom.derived((get) =\u003e ...)` -\u003e explicit derived atom\n\n### Runtime-Bound Atoms (Primary Service Pattern)\n\n```tsx\nimport { Effect, Layer, ServiceMap } from \"effect\";\nimport { Atom, Async, For, isPending, latest, Show } from \"effect-atom-jsx\";\n\nconst Api = ServiceMap.Service\u003c{\n  readonly listUsers: () =\u003e Effect.Effect\u003cReadonlyArray\u003c{ id: string; name: string }\u003e\u003e;\n  readonly addUser: (name: string) =\u003e Effect.Effect\u003cvoid\u003e;\n}\u003e(\"Api\");\n\nconst ApiLive = Layer.succeed(Api, {\n  listUsers: () =\u003e Effect.succeed([{ id: \"1\", name: \"Alice\" }, { id: \"2\", name: \"Bob\" }]),\n  addUser: (_name: string) =\u003e Effect.void,\n});\n\nconst apiRuntime = Atom.runtime(ApiLive);\n\nconst users = Atom.withReactivity(\n  apiRuntime.atom(\n    Effect.gen(function* () {\n      const api = yield* Api;\n      return yield* api.listUsers();\n    }),\n  ),\n  [\"users\"],\n);\n\nconst addUser = apiRuntime.action(\n  Effect.fn(function* (name: string) {\n    const api = yield* Api;\n    yield* api.addUser(name);\n  }),\n  {\n    name: \"add-user\",\n    reactivityKeys: [\"users\"],\n    onTransition: ({ phase }) =\u003e {\n      if (phase === \"failure\" || phase === \"defect\") {\n        console.warn(\"add-user failed\");\n      }\n    },\n  },\n);\n\n// Typed composition path\nconst addUserProgram = addUser.effect(\"Charlie\");\nconst usersProgram = users.effect();\n\nfunction UsersView() {\n  const refreshing = isPending(users);\n  const latestUsers = latest(users);\n  return (\n    \u003c\u003e\n      \u003cShow when={refreshing()}\u003e\n        \u003cp\u003eRefreshing...\u003c/p\u003e\n      \u003c/Show\u003e\n      \u003cShow when={latestUsers()}\u003e\n        {(xs) =\u003e \u003cp\u003eShowing {xs().length} cached users while revalidating.\u003c/p\u003e}\n      \u003c/Show\u003e\n      \u003cAsync\n        result={users()}\n        loading={() =\u003e \u003cp\u003eLoading...\u003c/p\u003e}\n        success={(xs) =\u003e (\n          \u003cul\u003e\u003cFor each={xs}\u003e{(u) =\u003e \u003cli\u003e{u().name}\u003c/li\u003e}\u003c/For\u003e\u003c/ul\u003e\n        )}\n      /\u003e\n      \u003cbutton onClick={() =\u003e addUser(\"Charlie\")}\u003eAdd\u003c/button\u003e\n    \u003c/\u003e\n  );\n}\n```\n\n`isPending(resultAccessor)` returns `Accessor\u003cboolean\u003e` and is true only during `Refreshing`.\n`latest(resultAccessor)` returns `Accessor\u003cA | undefined\u003e` with the last successful value.\n\nHow this flow maps to concepts:\n\n- `users` is an async atom (query-like read) whose value is `Result\u003cUser[], E\u003e`.\n- `addUser` is an action (write) that runs an `Effect` and invalidates logical reactivity keys.\n- `Async` handles first load; `isPending` + `latest` handle stale-while-revalidate updates.\n- `users.effect()` / `addUser.effect(...)` are composition bridges when you need pure `Effect` programs.\n\n### Atom.family with Eviction\n\nUse `Atom.family` for keyed atom factories. Entries are cached by key until explicitly evicted.\n\n```ts\nconst userAtom = Atom.family((id: string) =\u003e\n  apiRuntime.atom(\n    Effect.gen(function* () {\n      const api = yield* Api;\n      return yield* api.findUser(id);\n    }),\n  ),\n);\n\nconst a = userAtom(\"user-1\");\nconst b = userAtom(\"user-2\");\n\nuserAtom.evict(\"user-1\"); // remove one cached entry\nuserAtom.clear(); // remove all cached entries\n\n// In components, evict key-scoped entries on unmount when appropriate\nimport { onCleanup } from \"effect-atom-jsx/advanced\";\nfunction UserCard(props: { id: string }) {\n  const user = userAtom(props.id);\n  onCleanup(() =\u003e userAtom.evict(props.id));\n  return (\n    \u003cAsync\n      result={user()}\n      loading={() =\u003e \u003cdiv\u003eLoading user...\u003c/div\u003e}\n      success={(u) =\u003e \u003cdiv\u003e{u.name}\u003c/div\u003e}\n    /\u003e\n  );\n}\n```\n\nIn long-running SPAs, use `evict`/`clear` to avoid unbounded family cache growth.\n`Atom.family` also supports multiple key parts (`family((a, b) =\u003e ...)` with `evict(a, b)`).\nFor structural keys, pass custom equality: `Atom.family(factory, { equals: (a, b) =\u003e ... })`.\n\n### AtomRef — Object State\n\n`AtomRef` provides per-property reactive access to objects and arrays.\n\n```ts\nimport { AtomRef } from \"effect-atom-jsx\";\n\nconst todo = AtomRef.make({ title: \"Write docs\", done: false });\nconst title = todo.prop(\"title\");\n\nconsole.log(todo()); // { title: \"Write docs\", done: false }\nconsole.log(title()); // \"Write docs\"\ntitle.set(\"Ship release notes\");\nconsole.log(title()); // \"Ship release notes\"\n\n// Collections for arrays\nconst list = AtomRef.collection([\n  { id: 1, text: \"Buy milk\" },\n  { id: 2, text: \"Write tests\" },\n]);\nlist.push({ id: 3, text: \"Deploy\" });\nconsole.log(list.toArray().length); // 3\n```\n\n`todo.prop(\"title\")` returns an `AtomRef\u003cstring\u003e` (not an `Atom` directly). Primary read style is callable (`title()`).\nFor atom-graph interop (`Atom.map`, etc.), use `AtomRef.toAtom(title)`.\n\n```ts\nconst titleAtom = AtomRef.toAtom(title);\nconst upper = Atom.map(titleAtom, (s) =\u003e s.toUpperCase());\n\nconst titleQuery = defineQuery(() =\u003e Effect.succeed(titleAtom()), { name: \"title\" });\n```\n\n`get.result(...)` expects an atom carrying `Result`/`FetchResult`; use `AtomRef.toAtom(...)` for value-level interop first.\n\n### Advanced: defineQuery / atomEffect / Result\n\nBoth create reactive async computations backed by Effect fibers. When tracked dependencies change, the previous fiber is interrupted and a new one starts.\n\n```tsx\nimport { Effect } from \"effect\";\nimport { atomEffect, defineQuery, useService } from \"effect-atom-jsx\";\nimport { Result, Async } from \"effect-atom-jsx/advanced\";\n\n// atomEffect — standalone, no runtime needed\nconst time = atomEffect(() =\u003e\n  Effect.succeed(new Date().toISOString()).pipe(Effect.delay(\"1 second\"))\n);\n\n// defineQuery — uses ambient Layer runtime from mount()\nconst data = defineQuery(() =\u003e useService(Api).load(), { name: \"data\" });\n\nconst users = defineQuery(() =\u003e useService(Api).listUsers(), {\n  name: \"users\",\n  retrySchedule: Schedule.exponential(\"1 second\").pipe(Schedule.compose(Schedule.recurs(3))),\n  pollSchedule: Schedule.spaced(\"30 seconds\"),\n});\n\n// Pattern-match on the result in JSX\n\u003cAsync\n  result={data.result()}\n  loading={() =\u003e \u003cp\u003eLoading...\u003c/p\u003e}\n  error={(e) =\u003e \u003cp\u003eError: {e.message}\u003c/p\u003e}\n  success={(value) =\u003e \u003cp\u003e{value}\u003c/p\u003e}\n/\u003e\n```\n\n**Key difference:** `defineQuery` uses the ambient runtime injected by `mount()`, while `atomEffect` runs Effects directly (or accepts an explicit runtime parameter).\n\n`defineQuery` supports Phase E scheduling/observability options:\n\n- `retrySchedule`: retry typed failures before settling\n- `pollSchedule`: periodic invalidation/polling via Effect schedule\n- `onTransition` and `observe`: lightweight execution hooks for tracing/metrics\n\nFor ergonomic key + invalidation wiring, pass `query.key` into `defineMutation({ invalidates })`.\n\n#### `Result` state mapping defaults\n\n`Async` supports all `Result` states:\n\n- `Loading` -\u003e `loading()`\n- `Refreshing(previous)` -\u003e `refreshing(previous)` if provided, otherwise reuses the settled previous renderer\n- `Success(value)` -\u003e `success(value)`\n- `Failure(error)` -\u003e `error(error)` if provided, otherwise `null`\n- `Defect(cause)` -\u003e `defect(cause)` if provided, otherwise `null`\n\nIf you want defects or typed failures to escalate globally, leave local handlers undefined and use boundaries at higher levels.\n\n\n\n### Advanced Compatibility: FetchResult\n\nUse `Result` as the default async model. `FetchResult` is an advanced compatibility model.\n\n| Type | Module | Used by | Purpose |\n|------|--------|---------|---------|\n| `Result\u003cA, E\u003e` | `effect-ts.ts` | Default | Unified async state (Loading / Refreshing / Success / Failure / Defect) |\n| `FetchResult\u003cA, E\u003e` | `Result.ts` | Advanced compat | Data-fetching state (Initial / Success / Failure) with waiting flag |\n\nConvert between them with `FetchResult.fromResult()` and `FetchResult.toResult()`.\n\nImportant: 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.\n\nFor explicit non-suspense rendering, use `FetchResult.builder(...)`:\n\n```tsx\nconst view = FetchResult.builder(FetchResult.fromResult(users()))\n  .onInitial(() =\u003e \u003cSpinner /\u003e)\n  .onFailure((cause) =\u003e \u003cErrorCard cause={cause} /\u003e)\n  .onSuccess((data, { waiting }) =\u003e (\n    \u003c\u003e\n      {waiting \u0026\u0026 \u003cRefreshIndicator /\u003e}\n      \u003cFor each={data}\u003e{(u) =\u003e \u003cli\u003e{u().name}\u003c/li\u003e}\u003c/For\u003e\n    \u003c/\u003e\n  ))\n  .render();\n```\n\n`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.\n\n### Mutations: Linear First\n\nPrefer linear write flows with `Atom.runtime(layer).action(...)` when working with services.\n\n```ts\nimport { Effect } from \"effect\";\nimport { Atom, createOptimistic } from \"effect-atom-jsx\";\n\nconst optimisticUsers = createOptimistic(users);\n\nconst addUser = apiRuntime.action(\n  Effect.fn(function* (name: string) {\n    optimisticUsers.set((prev) =\u003e [...prev, { id: \"optimistic\", name }]);\n    const api = yield* Api;\n    yield* api.addUser(name);\n  }),\n  {\n    reactivityKeys: [\"users\"],\n    onError: () =\u003e optimisticUsers.clear(),\n    onSuccess: () =\u003e optimisticUsers.clear(),\n  },\n);\n```\n\nUse `defineMutation(...)` when you want callback-style lifecycle hooks.\n\n```ts\nimport { Effect } from \"effect\";\nimport { Atom, createOptimistic, defineMutation } from \"effect-atom-jsx\";\n\nconst savedCount = Atom.make(0);\nconst optimistic = createOptimistic(savedCount);\n\nconst save = defineMutation(\n  (next: number) =\u003e Effect.succeed(next).pipe(Effect.delay(\"250 millis\")),\n  {\n    optimistic: (next) =\u003e optimistic.set(next),\n    rollback: () =\u003e optimistic.clear(),\n    onSuccess: (next) =\u003e {\n      optimistic.clear();\n      savedCount.set(next);\n    },\n  },\n);\n\n// Typed composition path\nconst saveProgram = save.effect(10);\n\nsave.run(10);\nconsole.log(optimistic()); // 10 immediately\n```\n\n### defineMutation — Callback Alternative\n\nComposition summary:\n\n- `defineQuery(...).effect()` returns `Effect\u003cA, E | BridgeError\u003e`\n- `defineMutation(...).effect(input)` returns `Effect\u003cvoid, E | BridgeError | MutationSupersededError\u003e`\n- `Atom.runtime(...).action(...).effect(input)` returns `Effect\u003cvoid, E | BridgeError | MutationSupersededError\u003e`\n- `Atom.runtime(...).action(...).runEffect(input)` returns `Effect\u003cA, E | BridgeError | MutationSupersededError\u003e` (preserves action success value)\n- `Atom.result(atom)` converts result-like atoms into typed `Effect` values for pipelines\n\n`BridgeError` is tagged (`ResultLoadingError` | `ResultDefectError`) so composition errors stay explicit in the Effect error channel.\n\n### AtomSchema — Form Validation\n\nWraps atoms with Effect Schema for reactive validation with form state tracking.\n\n```ts\nimport { Schema, Effect, Option } from \"effect\";\nimport { Atom, AtomSchema } from \"effect-atom-jsx\";\n\nconst ageField = AtomSchema.makeInitial(Schema.Int, 25);\n\n// Each field provides reactive accessors\nageField.value;   // Atom\u003cOption\u003cnumber\u003e\u003e — parsed value\nageField.error;   // Atom\u003cOption\u003cSchemaError\u003e\u003e — validation error\nageField.isValid; // Atom\u003cboolean\u003e\nageField.touched; // Atom\u003cboolean\u003e — modified since creation?\nageField.dirty;   // Atom\u003cboolean\u003e — differs from initial?\n\n// Write invalid input\nEffect.runSync(Atom.set(ageField.input, 1.5));\nEffect.runSync(Atom.get(ageField.isValid)); // false\n\n// Reset everything\nageField.reset(); // restores initial value, clears touched\n\nconst profile = AtomSchema.struct({\n  age: AtomSchema.makeInitial(Schema.Int, 25),\n  score: AtomSchema.makeInitial(Schema.Int, 10),\n});\nprofile.isValid();\nprofile.touch();\nprofile.input.set({ age: 30, score: 11 });\nprofile.values(); // Accessor\u003cOption\u003c{ age: number; score: number }\u003e\u003e\n\nconst address = AtomSchema.struct({\n  city: AtomSchema.makeInitial(Schema.String, \"\"),\n  zip: AtomSchema.makeInitial(Schema.Int, 12345),\n});\nconst userForm = AtomSchema.struct({\n  profile,\n  address,\n});\nuserForm.reset();\n```\n\n### AtomLogger — Debug Tracking\n\nStructured logging for atom reads and writes using Effect's Logger.\n\n```ts\nimport { Effect } from \"effect\";\nimport { Atom, AtomLogger } from \"effect-atom-jsx\";\n\nconst count = Atom.make(0);\n\n// Wrap to automatically log all reads/writes\nconst traced = AtomLogger.tracedWritable(count, \"count\");\n// logs: atom:read { atom: \"count\", op: \"read\", value: \"0\" }\n// logs: atom:write { atom: \"count\", op: \"write\", value: \"5\" }\n\n// Effect-based logging\nEffect.runSync(AtomLogger.logGet(count, \"count\"));\n\n// Capture state snapshot\nconst snap = Effect.runSync(\n  AtomLogger.snapshot([[\"count\", count], [\"other\", otherAtom]])\n);\n// { count: 0, other: \"hello\" }\n```\n\n### fromStream / fromQueue / fromSchedule — Streaming Atoms\n\nCreate atoms whose values are continuously updated from Effect Streams or Queues.\n\n```ts\nimport { Stream, Queue, Effect, Schedule } from \"effect\";\nimport { Atom } from \"effect-atom-jsx\";\n\n// Atom fed by a Stream — starts a fiber on first read\nconst prices = Atom.fromStream(\n  Stream.fromIterable([10, 20, 30]),\n  0, // initial value\n);\n\n// Atom fed by a Queue\nconst queue = Effect.runSync(Queue.unbounded\u003cstring\u003e());\nconst messages = Atom.fromQueue(queue, \"\");\n\n// Atom fed by a Schedule (via Stream.fromSchedule)\nconst ticks = Atom.fromSchedule(Schedule.recurs(3), 0 as any);\n\n// Stream recipe for UI text inputs (trim + length filtering)\nconst rawInput = Stream.make(\"  hello  \", \" \", \"x\", \" world \");\nconst queryInput = Atom.Stream.textInput(rawInput, { minLength: 2 });\n\n// Search-box recipe (text normalization + optional dedupe)\nconst searchTerms = Atom.Stream.searchInput(rawInput, {\n  minLength: 2,\n  lowercase: true,\n});\n\n// Both helpers return Effect Streams, so compose them into atoms.\nfunction SearchBox() {\n  const [input, setInput] = createSignal(\"\");\n  const results = Atom.fromStream(\n    Atom.Stream.searchInput(inputToStream(input), { minLength: 2, lowercase: true }),\n    [] as ReadonlyArray\u003cstring\u003e,\n  );\n  return \u003cinput onInput={(e) =\u003e setInput((e.currentTarget as HTMLInputElement).value)} /\u003e;\n}\n```\n\n### Server-Side Rendering\n\nRender components to HTML strings on the server and hydrate on the client.\n\n```ts\nimport {\n  renderToString, hydrateRoot, isServer,\n  setRequestEvent, getRequestEvent,\n} from \"effect-atom-jsx\";\nimport { Hydration, Atom } from \"effect-atom-jsx\";\nimport * as Registry from \"effect-atom-jsx/Registry\";\n\n// ─── Server ─────────────────────────────────────────────────────\nsetRequestEvent({ url: req.url, headers: req.headers });\n\nconst html = renderToString(() =\u003e \u003cApp /\u003e);\n\n// Serialize atom state for the client\nconst registry = Registry.make();\nconst state = Hydration.dehydrate(registry, [\n  [\"count\", countAtom],\n  [\"user\", userAtom],\n]);\n\nres.send(`\n  \u003cdiv id=\"root\"\u003e${html}\u003c/div\u003e\n  \u003cscript\u003ewindow.__STATE__ = ${JSON.stringify(state)}\u003c/script\u003e\n`);\n\n// ─── Client ─────────────────────────────────────────────────────\n// Restore atom state from server\nHydration.hydrate(registry, window.__STATE__, {\n  count: countAtom,\n  user: userAtom,\n});\n\n// Optional validation hooks for development diagnostics:\nHydration.hydrate(registry, window.__STATE__, { count: countAtom, user: userAtom }, {\n  onUnknownKey: (key) =\u003e console.warn(\"Unknown hydration key:\", key),\n  onMissingKey: (key) =\u003e console.warn(\"Missing hydration key:\", key),\n});\n\n// Attach reactivity to existing DOM\nconst dispose = hydrateRoot(() =\u003e \u003cApp /\u003e, document.getElementById(\"root\")!);\n```\n\n## Control-Flow Components\n\nJSX components for declarative conditional and list rendering:\n\n| Component | Purpose | Example |\n|-----------|---------|---------|\n| `Show` | Conditional rendering | `\u003cShow when={show()}\u003e\u003cp\u003eVisible\u003c/p\u003e\u003c/Show\u003e` |\n| `For` | List rendering with keying | `\u003cFor each={items()}\u003e{(item) =\u003e \u003cli\u003e{item}\u003c/li\u003e}\u003c/For\u003e` |\n| `Async` | Result pattern matching | `\u003cAsync result={r} loading={...} success={...} /\u003e` |\n| `Loading` | Show content while loading | `\u003cLoading when={result}\u003e\u003cSpinner /\u003e\u003c/Loading\u003e` |\n| `Errored` | Show content on error | `\u003cErrored result={r}\u003e{(e) =\u003e \u003cp\u003e{e}\u003c/p\u003e}\u003c/Errored\u003e` |\n| `Switch` / `Match` | Multi-case matching | `\u003cSwitch\u003e\u003cMatch when={a()}\u003eA\u003c/Match\u003e...\u003c/Switch\u003e` |\n| `MatchTag` | Type-safe `_tag` matching | `\u003cMatchTag value={r} cases={{ Success: ... }} /\u003e` |\n| `Optional` | Render when value is truthy | `\u003cOptional when={val()}\u003e{(v) =\u003e \u003cp\u003e{v}\u003c/p\u003e}\u003c/Optional\u003e` |\n| `MatchOption` | Match Effect Option | `\u003cMatchOption value={opt} some={(v) =\u003e ...} /\u003e` |\n| `Dynamic` | Dynamic component selection | `\u003cDynamic component={Comp} ...props /\u003e` |\n| `WithLayer` | Provide a Layer boundary | `\u003cWithLayer layer={DbLive}\u003e...\u003c/WithLayer\u003e` |\n| `Frame` | Animation frame loop | `\u003cFrame\u003e{() =\u003e \u003ccanvas /\u003e}\u003c/Frame\u003e` |\n\n## API Reference\n\n### Namespace Modules\n\nPrimary modules are available as top-level namespace imports; advanced modules like `Registry` are deep-imported:\n\n```ts\n// Namespace import\nimport { Atom, AtomRef, Result, Hydration } from \"effect-atom-jsx\";\nimport { FetchResult } from \"effect-atom-jsx\"; // optional advanced compatibility\nimport { AtomSchema, AtomLogger, AtomRpc, AtomHttpApi } from \"effect-atom-jsx\";\n\n// Deep imports\nimport * as Atom from \"effect-atom-jsx/Atom\";\nimport * as AtomSchema from \"effect-atom-jsx/AtomSchema\";\nimport * as Registry from \"effect-atom-jsx/Registry\";\n```\n\n| Module | Key Exports |\n|--------|-------------|\n| `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` |\n| `AtomRef` | `make`, `collection` |\n| `Registry` | `make` (returns instance with `get`, `set`, `update`, `modify`, `mount`, `refresh`, `subscribe`, `reset`, `dispose`) |\n| `Result` | `loading`, `refreshing`, `success`, `failure`, `defect`, `match`, `map`, `flatMap`, `getOrElse`, `getOrThrow` |\n| `FetchResult` | `initial`, `success`, `failure`, `isInitial`, `isSuccess`, `isFailure`, `isWaiting`, `fromResult`, `toResult`, `map`, `flatMap`, `match`, `all` |\n| `Hydration` | `dehydrate`, `hydrate`, `toValues` |\n| `AtomSchema` | `make`, `makeInitial`, `path`, `HtmlInput` |\n| `AtomLogger` | `traced`, `tracedWritable`, `logGet`, `logSet`, `snapshot` |\n| `AtomRpc` | `Tag()` factory with `query`, `mutation`, `refresh` |\n| `AtomHttpApi` | `Tag()` factory with grouped `query`, `mutation`, `refresh` |\n\n### Effect Integration\n\n```ts\nimport {\n  defineQuery, createQueryKey, invalidate,\n  isPending, latest,\n  createOptimistic, defineMutation,\n  useService, useServices, createMount, mount,\n} from \"effect-atom-jsx\";\n\nimport {\n  atomEffect,\n  layerContext,\n  scopedRootEffect,\n  scopedQueryEffect,\n  scopedMutationEffect,\n  Result, Async,\n} from \"effect-atom-jsx/advanced\";\n```\n\n### Reactive Core (Internals / Advanced)\n\n```ts\nimport {\n  createSignal, createEffect, createMemo, createRoot,\n  createContext, useContext,\n  onCleanup, onMount,\n  untrack, sample, flush,\n  mergeProps, splitProps,\n  getOwner, runWithOwner,\n} from \"effect-atom-jsx/advanced\";\n```\n\n`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.\n\nFull API reference: [`docs/API.md`](docs/API.md)\n\nDedicated Effect integration guide: [`docs/ACTION_EFFECT_USE_RESOURCE.md`](docs/ACTION_EFFECT_USE_RESOURCE.md)\n\nEffect-atom migration/equivalents guide: [`docs/EFFECT_ATOM_EQUIVALENTS.md`](docs/EFFECT_ATOM_EQUIVALENTS.md)\n\nArchitecture decisions (in progress): `docs/adr/`\n\n## Examples\n\n| Example | Location | What it shows |\n|---------|----------|---------------|\n| Counter | `examples/counter/` | Signals, atoms, Registry, async data with `atomEffect` |\n| Projection | `examples/projection/` | `Atom.projection` + `Atom.projectionAsync` with `Async` rendering |\n| OOO Async | `examples/ooo-async/` | `Atom.pull` + OOO chunk merge, rendered via `Async`, `Loading`, and `Errored` |\n| TodoMVC | `examples/todomvc/` | Full app with `defineQuery`, `defineMutation`, optimistic UI, service injection |\n| RPC \u0026 HTTP API | `examples/rpc-httpapi/` | `AtomRpc.Tag()`, `AtomHttpApi.Tag()`, `MatchTag` component |\n| Schema Form | `examples/schema-form/` | `AtomSchema` validation, touched/dirty/reset, `AtomLogger.snapshot` |\n| SSR | `examples/ssr/` | `renderToString`, `hydrateRoot`, `Hydration.dehydrate/hydrate` |\n\n## How It Works\n\n1. **`Atom.runtime(layer)`** creates a runtime-bound API for reads (`runtime.atom`) and writes (`runtime.action`)\n2. Effects inside runtime-bound atoms/actions resolve services via Effect context (`yield* Api`) with requirements satisfied by the bound layer\n3. **`defineQuery()` / `atomEffect()`** run async effects reactively, exposing `Result` state\n4. **`defineMutation()`** remains the callback-style mutation alternative (optimistic/rollback hooks)\n5. Component lifetimes are scope-backed: mount/root and component boundaries map to Effect scopes so parent disposal interrupts descendant fibers transitively\n6. **`createMount(layer)` + `useService(Tag)`** remain the ambient-runtime alternative for simpler trees\n7. **`scopedRootEffect()` / `scopedQueryEffect()` / `scopedMutationEffect()`** are advanced Effect-first lifetime constructors\n8. Babel compiles JSX to **dom-expressions** helpers — reactivity updates only the affected DOM nodes\n\n## Testing\n\nDOM-free test harness via `effect-atom-jsx/testing`:\n\n```ts\nimport { Effect } from \"effect\";\nimport { Atom, defineQuery, defineMutation, useService } from \"effect-atom-jsx\";\nimport { withTestLayer, renderWithLayer, mockService } from \"effect-atom-jsx/testing\";\n\nconst ApiMock = mockService(Api, {\n  load: () =\u003e Effect.succeed(42),\n  save: (_n: number) =\u003e Effect.void,\n});\n\n// Option 1: runtime-first testing (primary)\nconst testRuntime = Atom.runtime(ApiMock);\nconst users = testRuntime.atom(\n  Effect.gen(function* () {\n    const api = yield* Api;\n    return yield* api.load();\n  }),\n);\nawait Effect.runPromise(Atom.result(users));\nawait testRuntime.dispose();\n\n// Option 2: withTestLayer — manual ambient runtime execution\nconst harness = withTestLayer(ApiMock);\nconst result = harness.run(() =\u003e defineQuery(() =\u003e useService(Api).load(), { name: \"load\" }));\nawait harness.tick();\nawait harness.dispose();\n\n// Option 3: renderWithLayer — runs UI immediately\nconst harness2 = renderWithLayer(ApiMock, () =\u003e {\n  const save = defineMutation((n: number) =\u003e useService(Api).save(n));\n  save.run(42);\n});\nawait harness2.tick();\nawait harness2.dispose();\n```\n\n\u003e See [`docs/TESTING.md`](docs/TESTING.md) for the full testing guide.\n\n## `flush()` Escape Hatch\n\nMicrotask batching is the default. Use `flush()` only when imperative DOM work needs synchronous commit ordering.\n\n```tsx\nimport { Atom } from \"effect-atom-jsx\";\n\nfunction handleSubmit(button: HTMLButtonElement) {\n  const submitted = Atom.make(false);\n  submitted.set(true);\n  Atom.flush();\n  button.focus();\n}\n```\n\n## Relationship to `@effect-atom/atom`\n\nThis project provides an effect-atom-like ergonomic surface, implemented natively for Effect v4.\n\n- **Same:** namespace-style API (`Atom`, `Result`, `Registry`, `AtomRef`), atom graph patterns, waiting/revalidation async model\n- **Different:** native implementation tuned for JSX + dom-expressions, targets Effect v4 beta (vs v3)\n- **Guidance:** if you already think in effect-atom terms, this API should feel familiar. Prefer `defineQuery` / `defineMutation` / `createMount` for Effect service integration.\n\n\n\n## Compatibility\n\n- Runtime: Effect v4 beta (`effect@^4.0.0-beta.29`)\n- JSX: `dom-expressions` via `effect-atom-jsx/runtime`\n- Test: `npm test` / Typecheck: `npm run typecheck` / Build: `npm run build`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoeixd%2Feffect-atom-jsx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdoeixd%2Feffect-atom-jsx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoeixd%2Feffect-atom-jsx/lists"}