Skip to content

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 events
class Connect extends Data.TaggedClass("CONNECT")<{}> {}
class Disconnect extends Data.TaggedClass("DISCONNECT")<{}> {}
type ConnectionEvent = Connect | Disconnect;
// Define context schema
const ContextSchema = Schema.Struct({
retryCount: Schema.Number,
status: Schema.String,
});
// Example dependency
class ApiClient extends Effect.Service<ApiClient>()("ApiClient", {
succeed: {
connect: () => Effect.tryPromise(() => fetch("/api/connect")),
},
}) {}
// Define your machine as a service
export 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 context
  • effect - Run an Effect
  • raise - Send an event to self
  • sendTo / sendParent - Communicate with other machines
  • spawnChild / stopChild - Manage child machines

Next Steps