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?
Was having the same problem today. I'm an xstate noob but I'm pretty sure I understand what's happening here, so this answer is just as much to get my thinking straight as it is to help you out.
First, and your main problem: the sendTo() and sendParent() functions are not imperative side-effects that you can use to communicate directly from a "promise actor" (really an Actor based on PromiseActorLogic) to a parent actor. These functions return a SendToAction type which can be used to configure an action in a state machine configuration. Since your PromiseActorLogic is not a StateMachine, it doesn't apply here.
As I understand it, fromPromise() is intended to be used when you just have a straightforward bit of promise logic and only need to define transitions for when the promise resolves or rejects -- using the onDone and onError events and transitions that you have already found. For sending back arbitrary events, the Callback Actor may be a better choice, since it provides you with a sendBack() function, which does exactly what you want -- sends an event back to the parent actor.
Callback Actors don't mean you are unable to use async/await -- an async function returns a promise, and promises are compatible with callbacks, so you can write your async function and call it without await -- it will return a promise which you can .then() and .catch() as needed.
Despite Promise Actors' intention to only return onDone and onError events, you can get around this and send arbitrary events to the parent by including a reference to the parent in the actor's input.
const machine = createMachine({
states: {
uploading: {
invoke: {
src: 'upload',
input: ({ self }) => ({ parent: self }),
...
const upload = fromPromise(async ({ input }) => {
input.parent.send({ type: 'PROGRESS' });
});
Note that you MUST capture the state machine's self as parent in input, since the self passed to fromPromise() is the child actor (the promise actor's self).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With