Introduction
effstate is a state machine library built on Effect, designed for TypeScript applications that need robust, type-safe state management.
Why Effect.Service?
The recommended pattern for using effstate is to wrap your machines in Effect.Service. This approach provides:
- Dependency Injection - Your machine can access other services (API clients, databases, etc.)
- Testability - Swap implementations by providing different layers in tests
- Composability - Services automatically compose their dependency trees
- Type Safety - Full inference for dependencies and effects
import { createMachine, interpret, assign, effect, invoke } from "effstate";import { Data, Effect, Schema } from "effect";
// Define eventsclass Connect extends Data.TaggedClass("CONNECT")<{}> {}class Disconnect extends Data.TaggedClass("DISCONNECT")<{}> {}
type ConnectionEvent = Connect | Disconnect;
// Define context schemaconst ContextSchema = Schema.Struct({ retryCount: Schema.Number, status: Schema.String,});
// Example dependencyclass ApiClient extends Effect.Service<ApiClient>()("ApiClient", { succeed: { connect: () => Effect.tryPromise(() => fetch("/api/connect")), },}) {}
// Define your machine as a serviceexport class ConnectionMachine extends Effect.Service<ConnectionMachine>()( "ConnectionMachine", { effect: Effect.gen(function* () { const api = yield* ApiClient; // Inject dependency
const machine = createMachine({ id: "connection", initial: "disconnected", context: ContextSchema, initialContext: { retryCount: 0, status: "idle" }, states: { disconnected: { on: { CONNECT: { target: "connecting" }, }, }, connecting: { invoke: invoke({ src: () => api.connect(), // Use injected service onSuccess: { target: "connected" }, onFailure: { target: "disconnected", actions: [ assign(({ context }) => ({ retryCount: context.retryCount + 1, status: "failed", })), ], }, }), }, connected: { entry: [assign({ status: "connected", retryCount: 0 })], on: { DISCONNECT: { target: "disconnected" }, }, }, }, });
return { machine, createActor: () => interpret(machine), }; }), dependencies: [ApiClient.Default], }) {}Key Features
Effect-First Architecture
Built on Effect for composable, type-safe side effects with proper error handling.
Schema-First Context
Effect Schema is required for context - enabling automatic serialization, cross-tab sync, and validation out of the box.
Actor Model
Parent-child machine composition with proper lifecycle management. Spawn, communicate with, and stop child machines.
React Integration
First-class React support via @effstate/react with hooks and atoms.
Full Type Safety
Compile-time guarantees for events, states, and context. Event handlers receive properly narrowed event types.
Core Concepts
Events
Events trigger transitions. Define them using Effect’s Data.TaggedClass:
class Increment extends Data.TaggedClass("INCREMENT")<{ amount: number }> {}class Reset extends Data.TaggedClass("RESET")<{}> {}
type CounterEvent = Increment | Reset;States
States are string values representing the machine’s current status. Each state can have:
- Entry actions - Run when entering the state
- Exit actions - Run when leaving the state
- Transitions - Event handlers that move to other states
- Activities - Long-running effects that run while in the state
- Invocations - One-shot async operations
Context
Context is the machine’s extended state - data that persists across transitions. Define it with Effect Schema:
const ContextSchema = Schema.Struct({ count: Schema.Number, lastUpdated: Schema.DateFromString,});Actions
Actions are side effects that run during transitions:
assign- Update contexteffect- Run an Effectraise- Send an event to selfsendTo/sendParent- Communicate with other machinesspawnChild/stopChild- Manage child machines
Next Steps
- Installation - Get effstate set up
- Quick Start - Build your first machine
- Actions Guide - Learn all available actions
- Comparison - See how effstate compares to XState