Use @effect-atom/atom-solid with SolidJS
main@effect-atom/atom library. It allows you to integrate Effect-based atoms into SolidJS applications, leveraging Solid's fine-grained reactivity system.repository·main·Indexed 20 days ago
https://github.com/tim-smart/effect-atomA 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.
@effect-atom/atom library. It allows you to integrate Effect-based atoms into SolidJS applications, leveraging Solid's fine-grained reactivity system.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.Atoms can be backed by Effect Streams:
Atom.make(stream) creates an Atom that always holds the latest value emitted by the stream as a Result.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()
}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
// ...
}There are two primary ways to create derived state:
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.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)The Result<A, E> type represents the state of an asynchronous or effectful operation. It is a union of three possible states:
Initial<A, E>: The operation has not yet started or has no value.Success<A, E>: The operation completed successfully. It contains the value of type A and a timestamp.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>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>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.
HttpApi.make and HttpApiEndpoint to define your endpoints, methods, paths, and success schemas.AtomHttpApi.Tag to create a specialized Context.Tag. You must provide the api definition, an httpClient (e.g., FetchHttpClient.layer), and a baseUrl.CountClient.query(groupName, endpointName, options) for read-only data. Use reactivityKeys to enable invalidation.CountClient.mutation(groupName, endpointName) for state changes. Mutations can include reactivityKeys to invalidate queries when finished.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>
)
}You can bridge Effect's dependency injection (Layers/Services) with Atoms using Atom.runtime.
AtomRuntime from an Effect Layer using Atom.runtime(Layer).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
}),
)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);<script setup> Single File Components (SFCs).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.
RpcGroup.make to define your RPC methods and their success schemas.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).CountClient.query(name, payload, options) for read-only data. Use reactivityKeys to enable invalidation.CountClient.mutation(name) for state changes. Mutations can also include reactivityKeys to automatically invalidate related queries upon completion.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>
)
}