I'm working on a state machine to handle uploading a file. During uploading, I'd like to show the user the progress. Here's what I have so far:
// machine.tsx
import upload from './actors/upload'
const machine = createMachine({
id: 'Document Upload Machine',
context: ({ input: { file } }) => ({
created: Date.now(),
file,
name: getDocumentTitle(file.name)
}),
initial: 'init',
states: {
init: {},
uploading: {
entry: 'resetProgress',
invoke: {
src: 'upload',
input: ({ context: { file, upload } }) => ({ file, urls: upload.urls }),
onDone: {
target: 'startProcessing',
actions: assign({ upload: ({ context, event }) => ({ ...context.upload, parts: event.output }) }),
},
onError: {
target: 'failed',
actions: assign({ error: 'upload' }),
},
},
on: {
PROGRESS: {
actions: {
type: 'setProgress',
params: ({ event }) => ({ value: event.value })
}
}
}
},
...
}
}, {
actions: {
resetProgress: assign({ progress: 0 }),
setProgress: assign({ progress: (_, { value }) => Math.round(value) }),
},
actors: {
upload
}
})
// actors/upload.tsx
interface Input {
file: File
urls: string[]
}
const upload = fromPromise<IMultipartPart[], Input>(async ({ input: { file, urls }, self }) => {
const partCount = urls.length
const total = file.size
const overallProgress = Array.from({ length: partCount }).map(() => 0)
const _onUploadProgress = (part: number) => (event: ProgressEvent) => {
overallProgress[part - 1] = event.loaded
const loaded = overallProgress.reduce((loaded, part) => loaded + part, 0)
console.log('Sending progress to parent', (loaded * 100) / total)
sendParent({ type: 'PROGRESS', value: (loaded * 100) / total })
}
const parts = await Promise.all(
urls.map(async (url, index) => {
const partNumber = index + 1
return uploadPart(
url,
{ file, partCount, partNumber },
{ onUploadProgress: _onUploadProgress(partNumber) }
)
})
)
return parts
})
While I'm seeing Sending progress to parent
in the console, a breakpoint in the PROGRESS
event for the uploading
state is not hit, showing that the event is not being picked up by the parent machine, so the progress never gets updated. I've scoured the v5 documentation, the jsdocs site for the API, the xstate GitHub issues and discussions and pretty much exhausted Google searching for an answer on how to properly send events from an Actor to Parent.
To my understanding, in xstate v5, a Promise Actor can send events to its parent via sendParent
. What am I doing wrong or misunderstanding?
I posted this question in the GitHub Discussions for xstate. They responded with two approaches... the first matching what @NRielingh is saying. The second, and this is their suggested approach, is to use the new Actor System to send messages between Actors.
For my use case, here's how that looks...
There's a parent machine responsible for spawning Document Upload Machines
for each file being uploaded.
// Parent machine
const parent = createMachine(
{
...
},
{
actions: {
addMachine: assign(({ context, spawn }, { file }) => {
// This is used as both the `id` and the `systemId`
const id = createId()
return {
machines: context.machines.concat(
spawn(documentUploadMachine, { id, input: { file }, systemId: id })
)
}
})
}
}
)
systemId
to actor// Document Upload Machine
const documentUploadMachine = createMachine({
...
states: {
...
uploading: {
invoke: {
src: 'upload',
input: ({ context, self }) => ({
file: context.file,
// self.id is the same as the systemId assigned during spawn
parentId: self.id,
urls: context.uploads.urls
})
...
}
}
...
}
})
parent.send
to send to parent actor// ./actors/upload.ts
interface Input {
file: File
parentId: string
urls: string[]
}
const upload = fromPromise<IMultipartPart[], Input>(
async ({ input: { file, parentId, urls }, self, system }) => {
const parent = system.get(parentId)
...
const _onUploadProgress = (part: number) => (event: ProgressEvent) => {
overallProgress[part - 1] = event.loaded
const loaded = overallProgress.reduce((loaded, part) => loaded + part, 0)
parent.send({ type: 'PROGRESS', value: (loaded * 100) / total })
}
...
}
)