effect-atom

repository·main·Indexed 20 days ago

https://github.com/tim-smart/effect-atom

A reactive state management toolkit built on the Effect ecosystem. It provides Atoms backed by simple values, Effects, Streams, or browser APIs like LocalStorage and URL search params. The library includes official bindings for React (@effect-atom/atom-react) and SolidJS (@effect-atom/atom-solid), and features advanced integrations for @effect/rpc, HttpApi, and AtomLiveStore for managing live state and remote queries.

Tokens
41.6K
Snippets
151
Records
178
Agent score
72%

What's inside @effect-atom/atom

  1. Explore the @effect-atom/atom core modules

    main

    The @effect-atom/atom package provides a modular set of tools for managing state and effects. The main entry point re-exports several specialized modules that handle different aspects of the atom lifecycle and communication:

    • Atom: The core module for defining and managing atoms.
    • AtomHttpApi: Tools for interacting with atoms via HTTP APIs.
    • AtomRef: Utilities for handling atom references.
    • AtomRpc: Tools for Remote Procedure Call (RPC) capabilities with atoms.
    • Hydration: Mechanisms for hydrating state (e.g., from a server or local storage).
    • Registry: Management of atom registries.
    • Result: Types and utilities for handling operation results.
  2. Work with Streams and Pull Atoms

    main

    Atoms can be backed by Effect Streams:

    • Standard Stream Atom: Atom.make(stream) creates an Atom that always holds the latest value emitted by the stream as a Result.
    • Pull Atom: Atom.pull(stream) creates a Writable Atom that allows you to pull chunks of data from a stream one at a time (useful for infinite scrolling). Use useAtom in React to get both the result and a pull function.
    import { Atom, Result, useAtom } from "@effect-atom/atom-react"
    import { Cause, Schedule, Stream } from "effect"
    
    // Emits incrementing number every second
    const countAtom = Atom.make(Stream.fromSchedule(Schedule.spaced(1000)))
    
    // Pull-based Atom for paginated data
    const countPullAtom = Atom.pull(Stream.make(1, 2, 3, 4, 5))
    
    function CountPullAtomComponent() {
      const [result, pull] = useAtom(countPullAtom)
    
      return Result.builder(result)
        .onInitial(() => <div>Loading...</div>)
        .onFailure((cause) => <div>Error: {Cause.pretty(cause)}</div>)
        .onSuccess(({ items }, { waiting }) => (
          <div>
            <ul>{items.map((item) => <li key={item}>{item}</li>)}</ul>
            <button onClick={() => pull()}>Load more</button>
            {waiting ? <p>Loading more...</p> : <p>Loaded chunk</p>}
          </div>
        ))
        .render()
    }
  3. Use ScopedAtom in React

    main

    A ScopedAtom is a pattern used to provide an Atom instance within a specific React component subtree. It bundles the Atom itself, a React Context, and a Provider component to manage the lifecycle and scope of the Atom.

    To use it, you typically create a scoped atom using make and then wrap your component tree with the returned Provider component, passing in an optional value if the atom requires input.

    // Conceptual usage pattern
    const myScopedAtom = make(() => createAtom(0));
    
    function App() {
      return (
        <myScopedAtom.Provider>
          <Child />
        </myScopedAtom.Provider>
      );
    }
    
    function Child() {
      const atom = useScopedAtom(); // Uses the atom from context
      // ...
    }
  4. Create derived state from Atoms

    main

    There are two primary ways to create derived state:

    1. Using Atom.Context: Pass a function to Atom.make that receives a get object (of type Atom.Context). You can use get(otherAtom) to retrieve the value of another Atom.
    2. Using Atom.map: Use Atom.map(sourceAtom, transformFn) to create a new Atom that automatically updates whenever the source Atom changes.
    import { Atom } from "@effect-atom/atom-react"
    
    const countAtom = Atom.make(0)
    
    // Method 1: Using Atom.Context
    const doubleCountAtom = Atom.make((get) => get(countAtom) * 2)
    
    // Method 2: Using Atom.map
    const tripleCountAtom = Atom.map(countAtom, (count) => count * 3)
  5. Understand the Result type and its states

    main

    The Result<A, E> type represents the state of an asynchronous or effectful operation. It is a union of three possible states:

    1. Initial<A, E>: The operation has not yet started or has no value.
    2. Success<A, E>: The operation completed successfully. It contains the value of type A and a timestamp.
    3. Failure<A, E>: The operation failed. It contains a cause of type Cause.Cause<E> and an optional previousSuccess (of type Option.Option<Success<A, E>>) if a successful value existed before the failure.

    All Result types implement the Proto<A, E> interface, which provides access to the types A and E via TypeId and a waiting boolean flag.

    type Result<A, E = never> = Initial<A, E> | Success<A, E> | Failure<A, E>
  6. Manage the Registry in Vue

    main

    The registry manages the lifecycle and lookup of atoms. In @effect-atom/atom-vue, you can interact with the registry using the following tools:

    • defaultRegistry: The standard registry instance.
    • injectRegistry: A function to retrieve the registry (useful in contexts where the registry is provided via dependency injection).
    • registryKey: A Vue InjectionKey used to provide/inject the registry into the Vue component tree.
    export declare const defaultRegistry: Registry.Registry
    export declare const injectRegistry: () => Registry.Registry
    export declare const registryKey: InjectionKey<Registry.Registry>
  7. Integrate HttpApi with effect-atom

    main

    The AtomHttpApi module allows you to create an HTTP API client that integrates with effect-atom. This enables you to use HTTP endpoints as reactive atoms.

    Key Workflow:

    1. Define the API: Use HttpApi.make and HttpApiEndpoint to define your endpoints, methods, paths, and success schemas.
    2. Create the Client Tag: Use AtomHttpApi.Tag to create a specialized Context.Tag. You must provide the api definition, an httpClient (e.g., FetchHttpClient.layer), and a baseUrl.
    3. Use in Components:
      • Use CountClient.query(groupName, endpointName, options) for read-only data. Use reactivityKeys to enable invalidation.
      • Use CountClient.mutation(groupName, endpointName) for state changes. Mutations can include reactivityKeys to invalidate queries when finished.
    4. Use in Effect Services: Add CountClient.layer to your service's dependencies to access the client via yield* CountClient.
    import { AtomHttpApi, Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react"
    import { FetchHttpClient, HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform"
    import { Effect, Schema } from "effect"
    
    // 1. Define your api
    class Api extends HttpApi.make("api").add(
      HttpApiGroup.make("counter").add(
        HttpApiEndpoint.get("count", "/count").addSuccess(Schema.Number)
      ).add(
        HttpApiEndpoint.post("increment", "/increment")
      )
    ) {}
    
    // 2. Create the Client Tag
    class CountClient extends AtomHttpApi.Tag<CountClient>()("CountClient", {
      api: Api,
      httpClient: FetchHttpClient.layer,
      baseUrl: "http://localhost:3000"
    }) {}
    
    function SomeComponent() {
      // 3. Use in Components
      const count = useAtomValue(CountClient.query("counter", "count", {
        reactivityKeys: ["count"]
      }))
    
      const increment = useAtomSet(CountClient.mutation("counter", "increment"))
    
      return (
        <div onClick={() => increment({ payload: void 0, reactivityKeys: ["count"] })}>
          <p>Count: {Result.getOrElse(count, () => 0)}</p>
        </div>
      )
    }
  8. Use Effect Services and Layers with AtomRuntime

    main

    You can bridge Effect's dependency injection (Layers/Services) with Atoms using Atom.runtime.

    1. Create an AtomRuntime from an Effect Layer using Atom.runtime(Layer).
    2. Use the runtime to create Atoms that can access services via yield* ServiceName within an effectful definition.
    import { Atom } from "@effect-atom/atom-react"
    import { Effect } from "effect"
    
    class Users extends Effect.Service<Users>()("app/Users", {
      effect: Effect.succeed({
        getAll: Effect.succeed([{ id: "1", name: "Alice" }])
      } as const),
    }) {}
    
    const runtimeAtom = Atom.runtime(Users.Default)
    
    const usersAtom = runtimeAtom.atom(
      Effect.gen(function* () {
        const users = yield* Users
        return yield* users.getAll
      }),
    )
  9. Use Vue composables with @effect-atom/atom-vue

    main

    The @effect-atom/atom-vue package provides Vue-specific composables to interact with Atoms within the Vue reactivity system. These composables allow you to bind Atom state to Vue Refs and handle updates (writes) using various modes including synchronous values and asynchronous promises.

    import { useAtom, useAtomValue, useAtomSet } from '@effect-atom/atom-vue';
    
    // Example usage pattern
    // const [state, setState] = useAtom(() => myAtom);
    // const value = useAtomValue(() => myAtom);
  10. Integrate @effect/rpc with effect-atom

    main

    You can use the AtomRpc module to create an RPC client that integrates with effect-atom. This allows you to treat RPC queries and mutations as atoms, providing reactivity and easy integration with React hooks like useAtomValue and useAtomSet.

    Key Workflow:

    1. Define RPCs: Use RpcGroup.make to define your RPC methods and their success schemas.
    2. Create the Client Tag: Use AtomRpc.Tag to create a specialized Context.Tag. You must provide a group (your RPC definitions) and a protocol (a Layer providing the RpcClient.Protocol, such as a WebSocket layer).
    3. Use in Components:
      • Use CountClient.query(name, payload, options) for read-only data. Use reactivityKeys to enable invalidation.
      • Use CountClient.mutation(name) for state changes. Mutations can also include reactivityKeys to automatically invalidate related queries upon completion.
    4. Use in Effect Services: Add CountClient.layer to your service's dependencies to access the client via yield* CountClient.
    import { AtomRpc, Result, useAtomSet, useAtomValue } from "@effect-atom/atom-react"
    import { Effect, Layer, Schema } from "effect"
    import { BrowserSocket } from "@effect/platform-browser"
    import { Rpc, RpcClient, RpcGroup, RpcSerialization } from "@effect/rpc"
    
    // 1. Define the RPCs
    class Rpcs extends RpcGroup.make(
      Rpc.make("increment"),
      Rpc.make("count", { success: Schema.Number })
    ) {}
    
    // 2. Create the Client Tag
    class CountClient extends AtomRpc.Tag<CountClient>()("CountClient", {
      group: Rpcs,
      protocol: RpcClient.layerProtocolSocket({
        retryTransientErrors: true
      }).pipe(
        Layer.provide(BrowserSocket.layerWebSocket("ws://localhost:3000/rpc")),
        Layer.provide(RpcSerialization.layerJson)
      )
    }) {}
    
    function SomeComponent() {
      // 3. Use in Components
      const count = useAtomValue(CountClient.query("count", void 0, {
        reactivityKeys: ["count"]
      }))
    
      const increment = useAtomSet(CountClient.mutation("increment"))
    
      return (
        <div onClick={() => increment({ payload: void 0, reactivityKeys: ["count"] })}>
          <p>Count: {Result.getOrElse(count, () => 0)}</p>
        </div>
      )
    }