Xstate: How to assign and transition conditionally?

Created on 6 Dec 2019  路  6Comments  路  Source: davidkpiano/xstate

Bug or feature request?

Question/Feature Request

Description:

I am in the process of building a multi step form. When a field is filled, I am firing an event. In state config, I assign the field value to context when event is fired. I need to now validate the context and if there are no errors, need to transition to next state. If there are errors, the error message should be put in context. How do I do this? Additionally, the validation might be asynchronous.

{
  id: 'onboarding',
  context: {
    mobileNumber: null,
    mobileNumberError: null
  },
  initial: 'mobileNumber',
  states: {
    mobileNumber: {
      id: 'mobileNumber',
      initial: 'invalid',
      states: {
        invalid: {
          on: {
            SET_MOBILE_NUMBER: {
              actions: (context, event) => assign({mobileNumber: event.value}),
              // Validate and assign error message. transition to valid if there is no error
            },
          },
        },
        valid: {
          on: {
            SET_MOBILE_NUMBER: {
              actions: (context, event) => assign({mobileNumber: event.value}),
              // Validate and assign error message, transition to invalid if there is error
            },
            FETCH_MOBILE_NUMBER_OTP: '#mobileNumberOtp',
          },
        },
      },
    },
    mobileNumberOtp: {
      id: 'mobileNumberOtp',
      initial: 'creating',
      states: {
        creating: {
          invoke: {
            id: 'fetchMobileNumberOtp',
            src: fetchMobileNumberOtp,
            onDone: 'invalid',
            onError: '#mobileNumber.valid',
          },
        },
    },
}

(Bug) Expected result:

NA

(Bug) Actual result:

NA

(Bug) Potential fix:

NA

(Feature) Potential implementation:

I am sure there is a way to do this already. You may want to put up a recipe of this in documentation.

Link to reproduction or proof-of-concept:

NA

question

Most helpful comment

Guarded Transitions

...
validating: {
  invoke: {
    src: 'validateMobileNumber',
    onDone: [
      {
        target: 'valid',
        actions: assign({mobileNumberError: ''}),
        cond: (_ctx, event) => {
          const validationResponse = event.data;
          if (validationResponse.result) { // I don't know what's in your response... Just an example
            return true;
          } else {
            return false;
          }
        }
      },
      // If the condition is false, validation failed, so go to invalid
      {
        target: 'invalid',
        actions: assign((_ctx, event) => ({ mobileNumberError: event.data.validationMessage }))
      }
    ],
    onError: { ... }

All 6 comments

Here is what I tried later:

Invoking a promise to validate

{
  states: {
    mobileNumber: {
      id: 'mobileNumber',
      initial: 'invalid',
      on: {
        SET_MOBILE_NUMBER: {
          target: '.validating',
          actions: (context, event) => assign({mobileNumber: event.value}),
        },
      },
      states: {
        validating: {
          invoke: {
            src: validateMobileNumber,
            onDone: {
              target: 'valid',
              actions: assign({mobileNumberError: ''}),
            },
            onError: {
              target: 'invalid',
              actions: assign({
                mobileNumberError: (context, event) => event.data,
              }),
            },
          },
        },
        invalid: {},
        valid: {},
      },
    },
}

Invoking service (with Promise) sounds like a good idea. I'm still learning about fsm but in the end I came up with similar solution https://codesandbox.io/s/serverless-http-1putt

Maybe we can get some pro hints from @davidkpiano ;)

Guarded Transitions

...
validating: {
  invoke: {
    src: 'validateMobileNumber',
    onDone: [
      {
        target: 'valid',
        actions: assign({mobileNumberError: ''}),
        cond: (_ctx, event) => {
          const validationResponse = event.data;
          if (validationResponse.result) { // I don't know what's in your response... Just an example
            return true;
          } else {
            return false;
          }
        }
      },
      // If the condition is false, validation failed, so go to invalid
      {
        target: 'invalid',
        actions: assign((_ctx, event) => ({ mobileNumberError: event.data.validationMessage }))
      }
    ],
    onError: { ... }

Yep, guarded transitions (linked above by @semopz) are what you want.

Interesting, I am also facing a similar issue but it involves multiple checks.

For instance. how would you validate a text input with multiple guards? min length, max length, invalid characters, maybe a regex as well. I am rewriting a multiform wizard that currently uses Formik and Yup, but ideally would like all logic the machine so it can be tested independently of Formik.

@jaetask You can probably come up with a solution in userland for this, no?

Was this page helpful?
0 / 5 - 0 ratings