Quick Start
This guide walks you through building a counter state machine using the recommended Effect.Service pattern.
1. Define Your Events
Events are defined using Effect’s Data.TaggedClass:
import { Data } from "effect";
class Increment extends Data.TaggedClass("INCREMENT")<{}> {}class Decrement extends Data.TaggedClass("DECREMENT")<{}> {}class Reset extends Data.TaggedClass("RESET")<{}> {}
type CounterEvent = Increment | Decrement | Reset;2. Define Your Context Schema
Context is defined using Effect Schema for type-safe serialization:
import { Schema } from "effect";
const CounterContextSchema = Schema.Struct({ count: Schema.Number, lastUpdated: Schema.DateFromString,});
type CounterContext = typeof CounterContextSchema.Type;3. Create Your Machine Service
Wrap your machine in an Effect.Service for proper dependency injection:
import { createMachine, interpret, assign, effect } from "effstate";import { Effect } from "effect";
export class CounterMachine extends Effect.Service<CounterMachine>()( "CounterMachine", { effect: Effect.gen(function* () { const machine = createMachine< "idle" | "active", CounterEvent, typeof CounterContextSchema >({ id: "counter", initial: "idle", context: CounterContextSchema, initialContext: { count: 0, lastUpdated: new Date(), }, states: { idle: { on: { INCREMENT: { target: "active", actions: [ assign(({ context }) => ({ count: context.count + 1, lastUpdated: new Date(), })), ], }, }, }, active: { on: { INCREMENT: { actions: [ assign(({ context }) => ({ count: context.count + 1, lastUpdated: new Date(), })), ], }, DECREMENT: { actions: [ assign(({ context }) => ({ count: context.count - 1, lastUpdated: new Date(), })), ], }, RESET: { target: "idle", actions: [assign({ count: 0 })], }, }, }, }, });
return { machine, createActor: () => interpret(machine), }; }), }) {}4. Use Your Machine
const program = Effect.gen(function* () { const counterService = yield* CounterMachine; const actor = yield* counterService.createActor();
// Subscribe to state changes actor.subscribe((snapshot) => { console.log(`State: ${snapshot.value}, Count: ${snapshot.context.count}`); });
// Send events actor.send(new Increment()); // idle -> active, count: 1 actor.send(new Increment()); // active, count: 2 actor.send(new Decrement()); // active, count: 1 actor.send(new Reset()); // active -> idle, count: 0
return actor.getSnapshot();});
// Run with dependenciesEffect.runPromise( program.pipe( Effect.scoped, Effect.provide(CounterMachine.Default) ));5. Add External Dependencies
The power of Effect.Service is dependency injection. Add an API client:
class AnalyticsService extends Effect.Service<AnalyticsService>()( "AnalyticsService", { succeed: { track: (event: string) => Effect.log(`Analytics: ${event}`), }, }) {}
export class CounterMachine extends Effect.Service<CounterMachine>()( "CounterMachine", { effect: Effect.gen(function* () { const analytics = yield* AnalyticsService; // Inject dependency
const machine = createMachine({ // ... same config as before states: { active: { entry: [ effect(() => analytics.track("counter_activated")), ], // ... }, }, });
return { machine, createActor: () => interpret(machine), }; }), dependencies: [AnalyticsService.Default], // Declare dependencies }) {}6. Use with React (Optional)
import { createUseMachineHook } from "@effstate/react";import { Atom } from "@effect-atom/atom-react";import { Layer, SubscriptionRef } from "effect";
// Create app runtime with service layersconst AppLayer = Layer.mergeAll(CounterMachine.Default);const appRuntime = Atom.runtime(AppLayer);
// Create atoms for the machineconst actorAtom = appRuntime.atom( Effect.gen(function* () { const service = yield* CounterMachine; return yield* service.createActor(); })).pipe(Atom.keepAlive);
const snapshotAtom = appRuntime.subscriptionRef((get) => Effect.gen(function* () { const actor = yield* get.result(actorAtom); const ref = yield* SubscriptionRef.make(actor.getSnapshot()); actor.subscribe((snapshot) => { Effect.runSync(SubscriptionRef.set(ref, snapshot)); }); return ref; })).pipe(Atom.keepAlive);
// Create the hookconst useCounterMachine = createUseMachineHook( actorAtom, snapshotAtom, { value: "idle", context: { count: 0, lastUpdated: new Date() }, event: null });
// Use in a componentfunction Counter() { const { snapshot, context, send, isLoading } = useCounterMachine();
if (isLoading) return <div>Loading...</div>;
return ( <div> <p>State: {snapshot.value}</p> <p>Count: {context.count}</p> <button onClick={() => send(new Increment())}>+</button> <button onClick={() => send(new Decrement())}>-</button> <button onClick={() => send(new Reset())}>Reset</button> </div> );}Next Steps
- Learn about Guards for conditional transitions
- Explore Activities for long-running effects
- See Invoke for async operations
- Read about Parent-Child Machines for composition