Upload file XState form actors with Effect

Languages

typescript5.8.3

Libraries

effect3.14.20
nodejs22.8.6
react19.1.0
xstate5.19.2
GithubCode

Using actors with XState to implement a <form> to upload a file (<input> with type="file").

The logic to upload the file is separate inside upload-file-machine.ts. This machine is only responsible to handle file inputs.

It implements the following events:

  • file.upload: when a user selects a file from the input
  • file.remove: allows to remove the current uploaded file

This machine (actor) has a single responsibility: upload the file.

It then interacts with another parent actor that is responsible to submit the form.

A reference to the parent actor is provided as input to the machine (parentId of type AnyActorRef).

The child machine notifies the parent by sending shared events (UploadFileEvent events sent using sendTo).

Parent machine

The form-machine.ts stores a reference to the child inside its context (uploadImage of type ActorRefFrom<typeof UploadFileMachine.machine>).

When the machine starts, an instance of the child machine is spawned inside context using spawn.

The parent provides an instance of itself as input to the child (self).

It will then respond to the events from the child actor and store the imageUrl sent by the child.

Each actor is responsible for a specific functionality, and they all interact by sending events between each other.
Each actor is responsible for a specific functionality, and they all interact by sending events between each other.
import { assign, setup, type ActorRefFrom } from "xstate";

import * as UploadFileMachine from "./upload-file-machine";

export const machine = setup({
  types: {
    /** Events shared with the child machine */
    events: {} as UploadFileMachine.UploadFileEvent,

    context: {} as {
      /** Reference to the child machine that handles the file upload */
      uploadImage: ActorRefFrom<typeof UploadFileMachine.machine>;
      imageUrl: string | null;
    },
  },
}).createMachine({
  context: ({ self, spawn }) => ({
    imageUrl: null,

    /** Spawn the child machine and pass a reference to itself */
    uploadImage: spawn(UploadFileMachine.machine, {
      input: { parentId: self },
    }),
  }),
  initial: "Editing",
  states: {
    Editing: {
      /** Events sent by the child machine */
      on: {
        "file.remove": {
          actions: assign({ imageUrl: null }),
        },
        "file.upload": {
          actions: assign(({ event }) => ({ imageUrl: event.fileUrl })),
        },
      },
    },
  },
});