Documentation Index
Fetch the complete documentation index at: https://mintlify.com/statelyai/xstate/llms.txt
Use this file to discover all available pages before exploring further.
The fromCallback() function creates actor logic from a callback function. This is useful for subscription-based or event-driven logic that can send events back to the parent actor.
Signature
function fromCallback<
TEvent extends EventObject,
TInput = NonReducibleUnknown,
TEmitted extends EventObject = EventObject
>(
callback: ({
input,
system,
self,
sendBack,
receive,
emit
}: {
input: TInput;
system: AnyActorSystem;
self: CallbackActorRef<TEvent>;
sendBack: (event: TSentEvent) => void;
receive: (listener: (event: TEvent) => void) => void;
emit: (emitted: TEmitted) => void;
}) => (() => void) | void
): CallbackActorLogic<TEvent, TInput, TEmitted>;
Parameters
A function that defines the callback logic. Receives an object with:Data provided to the callback actor when created or invoked.
Reference to the callback actor itself.
The actor system to which the callback actor belongs.
sendBack
(event: TSentEvent) => void
Function to send events back to the parent actor.
receive
(listener: (event: TEvent) => void) => void
Function to register a listener for events received by the actor.
emit
(emitted: TEmitted) => void
Function to emit custom events to subscribers.
The callback can optionally return a cleanup function that will be called when the actor is stopped.
Returns
Actor logic that can be used with createActor() or invoked in a state machine.
Usage
Basic Example
import { fromCallback, createActor } from 'xstate';
const callbackLogic = fromCallback(({ sendBack }) => {
const handler = (event: MouseEvent) => {
sendBack({ type: 'CLICK', x: event.clientX, y: event.clientY });
};
document.addEventListener('click', handler);
// Cleanup function
return () => {
document.removeEventListener('click', handler);
};
});
const actor = createActor(callbackLogic);
actor.start();
Receiving Events
import { fromCallback, createActor } from 'xstate';
type Event =
| { type: 'LOCK' }
| { type: 'UNLOCK' }
| { type: 'CLICK'; x: number; y: number };
const callbackLogic = fromCallback<Event>(({ sendBack, receive }) => {
let isLocked = false;
receive((event) => {
if (event.type === 'LOCK') {
isLocked = true;
} else if (event.type === 'UNLOCK') {
isLocked = false;
}
});
const handler = (e: MouseEvent) => {
if (!isLocked) {
sendBack({ type: 'CLICK', x: e.clientX, y: e.clientY });
}
};
document.addEventListener('click', handler);
return () => {
document.removeEventListener('click', handler);
};
});
const actor = createActor(callbackLogic);
actor.start();
actor.send({ type: 'LOCK' });
// Clicks are now ignored
actor.send({ type: 'UNLOCK' });
// Clicks are now sent back
Invoking in a Machine
import { setup, fromCallback } from 'xstate';
const websocketLogic = fromCallback<
{ type: 'WS.MESSAGE'; data: unknown },
{ url: string }
>(({ input, sendBack }) => {
const ws = new WebSocket(input.url);
ws.addEventListener('message', (event) => {
sendBack({ type: 'WS.MESSAGE', data: event.data });
});
ws.addEventListener('open', () => {
sendBack({ type: 'WS.OPEN' });
});
ws.addEventListener('error', () => {
sendBack({ type: 'WS.ERROR' });
});
return () => {
ws.close();
};
});
const machine = setup({
actors: {
websocket: websocketLogic
}
}).createMachine({
initial: 'connecting',
states: {
connecting: {
invoke: {
src: 'websocket',
input: { url: 'wss://example.com/socket' }
},
on: {
'WS.OPEN': 'connected',
'WS.ERROR': 'failed'
}
},
connected: {
on: {
'WS.MESSAGE': {
actions: ({ event }) => {
console.log('Received:', event.data);
}
},
'WS.ERROR': 'failed'
}
},
failed: {}
}
});
Interval Timer
import { fromCallback, createActor } from 'xstate';
type Event = { type: 'TICK' };
type Input = { interval: number };
const intervalLogic = fromCallback<Event, Input>(({ input, sendBack }) => {
const intervalId = setInterval(() => {
sendBack({ type: 'TICK' });
}, input.interval);
return () => {
clearInterval(intervalId);
};
});
const actor = createActor(intervalLogic, {
input: { interval: 1000 }
});
actor.start();
actor.subscribe((snapshot) => {
console.log('Actor status:', snapshot.status);
});
// Stop after 5 seconds
setTimeout(() => {
actor.stop();
}, 5000);
Geolocation Tracking
import { fromCallback } from 'xstate';
type Event =
| { type: 'POSITION'; coords: GeolocationCoordinates }
| { type: 'ERROR'; error: GeolocationPositionError };
const geolocationLogic = fromCallback<Event>(({ sendBack }) => {
const watchId = navigator.geolocation.watchPosition(
(position) => {
sendBack({ type: 'POSITION', coords: position.coords });
},
(error) => {
sendBack({ type: 'ERROR', error });
}
);
return () => {
navigator.geolocation.clearWatch(watchId);
};
});
Resize Observer
import { fromCallback } from 'xstate';
type Event = {
type: 'RESIZE';
width: number;
height: number;
};
type Input = {
element: HTMLElement;
};
const resizeLogic = fromCallback<Event, Input>(({ input, sendBack }) => {
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
sendBack({ type: 'RESIZE', width, height });
}
});
observer.observe(input.element);
return () => {
observer.disconnect();
};
});
Message Channel
import { fromCallback } from 'xstate';
type Event = { type: 'MESSAGE'; data: unknown };
type Input = { port: MessagePort };
const messageChannelLogic = fromCallback<Event, Input>(
({ input, sendBack, receive }) => {
const { port } = input;
port.addEventListener('message', (event) => {
sendBack({ type: 'MESSAGE', data: event.data });
});
port.start();
receive((event) => {
if (event.type === 'SEND') {
port.postMessage((event as any).data);
}
});
return () => {
port.close();
};
}
);
Emitting Events
import { fromCallback, createActor } from 'xstate';
type EmittedEvent =
| { type: 'heartbeat' }
| { type: 'metric'; name: string; value: number };
const monitorLogic = fromCallback<never, void, EmittedEvent>(
({ emit }) => {
const heartbeatId = setInterval(() => {
emit({ type: 'heartbeat' });
}, 1000);
const metricId = setInterval(() => {
emit({
type: 'metric',
name: 'memory',
value: performance.memory?.usedJSHeapSize ?? 0
});
}, 5000);
return () => {
clearInterval(heartbeatId);
clearInterval(metricId);
};
}
);
const actor = createActor(monitorLogic);
actor.on('heartbeat', () => {
console.log('♥');
});
actor.on('metric', (event) => {
console.log(`${event.name}: ${event.value}`);
});
actor.start();
Snapshot
The callback actor snapshot has the following structure:
interface CallbackSnapshot<TInput> {
status: 'active' | 'stopped';
output: undefined;
error: undefined;
input: TInput;
}
Status Values
'active' - Actor is running
'stopped' - Actor has been stopped
Behavior
- Continuous execution: Callback actors run continuously until stopped
- Bidirectional communication: Can both receive and send events
- Cleanup support: Cleanup function is called when actor is stopped
- No output: Callback actors don’t produce a final output value
- Event handling: Can register listeners via
receive() to handle incoming events
Type Parameters
The type of events the actor can receive.
TInput
type
default:"NonReducibleUnknown"
The type of the input data.
TEmitted
EventObject
default:"EventObject"
The type of events that can be emitted.
Best Practices
- Always clean up: Return a cleanup function to remove event listeners, clear intervals, etc.
- Handle errors: Wrap potentially failing operations in try-catch and send error events
- Type events: Use discriminated unions for event types
- Avoid memory leaks: Ensure all subscriptions are properly cleaned up
- Use receive() for control: Implement control logic by listening to received events
See Also