#I am confused

1 messages · Page 1 of 1 (latest)

fresh rain
#

Infact there are several things that confuse me with xstate but the one I am frustrated with at this moment is with an invoked service...

How is it possible that the onDone event transitions to the next state BEFORE the service gets a response ???

Example here is the service ...

const postEnrolmentForm = (enrolment: TEnrolment) => {
  const progressHandler = ({progress}: AxiosProgressEvent) => {
    console.log('Upload Enrolment Form Progress:', progress);
    send({ type: 'UPLOAD_PROGRESS', progress: Math.ceil((progress || 0) * 100)}); // progress is between 0 and 1
  };
  return axios.post(`${baseUrl}/enrol`, enrolment, {
    onUploadProgress: (progress) => progressHandler(progress),
  }).then(res => {
    console.log('Got Back from /enrol : ', res.data);
    return res.data
  })
};

And here is the invoke....

              submitForm: {
                  invoke: {
                    id: 'postSubmitForm',
                    src: 'postEnrolmentForm',
                    // onDone: 'idle',
                    onDone: {
                      target: '#registrationForm.editing.success',
                    },
                    onError: {
                      target: '#registrationForm.error',
                      actions: ['setAsyncService', 'setErrorMessage'],
                    },
                  },

The success target in the submitForm invoke is transitioned WAY before the actual service returns a value

tough igloo
#

This should not happen

#

But also you have send(...) in the service which doesn't make sense. That should not be there - it is an action creator

#

Can you share a more full code example?

fresh rain
#

Hi David. That's in the Progress handler for the axios call. Basically I want to send progress events as they come in to update a progress bar in the UI

#

Are you saying that the progress handler breaks the invoke in some way ?

#

I was chatting with @plain hollow earlier and he had suggesting converting this call to a callback service. I'm not sure why I would have to do that though, maybe somebody could explain why and maybe even provide an example of an axios call implemented as a callback handle because the documentation is pretty lacking in this area i.e. very simple example that basically does nothing

#

I just removed the progress callback handle and the result is the same.
The invoke calling the axios service transitions via the onDone WAY before the axios call actually returns a value

#

Here is the updated service that exhibits this functionality.


const postEnrolmentForm = (enrolment: TEnrolment) => {
  return axios.post(`${baseUrl}/enrol`, enrolment, {
    // onUploadProgress: (progress) => progressHandler(progress),
  }).then(res => {
    console.log('Got Back from /enrol : ', res.data);
    return res.data
  })
};

I have a deliberate delay applied in the backend so it will not return any value for at least 5 seconds but the transition to #registrationForm.editing.success happens almost immediately and 5 seconds later I get the console.log message and it's already in the success state

rain cloak
# fresh rain Here is the updated service that exhibits this functionality. ``` const postEnr...

It's been a while since I used axios, so not sure this syntax is correct, but have you tried with a promise-based service? Something like this:

const postEnrolmentForm = async (enrolment: TEnrolment) => {
  const res = await axios
    .post(`${baseUrl}/enrol`, enrolment, {
      // onUploadProgress: (progress) => progressHandler(progress),
    });
  console.log('Got Back from /enrol : ', res.data);
  return res.data;
};
fresh rain
#

That does exactly the same thing.
It immediately transitions to the success state without awaiting the result of the call.
In this case I delay the result by 5 seconds for testing AND I return an error.
The onError transition never gets called but I get an exception in the console as the UI is already in the success state

      {state.matches('editing.success') && (
        <div className='w-full sm:w-10/12 m-auto'>
          <div className='font-light text-sm bg-white rounded-md sm:rounded-3xl drop-shadow pb-8'>
            <PageLayout
              header={{
                text: 'Your form has been submitted.',
                align: 'center',
              }}>
              <div className='flex flex-col items-center mb-20'>
                <div className='text-center mb-20 font-extralight'>
                  {location!.footer ? parser(location!.footer) : 'NO_FOOTER'}
                </div>
                <Button
                  sx={{ width: '20rem' }}
                  size='large'
                  variant='contained'
                  onClick={() => send({ type: 'REGISTER_ANOTHER_PATIENT' })}>
                  Register another patient
                </Button>
              </div>
            </PageLayout>
          </div>
        </div>
      )}

I have no idea why the invoke is doing this as I was under the impression that it would wait until the promise was resolved to take either the onDone or onError tansition

rain cloak
#

It should await the promise, so something is up. Can you recreate an example of this problem in a code sandbox. Then I'd happily take a look at that

fresh rain
#

I'll try yes

fresh rain
#

The axios endpoint has a fixed delay of 5 seconds and will respond by either echoing back the body OR sending a statusCode of 500 so a 50/50 good fail rate

fresh rain
#

I just cannot get to the bottom of this. It's taken me all day so far and nobody has any idea what's going on here ???

rain cloak
#

Thank you for the sand box. I'll have a look when I get back to the keyboard 😊

fresh rain
#

👍

#

You can ignore that codesandbox. I obviously don't know how to use that either. thats linked to the original one I thought I had cloned

rain cloak
#

yeah, the sandbox doesn't seem to contain the relevant code. Do you want to jump on a quick call to have a look at this?

fresh rain
#

Don't know why there is an error on the service call though

rain cloak
#

You can also upload a zip here or send me a direct message with a zip of your repo and then I can look at that

fresh rain
#

It's the invoke service in the sandbox

rain cloak
#

This is the machine I get from the link above

fresh rain
#

This is the the code I put in it

import React from "react";
import { useMachine } from "use-machine";
import axios from 'axios';

const machineConfig = {
  context: {
    data: {
      name: 'steve',
      age: 21
    }
  },
  id: "dataMachine",
  initial: "submitForm",
  states: {
      idle: {},
      submitForm: {
        invoke: {
          id: 'postSubmitForm',
          src: ({data}) => axios.post('https://nr.wellrevolution.com/api/identicus/enrol', data),
          onDone: {
            target: 'success',
          },
          onError: {
            target: 'error',
          },
        },
      },
      success: {final: true},
      error: {final: true}
    }
};

const App = () => {
  const [state] = useMachine(machineConfig);

  return (
    <div className="App">
      <div>
        This should be dispalyed all the time
        The axios post service has a 5 second delay with a 50/50 success or error after the delay
      </div>
      {(state.matches('success') &&
        <div>This will display only once success is transitioned too</div>
      )}
      {(state.matches('error') &&
        <div>This will display only once error is transitioned too</div>
      )}
    </div>
  );
};

export default App;

rain cloak
#

It is almost identical to yours; I just had to create a small test service to simulate the success or error after 5 seconds because I couldn't call the link in the snippet above

#

And I updated the code to the useMachine from @xstate/react. I guess you have created a helper hook that you use which wraps a config with createMachine etc...

fresh rain
#

I fixed the codesandbox, I did previously have the same imports / dependencies I found the issue but I don't know WHY its an issue

 states: {
    idle: {},
    submitForm: {
      invoke: {
        id: "postSubmitForm",
        // src: 'postData',
        src: ({data}) => postData(data),
        onDone: {
          target: "success"
        },
        onError: {
          target: "error"
        }
      }
    },
    success: { final: true },
    error: { final: true }
#

This works

#

Bit the commented out one does not and that is defined in the service: {} config as follows

#
{
  services: {
    postData: async ({ data })  => {
      postData(data);
    },  }
}
#

It's the async in front of the call that screws it up

rain cloak
#

Maybe you're missing a return here 🤔 But in both these examples, you don't need to add the async. You only need to add that if you use await inside the method.

{
  services: {
    postData: async ({ data })  => {
      return postData(data); // added return here
    },  }
}

// alternative version:
{
  services: {
    postData: ({ data })  => postData(data)
  }
}
fresh rain
#

Spoke too soon. I had not changed the commented out src. Without the async it NEVER returns

rain cloak
fresh rain
#

Well I thank you for your patients and I now have it running properly, as designed.
The issue was an entirely stupid one that I really should have spotted immediately..

 postEnrolmentForm: async ({ enrolment })  => {
          postEnrolmentForm(enrolment as TEnrolment);
        },

I had forgotten to put the return in this statement and it should have been simplified to just

postEnrolmentForm: async ({ enrolment })  => postEnrolmentForm(enrolment as TEnrolment);

Now I can go and beat myself up with something spikey

rain cloak
#

Happy to hear you're past this problem now. Don't beat yourself up; we all make these "mistakes" every day 😊 Btw. we're launching our new docs soon, and we have a section on how to invoke promise based services: https://stately.ai/docs/xstate/actors/promises. I hope it makes sense, if not - we'd love some feedback to make it better.

fresh rain
#

I'll take a look and let you know

#

Looks good... And you have the return in your example... Spent all day on this little bugger..
So now HOW do I send an event or update the context from a service ???

rain cloak
#

You might be able to use the sendBack method for your needs?

fresh rain
#

@rain cloak Quick question. Do callback services, executed by invoke, still have onDone and onError handlers ? It's not clear from the documentation you pointed me at

tough igloo
fresh rain
#

Hmmm. Then this would not resolve my issue

#

So @tough igloo do you or any of your team have any idea how I could solve the following.
I have a promise based service, an axios.post request, that has a definite onDone and onError state and as such I use the promise based invoke with the onDone & onError. This works just fine.
However.... the axios.post has the ability to add an event listener, that fires DURING the upload, to report the progress.
I need to tap into this progress event handler to update the percentage complete of the upload for the UI.
I would like to have this progress upload percentage updated in the machine context for the UI to display in a progress bar..
I have gone through every way I could think of using xstate for this but have failed with every attempt.
For instance, you previously pointed out I was using send({...}) in this handler and as you correctly said it absolutely did not work.

tough igloo
#

Can you provide some code to start with?

fresh rain
#

Here is the service and the progress handler


const progressHandler = ({loaded, total}: AxiosProgressEvent) => {
  const percent = Math.floor((loaded * 100) / total!)
  // TODO : I want to update the context.progress value here, either via sending an event to the machine OR set it directly into the context
  // updateProgress(percent) 
  console.log('Enrolment Form Upload Progress %:', percent);
};

const postEnrolmentForm = (enrolment: TEnrolment) => {
  return axios.post(`${baseUrl}/enrol`, enrolment, {
    onUploadProgress: (progress) => progressHandler(progress),
  }).then(res => res.data)
};

#

The promise postEnrolmentForm service is called via an invoke.

#

In the UI there is a <ProgressBar progress{state.context.progress}/> Component which is only visible IF the progress value is > 0 or < 100 i.e. it's displayed conditionally

tough igloo
#

So you're basically looking to make a hybrid promise + callback invocation

fresh rain
#

Correct

tough igloo
#

IIRC that's not possible in v4, so the workaround would be to just use a callback and have a custom "done event"

#

But I'll double check.

fresh rain
#

Thank you. An example of some sort would be VERY MUCH appreciated... This is doing my head in and taking SO MUCH time for what I originally thought should be a breeze

tough igloo
#

To make your example work, you'd add this:

.then(res => {
  sendBack({ type: 'somethingIsDone', data: res.data })
});

And then listen for that somethingIsDone (or whatever you name it) event

#

Oh hey, good news!

#

Scratch that, you don't need to do that. Here's the pattern:

fresh rain
#

Is it OK to have the promise service invoked as a callback ???

tough igloo
#
invoke: {
  src: (ctx, e) => async (sendBack) => {
    return axios.post('...', data, {
      onUploadProgress: (progress) => { sendBack({ type: 'progress', progress }) }
    })
  },
  onDone: {
    actions: (_, e) => { e.data } // resolved promise data
  }
},
on: {
  progress: { ... }
}
#

Something like that

#

This is the magic: (ctx, e) => async (sendBack) => ...

fresh rain
#

Can I keep my onError {...} as well with this pattern ???

#

I'll give this a try as it looks much more like meeting my needs 🙂

fresh rain
#

@tough igloo Thanks for your help here. I can report it's all working now. I get the correct progress and the progress bar shows in the UI

fresh rain
#

Hi @tough igloo I have just one issue to resolve because of this change.
My useMachine(...) is now screaming at me ... This usually means the machine config has an issue.

How do I now type the src invoked service ....

states: {
                idle: {},
                submitForm: {
                  invoke: {
                    id: 'postSubmitForm',
                    // src: 'postEnrolmentForm',
                    src: (ctx, e) => async (sendBack) => postEnrolmentForm(ctx.enrolment as TEnrolment, sendBack as Sender<TRegistrationPageEvents>),
                    onDone: {
                      target: '#registrationForm.editing.success',
                    },
                    onError: {
                      target: '#registrationForm.error',
                      actions: ['setAsyncService', 'setErrorMessage'],
                    },
                  },
                  on: {
                    UPDATE_PROGRESS: {
                      actions: ['updateProgress'],
                    }
                  }
                },
              },

As you can see it was originally defined as src: 'postEnrolmentForm' and was set in the services: {} schema section but is now in the callback syntax and although this runs just fine, I now get typescript errors where the useMachine is called.

#

Just to be clear the original promise service was defined in the machines services:{} schema as postEnrolmentForm: ({enrolment}) => postEnrolmentForm(enrolment as TEnrolment),

tough igloo
#

Can you share the full code?

fresh rain
#

Sorry for not getting back sooner @tough igloo but I fixed it. Another one of my little fo pars