Skip to content

Repository files navigation

@effect-atom/atom

A reactive state management library for Effect.

Installation

If you are using React:

pnpm add @effect-atom/atom-react

Creating a Counter with Atom

Let's create a simple Counter component, which will increment a number when you click a button.

We will use Atom.make to create our Atom, which is a reactive state container.

We can then use the useAtomValue & useAtomSet hooks to read and update the value of the Atom.

import{Atom,useAtomValue,useAtomSet}from"@effect-atom/atom-react"constcountAtom=Atom.make(0).pipe(// By default, the Atom will be reset when no longer used.// This is useful for cleaning up resources when the component unmounts.//// If you want to keep the value, you can use `Atom.keepAlive`.//Atom.keepAlive,)functionApp(){return(<div><Counter/><br/><CounterButton/></div>)}functionCounter(){constcount=useAtomValue(countAtom)return<h1>{count}</h1>}functionCounterButton(){constsetCount=useAtomSet(countAtom)return(<buttononClick={()=>setCount((count)=>count+1)}>Increment</button>)}

Derived State

You can create derived state from an Atom in a couple of ways.

import{Atom}from"@effect-atom/atom-react"constcountAtom=Atom.make(0)// You can use the `get` function to get the value of another Atom.//// The type of `get` is `Atom.Context`, which also has a bunch of other methods// on it to manage Atoms.//constdoubleCountAtom=Atom.make((get)=>get(countAtom)*2)// You can also use the `Atom.map` function to create a derived Atom.consttripleCountAtom=Atom.map(countAtom,(count)=>count*3)

Working with Effects

You can also pass effects to the Atom.make function.

When working with effectful Atoms, you will get back a Result type.

You can see all the ways to work with Result here: https://tim-smart.github.io/effect-atom/atom/Result.ts.html

import{Atom,Result}from"@effect-atom/atom-react"import{Effect}from"effect"// ┌─── Atom.Atom<Result.Result<number>>// ▼constcountAtom=Atom.make(Effect.succeed(0))// You can also pass a function to get access to the `Atom.Context`//// `get.result` can be used in `Effect`s to get the value of an `Atom.Atom<Result.Result>`.//// ┌─── Atom.Atom<Result.Result<number>>// ▼constresultWithContextAtom=Atom.make(Effect.fnUntraced(function*(get: Atom.Context){constcount=yield*get.result(countAtom)returncount+1}),)

Working with scoped Effects

All Atoms that use effects are provided with a Scope, so you can add finalizers that will be run when the Atom is no longer used.

import{Atom}from"@effect-atom/atom-react"import{Effect}from"effect"constresultAtom=Atom.make(Effect.gen(function*(){// Add a finalizer to the `Scope` for this Atom// It will run when the Atom is rebuilt or no longer neededyield*Effect.addFinalizer(()=>Effect.log("finalizer"))return"hello"}),)

Working with Effect Services / Layers

import{Atom}from"@effect-atom/atom-react"import{Effect}from"effect"classUsersextendsEffect.Service<Users>()("app/Users",{effect: Effect.gen(function*(){constgetAll=Effect.succeed([{id: "1",name: "Alice"},{id: "2",name: "Bob"},{id: "3",name: "Charlie"},])return{ getAll }asconst}),}){}// Create a `AtomRuntime` from a `Layer`.//// ┌─── Atom.AtomRuntime<Users>// ▼construntimeAtom=Atom.runtime(Users.Default)// You can then use the `AtomRuntime` to make Atoms that use the services from the `Layer`.constusersAtom=runtimeAtom.atom(Effect.gen(function*(){constusers=yield*Usersreturnyield*users.getAll}),)

Adding global Layers to AtomRuntimes

This is useful for setting up Tracers, Loggers, ConfigProviders, etc.

import{Atom}from"@effect-atom/atom-react"import{ConfigProvider,Layer}from"effect"Atom.runtime.addGlobalLayer(Layer.setConfigProvider(ConfigProvider.fromJson(import.meta.env)),)

Working with Streams

import{Atom,Result,useAtom}from"@effect-atom/atom-react"import{Cause,Schedule,Stream}from"effect"// This will be a simple Atom that emits a incrementing number every second.//// Atom.make will give back the latest value of a `Stream` as a `Result`.//// ┌─── Atom.Atom<Result.Result<number>>// ▼constcountAtom=Atom.make(Stream.fromSchedule(Schedule.spaced(1000)))// You can use `Atom.pull` to create a specialized Atom that will pull from a `Stream`// one chunk at a time.//// This is useful for infinite scrolling or paginated data.//// With a `AtomRuntime`, you can use `runtimeAtom.pull` to create a pull Atom.//// ┌─── Atom.Writable<Atom.PullResult<number>, void>// ▼constcountPullAtom=Atom.pull(Stream.make(1,2,3,4,5))// Here is a component that uses `countPullAtom` to display the numbers in a list.//// You can use `useAtom` to both read the value of an Atom and gain access to the// setter function.//// Each time the setter function is called, it will pull a new chunk of data// from the `Stream`, and append it to the list.functionCountPullAtomComponent(){const[result,pull]=useAtom(countPullAtom)returnResult.builder(result).onInitial(()=><div>Loading...</div>).onFailure((cause)=><div>Error: {Cause.pretty(cause)}</div>).onSuccess(({ items },{ waiting })=>(<div><ul>{items.map((item)=>(<likey={item}>{item}</li>))}</ul><buttononClick={()=>pull()}>Load more</button>{waiting ? <p>Loading more...</p> : <p>Loaded chunk</p>}</div>)).render()}

Working with sets of Atoms

import{Atom}from"@effect-atom/atom-react"import{Effect}from"effect"classUsersextendsEffect.Service<Users>()("app/Users",{effect: Effect.gen(function*(){constfindById=(id: string)=>Effect.succeed({ id,name: "John Doe"})return{ findById }asconst}),}){}// Create a `AtomRuntime` from a `Layer`construntimeAtom=Atom.runtime(Users.Default)// Atoms work by reference, so we need to use `Atom.family` to dynamically create a// set of Atoms from a key.//// `Atom.family` will ensure that we get a stable reference to the Atom for each key.//// ┌─── (arg: string) => Atom.Atom<Result<{ id: string; name: string; }>>// ▼constuserAtom=Atom.family((id: string)=>runtimeAtom.atom(Effect.gen(function*(){constusers=yield*Usersreturnyield*users.findById(id)}),),)

Working with functions

import{Atom,useAtomSet}from"@effect-atom/atom-react"import{Effect,Exit}from"effect"// Create a simple `Atom.fn` that logs a numberconstlogAtom=Atom.fn(Effect.fnUntraced(function*(arg: number){yield*Effect.log("got arg",arg)}),)functionLogComponent(){// To call the `Atom.fn`, we need to use the `useAtomSet` hookconstlogNumber=useAtomSet(logAtom)return<buttononClick={()=>logNumber(42)}>Log42</button>}// You can also use it with `Atom.runtime`classUsersextendsEffect.Service<Users>()("app/Users",{effect: Effect.gen(function*(){constcreate=(name: string)=>Effect.succeed({id: 1, name })return{ create }asconst}),}){}construntimeAtom=Atom.runtime(Users.Default)// Here we are using `runtimeAtom.fn` to create a function from the `Users.create`// method.constcreateUserAtom=runtimeAtom.fn(Effect.fnUntraced(function*(name: string){constusers=yield*Usersreturnyield*users.create(name)}),)functionCreateUserComponent(){// If your function returns a `Result`, you can use the useAtomSet hook with `mode: "promiseExit"`constcreateUser=useAtomSet(createUserAtom,{mode: "promiseExit"})return(<buttononClick={async()=>{constexit=awaitcreateUser("John")if(Exit.isSuccess(exit)){console.log(exit.value)}}}>Createuser</button>)}

Wrapping an event listener

import{Atom}from"@effect-atom/atom-react"// This is a simple Atom that will emit the current scroll position of the// window.constscrollYAtom: Atom.Atom<number>=Atom.make((get)=>{// The handler will use `get.setSelf` to update the value of itselfconstonScroll=()=>{get.setSelf(window.scrollY)}// We need to use `get.addFinalizer` to remove the event listener when the// Atom is no longer used.window.addEventListener("scroll",onScroll)get.addFinalizer(()=>window.removeEventListener("scroll",onScroll))// Return the current scroll positionreturnwindow.scrollY})

Integration with search params

import{Atom}from"@effect-atom/atom-react"import{Option,Schema}from"effect"// Create an Atom that reads and writes to the URL search parameters.//// ┌─── Atom.Writable<string>// ▼constsimpleParamAtom=Atom.searchParam("paramName")// You can also use a schema to further parse the value//// ┌─── Atom.Writable<Option<number>>// ▼constnumberParamAtom=Atom.searchParam("paramName",{schema: Schema.NumberFromString,})

Integration with local storage

import{Atom}from"@effect-atom/atom-react"import{BrowserKeyValueStore}from"@effect/platform-browser"import{Schema}from"effect"construntime=Atom.runtime(BrowserKeyValueStore.layerLocalStorage)// Create an Atom that reads and writes to `localStorage`.//// It uses `Schema` to define the type of the value stored.//// ┌─── Atom.Writable<boolean, boolean>// ▼constflagAtom=Atom.kvs({runtime: runtime,key: "flag",schema: Schema.Boolean,defaultValue: ()=>false,})

Integration with Reactivity from @effect/experimental

Reactivity is an Effect service that allows you make queries reactive when mutations happen.

You can use an Atom.runtime to hook into the Reactivity service and trigger Atom refreshes when mutations happen.

import{Atom}from"@effect-atom/atom-react"import{Effect,Layer}from"effect"import{Reactivity}from"@effect/experimental"construntimeAtom=Atom.runtime(Layer.empty)leti=0// ┌─── Atom.Atom<number>// ▼constcount=Atom.make(()=>i++).pipe(// Refresh when the "counter" key changesAtom.withReactivity(["counter"]),// Or refresh when "counter" or "counter:1" or "counter:2" changesAtom.withReactivity({counter: [1,2],}),)constsomeMutation=runtimeAtom.fn(Effect.fn(function*(){yield*Effect.log("Mutating the counter")}),// Invalidate the "counter" key when the Effect is finished{reactivityKeys: ["counter"]},)constsomeMutationManual=runtimeAtom.fn(Effect.fn(function*(){yield*Effect.log("Mutating the counter again")// You can also manually invalidate the "counter" keyyield*Reactivity.invalidate(["counter"])}),)

@effect/rpc integration

You can use the AtomRpc module to create an RPC client with integration with effect-atom. It offers apis for both queries and mutations.

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"// Define the RPCsclassRpcsextendsRpcGroup.make(Rpc.make("increment"),Rpc.make("count",{success: Schema.Number})){}// Use `AtomRpc.Tag` to create a special `Context.Tag` that builds the RPC clientclassCountClientextendsAtomRpc.Tag<CountClient>()("CountClient",{group: Rpcs,// Provide a `Layer` that provides the RpcClient.Protocolprotocol: RpcClient.layerProtocolSocket({retryTransientErrors: true}).pipe(Layer.provide(BrowserSocket.layerWebSocket("ws://localhost:3000/rpc")),Layer.provide(RpcSerialization.layerJson))}){}functionSomeComponent(){// Use `CountClient.query` for readonly queriesconstcount=useAtomValue(CountClient.query("count",void0,{// You can also register reactivity keys, which can be used to invalidate// the queryreactivityKeys: ["count"]}))// Use `CountClient.mutation` for mutationsconstincrement=useAtomSet(CountClient.mutation("increment"))return(<div><p>Count: {Result.getOrElse(count,()=>0)}</p><buttononClick={()=>increment({payload: void0,// Mutations can also have reactivity keys, which will invalidate// the query when the mutation is done.reactivityKeys: ["count"]})}>Increment</button></div>)}// Or you can define custom atoms using the `CountClient.runtime`constincrementAtom=CountClient.runtime.fn(Effect.fnUntraced(function*(){constclient=yield*CountClient// Use the Tag to access the clientyield*client("increment",void0)}))// Or use it in your Effect servicesclassMyServiceextendsEffect.Service<MyService>()("MyService",{dependencies: [CountClient.layer],// Add the `CountClient` as a dependencyscoped: Effect.gen(function*(){constclient=yield*CountClient// Use the Tag to access the clientconstuseClient=()=>client("increment",void0)return{ useClient }asconst})}){}

HttpApi integration

You can use the AtomHttpApi module to create an HTTP API client with integration with effect-atom. It offers apis for both queries and mutations.

import{AtomHttpApi,Result,useAtomSet,useAtomValue}from"@effect-atom/atom-react"import{FetchHttpClient,HttpApi,HttpApiEndpoint,HttpApiGroup}from"@effect/platform"import{Effect,Schema}from"effect"// Define your apiclassApiextendsHttpApi.make("api").add(HttpApiGroup.make("counter").add(HttpApiEndpoint.get("count","/count").addSuccess(Schema.Number)).add(HttpApiEndpoint.post("increment","/increment"))){}// Use `AtomHttpApi.Tag` to create a special `Context.Tag` that builds the clientclassCountClientextendsAtomHttpApi.Tag<CountClient>()("CountClient",{api: Api,// Provide a Layer that provides the HttpClienthttpClient: FetchHttpClient.layer,baseUrl: "http://localhost:3000"}){}functionSomeComponent(){// Use `CountClient.query` for readonly queriesconstcount=useAtomValue(CountClient.query("counter","count",{// You can register reactivity keys, which can be used to invalidate// the queryreactivityKeys: ["count"]}))// Use `CountClient.mutation` for mutationsconstincrement=useAtomSet(CountClient.mutation("counter","increment"))return(<div><p>Count: {Result.getOrElse(count,()=>0)}</p><buttononClick={()=>increment({payload: void0,// Mutations can also have reactivity keys, which will invalidate// the query when the mutation is done.reactivityKeys: ["count"]})}>Increment</button></div>)}// Or you can define custom atoms using the `CountClient.runtime`constincrementAtom=CountClient.runtime.fn(Effect.fnUntraced(function*(){constclient=yield*CountClient// Use the Tag to access the clientyield*client.counter.increment()}))// Or use it in your Effect servicesclassMyServiceextendsEffect.Service<MyService>()("MyService",{dependencies: [CountClient.layer],// Add the `CountClient` as a dependencyscoped: Effect.gen(function*(){constclient=yield*CountClient// Use the Tag to access the clientconstuseClient=()=>client.counter.increment()return{ useClient }asconst})}){}

Releases

Packages

Used by

Contributors

Languages