Stately
XState v6 alpha

Spawn actors

Create child actors during a transition.

XState v6 is in alpha

APIs and behavior may change before the stable release.

Use enq.spawn(...) when a child should outlive the state that created it, or when the number of children is dynamic.

on: {
  'file.added': ({ context, event }, enq) => {
    const upload = enq.spawn(uploadLogic, {
      id: `upload-${event.fileId}`,
      input: { file: event.file }
    });

    return {
      context: {
        ...context,
        uploads: [...context.uploads, upload]
      }
    };
  }
}

enq.spawn(logic, options) returns the actor reference immediately, so the same transition can store it, send to it or subscribe to it.

Spawn options

OptionDescription
idIdentifier for the child, and its key in snapshot.children. Defaults to a generated id such as x:1.
inputInput for the child. Required when its logic requires input.
registryKeyRegisters the child in the actor registry under that key.
syncSnapshotWhen true, each child snapshot is sent to the parent as an xstate.snapshot.actor event.

The first argument is actor logic, never a registered name. Read logic from actors when it is registered:

entry: ({ actors }, enq) => {
  enq.spawn(actors.upload, { input: { file } });
};

Spawning with an id that a live child already uses replaces that entry in children; the previous actor is left running and unreferenced. Generate ids from something stable, such as a file or participant id.

Where you can spawn

enq.spawn(...) is available in every transition function: entry, exit, on, always and after. A child spawned in exit still starts, because the spawn is part of the transition, not part of the state being left.

The context initializer also receives a spawn function for children that exist from the start:

const machine = createMachine({
  actors: { connection },
  context: ({ spawn, actors }) => ({
    connection: spawn(actors.connection, { id: 'connection' })
  })
});

Spawn or invoke

InvokeSpawn
Started byEntering a stateA transition function
Stopped byExiting that stateenq.stop(ref), or the parent stopping
How manyFixed by the configAny number, decided at runtime
Outcome handlingonDone, onError, onSnapshotenq.subscribeTo(...), enq.listen(...)
PersistenceRestored with the parentSee below

Invoke a payment actor for the authorizing state. Spawn one upload actor per selected file, one participant actor per person in a call, or one track actor per queued item in a media player.

Referencing spawned children

Spawned children appear on snapshot.children under their id and are passed to transition functions as children:

on: {
  'upload.cancel': ({ children, event }, enq) => {
    enq.sendTo(children[`upload-${event.fileId}`], { type: 'cancel' });
  }
}

Store references in context instead when the machine needs its own ordering, grouping or metadata, such as an array of uploads rendered in order. Keep the context list and the children record in sync: children only reflects live actors.

Stopping spawned actors

A spawned actor runs until it completes, until enq.stop(ref) stops it, or until the parent actor stops. Exiting the state that spawned it does not stop it.

on: {
  'upload.remove': ({ context, event }, enq) => {
    const upload = context.uploads.find((ref) => ref.id === event.id);
    enq.stop(upload);

    return {
      context: {
        ...context,
        uploads: context.uploads.filter((ref) => ref !== upload)
      }
    };
  }
}

enq.stop(ref) removes the child from snapshot.children in the same transition. Remove the stored reference from context at the same time, otherwise the machine holds a reference to a stopped actor. A machine can only stop its own children; stopping any other actor reference errors.

Communicating with spawned actors

Send events to a child with enq.sendTo(ref, event). A child machine sends events back through its parent argument:

const uploadMachine = createMachine({
  on: {
    progress: ({ parent, event }, enq) => {
      enq.sendTo(parent, { type: 'uploadProgress', value: event.value });
    }
  }
});

Spawned children have no onDone or onError. Subscribe to their outcome instead:

entry: (_, enq) => {
  const upload = enq.spawn(uploadLogic, { id: 'upload' });

  enq.subscribeTo(upload, {
    done: (output) => ({ type: 'uploadFinished', output }),
    error: (error) => ({ type: 'uploadFailed', error })
  });

  enq.listen(upload, 'upload.*', (event) => ({
    type: 'uploadEvent',
    eventType: event.type
  }));
};

enq.listen(...) maps emitted events, enq.subscribeTo(...) maps snapshots and outcomes. Both return an actor that can be stopped with enq.stop(...), and both are available in entry and exit functions. See listen and subscribe.

syncSnapshot: true is the lower-level alternative: the parent then receives xstate.snapshot.actor events that it can handle with matches: { actorId }.

Persistence

Children spawned in the context initializer from logic registered in actors are persisted and restored with the parent snapshot:

const machine = createMachine({
  actors: { connection },
  context: ({ spawn, actors }) => ({
    connection: spawn(actors.connection, { id: 'connection' })
  })
});

The child records src: 'connection', which createActor(machine, { snapshot }) resolves back to the registered logic. Restoring a snapshot whose child source is not registered fails instead of silently dropping the child.

Warning: Children created with enq.spawn(...) are stored by logic value, not by source name, so they cannot be persisted. getPersistedSnapshot() throws An inline child actor cannot be persisted. in development while such a child is running. Invoke the child, or spawn it in the context initializer, when the machine must be persisted.

TypeScript

enq.spawn(...) returns an actor reference typed from the logic, and requires input when the logic requires it. Type references stored in context with ActorRefFrom:

const machine = createMachine({
  schemas: {
    context: z.object({
      uploads: z.custom<ActorRefFrom<typeof uploadLogic>[]>()
    })
  },
  context: { uploads: [] }
});

Declare schemas.children to type the children record for known ids:

schemas: {
  children: {
    connection: z.custom<ActorRefFromLogic<typeof connection>>()
  }
}

children.connection is then typed, and unknown keys are type errors. Dynamic children keyed by a runtime id stay typed through the context array instead.

Spawn cheatsheet

const child = enq.spawn(logic, { id, input, registryKey, syncSnapshot: true });
enq.sendTo(child, { type: 'start' });
enq.subscribeTo(child, { done: (output) => ({ type: 'finished', output }) });
enq.stop(child);

On this page