Activities
Activities are long-running effects that start when entering a state and automatically stop when leaving it. They’re perfect for:
- Polling or periodic updates
- Animations
- WebSocket connections
- Real-time data streams
Basic Activity
import { createMachine } from "effstate";import { Effect } from "effect";
const machine = createMachine({ id: "polling", initial: "idle", context: Schema.Struct({ data: Schema.Unknown }), initialContext: { data: null }, states: { idle: { on: { START: { target: "polling" }, }, }, polling: { activities: [ { id: "pollData", src: ({ context, send }) => Effect.gen(function* () { while (true) { yield* Effect.sleep("5 seconds"); const data = yield* fetchLatestData(); send(new DataReceived({ data })); } }), }, ], on: { DATA_RECEIVED: { actions: [assign(({ event }) => ({ data: event.data }))], }, STOP: { target: "idle" }, }, }, },});Activity Parameters
Activities receive three parameters:
{ id: "myActivity", src: ({ context, event, send }) => Effect.gen(function* () { // context - current machine context // event - the event that triggered entry to this state // send - function to send events back to the machine }),}Animation Example
Activities work great for animations:
const hamsterMachine = createMachine({ id: "hamster", initial: "idle", context: ContextSchema, initialContext: { rotation: 0, speed: 0 }, states: { idle: { on: { START: { target: "running" }, }, }, running: { activities: [ { id: "spinWheel", src: ({ context, send }) => Effect.gen(function* () { const startTime = Date.now(); while (true) { yield* Effect.sleep("16 millis"); // ~60fps const elapsed = Date.now() - startTime; const rotation = (elapsed * context.speed) % 360; send(new UpdateRotation({ rotation })); } }), }, ], on: { UPDATE_ROTATION: { actions: [assign(({ event }) => ({ rotation: event.rotation }))], }, STOP: { target: "idle" }, }, }, },});Multiple Activities
A state can have multiple activities running concurrently:
states: { active: { activities: [ { id: "heartbeat", src: () => Effect.gen(function* () { while (true) { yield* Effect.sleep("30 seconds"); yield* sendHeartbeat(); } }), }, { id: "dataSync", src: ({ send }) => Effect.gen(function* () { while (true) { yield* Effect.sleep("5 seconds"); const data = yield* fetchUpdates(); send(new DataUpdated({ data })); } }), }, ], },}WebSocket Connection
Activities are ideal for WebSocket connections:
export class ChatMachine extends Effect.Service<ChatMachine>()("ChatMachine", { effect: Effect.gen(function* () { const wsService = yield* WebSocketService;
const machine = createMachine({ id: "chat", initial: "disconnected", context: ContextSchema, initialContext: { messages: [] }, states: { disconnected: { on: { CONNECT: { target: "connected" } }, }, connected: { activities: [ { id: "websocket", src: ({ send }) => Effect.gen(function* () { const ws = yield* wsService.connect("/chat");
// Listen for messages yield* ws.onMessage((msg) => { send(new MessageReceived({ message: msg })); });
// Keep alive yield* Effect.never; }), }, ], on: { MESSAGE_RECEIVED: { actions: [ assign(({ context, event }) => ({ messages: [...context.messages, event.message], })), ], }, DISCONNECT: { target: "disconnected" }, }, }, }, });
return { machine, createActor: () => interpret(machine) }; }), dependencies: [WebSocketService.Default],}) {}Automatic Cleanup
Activities are automatically interrupted when:
- The machine transitions to a different state
- The machine is stopped
- The parent scope is closed
Effect’s fiber management ensures proper cleanup:
{ id: "resourceActivity", src: () => Effect.gen(function* () { // Acquire resource const connection = yield* acquireConnection();
// This runs when the activity is interrupted yield* Effect.addFinalizer(() => Effect.gen(function* () { yield* connection.close(); yield* Effect.log("Connection closed"); }) );
// Activity logic yield* Effect.never; }),}Activity vs Invoke
Use activities when:
- The effect should run for the duration of a state
- You need ongoing polling or streaming
- The effect doesn’t have a natural completion point
Use invoke when:
- You’re performing a one-shot async operation
- You want to handle success/failure with different transitions
- The effect will complete and you need to react to the result
// Activity: continuous polling while in stateactivities: [ { id: "poll", src: () => Effect.forever( Effect.delay(fetchData(), "5 seconds") ), },]
// Invoke: one-shot operation with result handlinginvoke: invoke({ src: () => fetchUser(), onSuccess: { target: "ready" }, onFailure: { target: "error" },})