{"id":16254575,"url":"https://github.com/tim-smart/effect-atom","last_synced_at":"2026-01-20T01:54:42.297Z","repository":{"id":195658522,"uuid":"692976564","full_name":"tim-smart/effect-atom","owner":"tim-smart","description":null,"archived":false,"fork":false,"pushed_at":"2025-09-06T10:32:56.000Z","size":1896,"stargazers_count":342,"open_issues_count":14,"forks_count":24,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-09-06T12:25:12.684Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://tim-smart.github.io/effect-atom/","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/tim-smart.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":"2023-09-18T05:13:42.000Z","updated_at":"2025-09-06T10:32:08.000Z","dependencies_parsed_at":"2023-09-23T02:40:37.862Z","dependency_job_id":"806f55f3-17ad-4864-a6c2-7a09d37d6c98","html_url":"https://github.com/tim-smart/effect-atom","commit_stats":null,"previous_names":["tim-smart/effect-rx","tim-smart/effect-atom"],"tags_count":596,"template":false,"template_full_name":null,"purl":"pkg:github/tim-smart/effect-atom","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tim-smart%2Feffect-atom","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tim-smart%2Feffect-atom/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tim-smart%2Feffect-atom/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tim-smart%2Feffect-atom/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tim-smart","download_url":"https://codeload.github.com/tim-smart/effect-atom/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tim-smart%2Feffect-atom/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":274583129,"owners_count":25311750,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-11T02:00:13.660Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":[],"created_at":"2024-10-10T15:22:58.297Z","updated_at":"2026-01-20T01:54:42.290Z","avatar_url":"https://github.com/tim-smart.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# @effect-atom/atom\n\nA reactive state management library for Effect.\n\n## Installation\n\nIf you are using React:\n\n```bash\npnpm add @effect-atom/atom-react\n```\n\n## Creating a Counter with Atom\n\nLet's create a simple Counter component, which will increment a number when you click a button.\n\nWe will use `Atom.make` to create our Atom, which is a reactive state container.\n\nWe can then use the `useAtomValue` \u0026 `useAtomSet` hooks to read and update the value\nof the Atom.\n\n```tsx\nimport { Atom, useAtomValue, useAtomSet } from \"@effect-atom/atom-react\"\n\nconst countAtom = Atom.make(0).pipe(\n  // By default, the Atom will be reset when no longer used.\n  // This is useful for cleaning up resources when the component unmounts.\n  //\n  // If you want to keep the value, you can use `Atom.keepAlive`.\n  //\n  Atom.keepAlive,\n)\n\nfunction App() {\n  return (\n    \u003cdiv\u003e\n      \u003cCounter /\u003e\n      \u003cbr /\u003e\n      \u003cCounterButton /\u003e\n    \u003c/div\u003e\n  )\n}\n\nfunction Counter() {\n  const count = useAtomValue(countAtom)\n  return \u003ch1\u003e{count}\u003c/h1\u003e\n}\n\nfunction CounterButton() {\n  const setCount = useAtomSet(countAtom)\n  return (\n    \u003cbutton onClick={() =\u003e setCount((count) =\u003e count + 1)}\u003eIncrement\u003c/button\u003e\n  )\n}\n```\n\n## Derived State\n\nYou can create derived state from an Atom in a couple of ways.\n\n```ts\nimport { Atom } from \"@effect-atom/atom-react\"\n\nconst countAtom = Atom.make(0)\n\n// You can use the `get` function to get the value of another Atom.\n//\n// The type of `get` is `Atom.Context`, which also has a bunch of other methods\n// on it to manage Atoms.\n//\nconst doubleCountAtom = Atom.make((get) =\u003e get(countAtom) * 2)\n\n// You can also use the `Atom.map` function to create a derived Atom.\nconst tripleCountAtom = Atom.map(countAtom, (count) =\u003e count * 3)\n```\n\n## Working with Effects\n\nYou can also pass effects to the `Atom.make` function.\n\nWhen working with effectful Atoms, you will get back a `Result` type.\n\nYou can see all the ways to work with `Result` here: https://tim-smart.github.io/effect-atom/atom/Result.ts.html\n\n```ts\nimport { Atom, Result } from \"@effect-atom/atom-react\"\nimport { Effect } from \"effect\"\n\n//        ┌─── Atom.Atom\u003cResult.Result\u003cnumber\u003e\u003e\n//        ▼\nconst countAtom = Atom.make(Effect.succeed(0))\n\n// You can also pass a function to get access to the `Atom.Context`\n//\n// `get.result` can be used in `Effect`s to get the value of an `Atom.Atom\u003cResult.Result\u003e`.\n//\n//             ┌─── Atom.Atom\u003cResult.Result\u003cnumber\u003e\u003e\n//             ▼\nconst resultWithContextAtom = Atom.make(\n  Effect.fnUntraced(function* (get: Atom.Context) {\n    const count = yield* get.result(countAtom)\n    return count + 1\n  }),\n)\n```\n\n## Working with scoped Effects\n\nAll Atoms that use effects are provided with a `Scope`, so you can add finalizers\nthat will be run when the Atom is no longer used.\n\n```ts\nimport { Atom } from \"@effect-atom/atom-react\"\nimport { Effect } from \"effect\"\n\nconst resultAtom = Atom.make(\n  Effect.gen(function* () {\n    // Add a finalizer to the `Scope` for this Atom\n    // It will run when the Atom is rebuilt or no longer needed\n    yield* Effect.addFinalizer(() =\u003e Effect.log(\"finalizer\"))\n    return \"hello\"\n  }),\n)\n```\n\n## Working with Effect Services / Layers\n\n```ts\nimport { Atom } from \"@effect-atom/atom-react\"\nimport { Effect } from \"effect\"\n\nclass Users extends Effect.Service\u003cUsers\u003e()(\"app/Users\", {\n  effect: Effect.gen(function* () {\n    const getAll = Effect.succeed([\n      { id: \"1\", name: \"Alice\" },\n      { id: \"2\", name: \"Bob\" },\n      { id: \"3\", name: \"Charlie\" },\n    ])\n    return { getAll } as const\n  }),\n}) {}\n\n// Create a `AtomRuntime` from a `Layer`.\n//\n//         ┌─── Atom.AtomRuntime\u003cUsers\u003e\n//         ▼\nconst runtimeAtom = Atom.runtime(Users.Default)\n\n// You can then use the `AtomRuntime` to make Atoms that use the services from the `Layer`.\nconst usersAtom = runtimeAtom.atom(\n  Effect.gen(function* () {\n    const users = yield* Users\n    return yield* users.getAll\n  }),\n)\n```\n\n## Adding global Layers to AtomRuntimes\n\nThis is useful for setting up `Tracer`s, `Logger`s, `ConfigProvider`s, etc.\n\n```ts\nimport { Atom } from \"@effect-atom/atom-react\"\nimport { ConfigProvider, Layer } from \"effect\"\n\nAtom.runtime.addGlobalLayer(\n  Layer.setConfigProvider(ConfigProvider.fromJson(import.meta.env)),\n)\n```\n\n## Working with `Stream`s\n\n```tsx\nimport { Atom, Result, useAtom } from \"@effect-atom/atom-react\"\nimport { Cause, Schedule, Stream } from \"effect\"\n\n// This will be a simple Atom that emits a incrementing number every second.\n//\n// Atom.make will give back the latest value of a `Stream` as a `Result`.\n//\n//        ┌─── Atom.Atom\u003cResult.Result\u003cnumber\u003e\u003e\n//        ▼\nconst countAtom = Atom.make(Stream.fromSchedule(Schedule.spaced(1000)))\n\n// You can use `Atom.pull` to create a specialized Atom that will pull from a `Stream`\n// one chunk at a time.\n//\n// This is useful for infinite scrolling or paginated data.\n//\n// With a `AtomRuntime`, you can use `runtimeAtom.pull` to create a pull Atom.\n//\n//        ┌─── Atom.Writable\u003cAtom.PullResult\u003cnumber\u003e, void\u003e\n//        ▼\nconst countPullAtom = Atom.pull(Stream.make(1, 2, 3, 4, 5))\n\n// Here is a component that uses `countPullAtom` to display the numbers in a list.\n//\n// You can use `useAtom` to both read the value of an Atom and gain access to the\n// setter function.\n//\n// Each time the setter function is called, it will pull a new chunk of data\n// from the `Stream`, and append it to the list.\nfunction CountPullAtomComponent() {\n  const [result, pull] = useAtom(countPullAtom)\n\n  return Result.builder(result)\n    .onInitial(() =\u003e \u003cdiv\u003eLoading...\u003c/div\u003e)\n    .onFailure((cause) =\u003e \u003cdiv\u003eError: {Cause.pretty(cause)}\u003c/div\u003e)\n    .onSuccess(({ items }, { waiting }) =\u003e (\n      \u003cdiv\u003e\n        \u003cul\u003e\n          {items.map((item) =\u003e (\n            \u003cli key={item}\u003e{item}\u003c/li\u003e\n          ))}\n        \u003c/ul\u003e\n        \u003cbutton onClick={() =\u003e pull()}\u003eLoad more\u003c/button\u003e\n        {waiting ? \u003cp\u003eLoading more...\u003c/p\u003e : \u003cp\u003eLoaded chunk\u003c/p\u003e}\n      \u003c/div\u003e\n    ))\n    .render()\n}\n```\n\n## Working with sets of Atoms\n\n```ts\nimport { Atom } from \"@effect-atom/atom-react\"\nimport { Effect } from \"effect\"\n\nclass Users extends Effect.Service\u003cUsers\u003e()(\"app/Users\", {\n  effect: Effect.gen(function* () {\n    const findById = (id: string) =\u003e Effect.succeed({ id, name: \"John Doe\" })\n    return { findById } as const\n  }),\n}) {}\n\n// Create a `AtomRuntime` from a `Layer`\nconst runtimeAtom = Atom.runtime(Users.Default)\n\n// Atoms work by reference, so we need to use `Atom.family` to dynamically create a\n// set of Atoms from a key.\n//\n// `Atom.family` will ensure that we get a stable reference to the Atom for each key.\n//\n//       ┌─── (arg: string) =\u003e Atom.Atom\u003cResult\u003c{ id: string; name: string; }\u003e\u003e\n//       ▼\nconst userAtom = Atom.family((id: string) =\u003e\n  runtimeAtom.atom(\n    Effect.gen(function* () {\n      const users = yield* Users\n      return yield* users.findById(id)\n    }),\n  ),\n)\n```\n\n## Working with functions\n\n```ts\nimport { Atom, useAtomSet } from \"@effect-atom/atom-react\"\nimport { Effect, Exit } from \"effect\"\n\n// Create a simple `Atom.fn` that logs a number\nconst logAtom = Atom.fn(\n  Effect.fnUntraced(function* (arg: number) {\n    yield* Effect.log(\"got arg\", arg)\n  }),\n)\n\nfunction LogComponent() {\n  // To call the `Atom.fn`, we need to use the `useAtomSet` hook\n  const logNumber = useAtomSet(logAtom)\n  return \u003cbutton onClick={() =\u003e logNumber(42)}\u003eLog 42\u003c/button\u003e\n}\n\n// You can also use it with `Atom.runtime`\nclass Users extends Effect.Service\u003cUsers\u003e()(\"app/Users\", {\n  effect: Effect.gen(function* () {\n    const create = (name: string) =\u003e Effect.succeed({ id: 1, name })\n\n    return { create } as const\n  }),\n}) {}\n\nconst runtimeAtom = Atom.runtime(Users.Default)\n\n// Here we are using `runtimeAtom.fn` to create a function from the `Users.create`\n// method.\nconst createUserAtom = runtimeAtom.fn(\n  Effect.fnUntraced(function* (name: string) {\n    const users = yield* Users\n    return yield* users.create(name)\n  }),\n)\n\nfunction CreateUserComponent() {\n  // If your function returns a `Result`, you can use the useAtomSet hook with `mode: \"promiseExit\"`\n  const createUser = useAtomSet(createUserAtom, { mode: \"promiseExit\" })\n  return (\n    \u003cbutton\n      onClick={async () =\u003e {\n        const exit = await createUser(\"John\")\n        if (Exit.isSuccess(exit)) {\n          console.log(exit.value)\n        }\n      }}\n    \u003e\n      Create user\n    \u003c/button\u003e\n  )\n}\n```\n\n## Wrapping an event listener\n\n```ts\nimport { Atom } from \"@effect-atom/atom-react\"\n\n// This is a simple Atom that will emit the current scroll position of the\n// window.\nconst scrollYAtom: Atom.Atom\u003cnumber\u003e = Atom.make((get) =\u003e {\n  // The handler will use `get.setSelf` to update the value of itself\n  const onScroll = () =\u003e {\n    get.setSelf(window.scrollY)\n  }\n  // We need to use `get.addFinalizer` to remove the event listener when the\n  // Atom is no longer used.\n  window.addEventListener(\"scroll\", onScroll)\n  get.addFinalizer(() =\u003e window.removeEventListener(\"scroll\", onScroll))\n\n  // Return the current scroll position\n  return window.scrollY\n})\n```\n\n## Integration with search params\n\n```ts\nimport { Atom } from \"@effect-atom/atom-react\"\nimport { Option, Schema } from \"effect\"\n\n// Create an Atom that reads and writes to the URL search parameters.\n//\n//          ┌─── Atom.Writable\u003cstring\u003e\n//          ▼\nconst simpleParamAtom = Atom.searchParam(\"paramName\")\n\n// You can also use a schema to further parse the value\n//\n//          ┌─── Atom.Writable\u003cOption\u003cnumber\u003e\u003e\n//          ▼\nconst numberParamAtom = Atom.searchParam(\"paramName\", {\n  schema: Schema.NumberFromString,\n})\n```\n\n## Integration with local storage\n\n```ts\nimport { Atom } from \"@effect-atom/atom-react\"\nimport { BrowserKeyValueStore } from \"@effect/platform-browser\"\nimport { Schema } from \"effect\"\n\nconst runtime = Atom.runtime(BrowserKeyValueStore.layerLocalStorage)\n\n// Create an Atom that reads and writes to `localStorage`.\n//\n// It uses `Schema` to define the type of the value stored.\n//\n//       ┌─── Atom.Writable\u003cboolean, boolean\u003e\n//       ▼\nconst flagAtom = Atom.kvs({\n  runtime: runtime,\n  key: \"flag\",\n  schema: Schema.Boolean,\n  defaultValue: () =\u003e false,\n})\n```\n\n## Integration with `Reactivity` from `@effect/experimental`\n\n`Reactivity` is an Effect service that allows you make queries reactive when\nmutations happen.\n\nYou can use an `Atom.runtime` to hook into the `Reactivity` service and trigger\n`Atom` refreshes when mutations happen.\n\n```ts\nimport { Atom } from \"@effect-atom/atom-react\"\nimport { Effect, Layer } from \"effect\"\nimport { Reactivity } from \"@effect/experimental\"\n\nconst runtimeAtom = Atom.runtime(Layer.empty)\n\nlet i = 0\n\n//      ┌─── Atom.Atom\u003cnumber\u003e\n//      ▼\nconst count = Atom.make(() =\u003e i++).pipe(\n  // Refresh when the \"counter\" key changes\n  Atom.withReactivity([\"counter\"]),\n  // Or refresh when \"counter\" or \"counter:1\" or \"counter:2\" changes\n  Atom.withReactivity({\n    counter: [1, 2],\n  }),\n)\n\nconst someMutation = runtimeAtom.fn(\n  Effect.fn(function* () {\n    yield* Effect.log(\"Mutating the counter\")\n  }),\n  // Invalidate the \"counter\" key when the Effect is finished\n  { reactivityKeys: [\"counter\"] },\n)\n\nconst someMutationManual = runtimeAtom.fn(\n  Effect.fn(function* () {\n    yield* Effect.log(\"Mutating the counter again\")\n    // You can also manually invalidate the \"counter\" key\n    yield* Reactivity.invalidate([\"counter\"])\n  }),\n)\n```\n\n## `@effect/rpc` integration\n\nYou can use the `AtomRpc` module to create an RPC client with integration with\n`effect-atom`. It offers apis for both queries and mutations.\n\n```ts\nimport {\n  AtomRpc,\n  Result,\n  useAtomSet,\n  useAtomValue\n} from \"@effect-atom/atom-react\"\nimport { Effect, Layer, Schema } from \"effect\"\nimport { BrowserSocket } from \"@effect/platform-browser\"\nimport { Rpc, RpcClient, RpcGroup, RpcSerialization } from \"@effect/rpc\"\n\n// Define the RPCs\nclass Rpcs extends RpcGroup.make(\n  Rpc.make(\"increment\"),\n  Rpc.make(\"count\", {\n    success: Schema.Number\n  })\n) {}\n\n// Use `AtomRpc.Tag` to create a special `Context.Tag` that builds the RPC client\nclass CountClient extends AtomRpc.Tag\u003cCountClient\u003e()(\"CountClient\", {\n  group: Rpcs,\n  // Provide a `Layer` that provides the RpcClient.Protocol\n  protocol: RpcClient.layerProtocolSocket({\n    retryTransientErrors: true\n  }).pipe(\n    Layer.provide(BrowserSocket.layerWebSocket(\"ws://localhost:3000/rpc\")),\n    Layer.provide(RpcSerialization.layerJson)\n  )\n}) {}\n\nfunction SomeComponent() {\n  // Use `CountClient.query` for readonly queries\n  const count = useAtomValue(CountClient.query(\"count\", void 0, {\n    // You can also register reactivity keys, which can be used to invalidate\n    // the query\n    reactivityKeys: [\"count\"]\n  }))\n\n  // Use `CountClient.mutation` for mutations\n  const increment = useAtomSet(CountClient.mutation(\"increment\"))\n\n  return (\n    \u003cdiv\u003e\n      \u003cp\u003eCount: {Result.getOrElse(count, () =\u003e 0)}\u003c/p\u003e\n      \u003cbutton\n        onClick={() =\u003e\n          increment({\n            payload: void 0,\n            // Mutations can also have reactivity keys, which will invalidate\n            // the query when the mutation is done.\n            reactivityKeys: [\"count\"]\n          })}\n      \u003e\n        Increment\n      \u003c/button\u003e\n    \u003c/div\u003e\n  )\n}\n\n// Or you can define custom atoms using the `CountClient.runtime`\nconst incrementAtom = CountClient.runtime.fn(Effect.fnUntraced(function*() {\n  const client = yield* CountClient // Use the Tag to access the client\n  yield* client(\"increment\", void 0)\n}))\n\n// Or use it in your Effect services\nclass MyService extends Effect.Service\u003cMyService\u003e()(\"MyService\", {\n  dependencies: [CountClient.layer], // Add the `CountClient` as a dependency\n  scoped: Effect.gen(function*() {\n    const client = yield* CountClient // Use the Tag to access the client\n    const useClient = () =\u003e client(\"increment\", void 0)\n    return { useClient } as const\n  })\n}) {}\n```\n\n## `HttpApi` integration\n\nYou can use the `AtomHttpApi` module to create an HTTP API client with\nintegration with `effect-atom`. It offers apis for both queries and mutations.\n\n```ts\nimport {\n  AtomHttpApi,\n  Result,\n  useAtomSet,\n  useAtomValue\n} from \"@effect-atom/atom-react\"\nimport {\n  FetchHttpClient,\n  HttpApi,\n  HttpApiEndpoint,\n  HttpApiGroup\n} from \"@effect/platform\"\nimport { Effect, Schema } from \"effect\"\n\n// Define your api\nclass Api extends HttpApi.make(\"api\").add(\n  HttpApiGroup.make(\"counter\").add(\n    HttpApiEndpoint.get(\"count\", \"/count\").addSuccess(Schema.Number)\n  ).add(\n    HttpApiEndpoint.post(\"increment\", \"/increment\")\n  )\n) {}\n\n// Use `AtomHttpApi.Tag` to create a special `Context.Tag` that builds the client\nclass CountClient extends AtomHttpApi.Tag\u003cCountClient\u003e()(\"CountClient\", {\n  api: Api,\n  // Provide a Layer that provides the HttpClient\n  httpClient: FetchHttpClient.layer,\n  baseUrl: \"http://localhost:3000\"\n}) {}\n\nfunction SomeComponent() {\n  // Use `CountClient.query` for readonly queries\n  const count = useAtomValue(CountClient.query(\"counter\", \"count\", {\n    // You can register reactivity keys, which can be used to invalidate\n    // the query\n    reactivityKeys: [\"count\"]\n  }))\n\n  // Use `CountClient.mutation` for mutations\n  const increment = useAtomSet(CountClient.mutation(\"counter\", \"increment\"))\n\n  return (\n    \u003cdiv\u003e\n      \u003cp\u003eCount: {Result.getOrElse(count, () =\u003e 0)}\u003c/p\u003e\n      \u003cbutton\n        onClick={() =\u003e\n          increment({\n            payload: void 0,\n            // Mutations can also have reactivity keys, which will invalidate\n            // the query when the mutation is done.\n            reactivityKeys: [\"count\"]\n          })}\n      \u003e\n        Increment\n      \u003c/button\u003e\n    \u003c/div\u003e\n  )\n}\n\n// Or you can define custom atoms using the `CountClient.runtime`\nconst incrementAtom = CountClient.runtime.fn(Effect.fnUntraced(function*() {\n  const client = yield* CountClient // Use the Tag to access the client\n  yield* client.counter.increment()\n}))\n\n// Or use it in your Effect services\nclass MyService extends Effect.Service\u003cMyService\u003e()(\"MyService\", {\n  dependencies: [CountClient.layer], // Add the `CountClient` as a dependency\n  scoped: Effect.gen(function*() {\n    const client = yield* CountClient // Use the Tag to access the client\n    const useClient = () =\u003e client.counter.increment()\n    return { useClient } as const\n  })\n}) {}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftim-smart%2Feffect-atom","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftim-smart%2Feffect-atom","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftim-smart%2Feffect-atom/lists"}