4

I have a multi step form that basically has these basic steps: select services -> contact -> billing. I display a progress bar and emit events when the user changes the step they're on, and this is my current basic pattern with xstate:

const formMachine = new Machine({
  id: 'form-machine',
  initial: 'selectService',
  context: {
    progress: 0,
    pathname: '/select-service',
  },
  states: {
    selectService: {
      entry: assign({
        progress: 0,
        pathname: '/select-service',
      }),
      on: {
        NEXT: 'contact',
      }
    },
    contact: {
      entry: assign({
        progress: 1 / 3,
        pathname: '/billing'
      }),
      on: {
        PREVIOUS: 'selectService',
        NEXT: 'billing',
      }
    },
    // ... there's more actions but that's the gist of it
  }
});

Here's the visualization: enter image description here

In my react component, I watch this service for changes in pathname so I can push to the history

function SignupFormWizard() {
  const history = useHistory();
  const [state, send, service] = useMachine(formMachine);

  useEffect(() => {
    const subscription = service.subscribe((state) => {
      history.push(state.context.pathname);
    });
    return subscription.unsubscribe;
  }, [service, history]);

  // ...
}

However, here's the problem: whenever I revisit a route (say, I directly navigate to /billing), it will immediately bring me back to /select-service. This makes sense with my code because of the initial state, and the subscription, that it will do that.

How would I go about initializing the state machine at a specific node?

corvid
  • 10,733
  • 11
  • 61
  • 130

2 Answers2

10

useMachine hook accepts second parameter which is a configuration object. In this object you can set state using state key, but you will have to construct state yourself, and it would look something like this:

let createdState = State.create(stateDefinition);
let resolvedState = formMachine.resolve(state);
let [state, send, service] = useMachine(formMachine, {state: resolvedState});

In my opinion it works well when you need to restore persisting state, but creating stateDefinition manually from scratch is just too much hustle.

What you can do, is create initial state and choose where you want to actually start:

initial: {
  always: [
    {
      target: "selectService",
      cond: "isSelectServicePage",
    },
    {
      target: "contact",
      cons: "isContactPage",
    },
    {
      target: "billing",
      cond: "isBillingPage",
    },
  ],
}

Then, when you are starting your machine, all you have to do is set initial context value:

let location = useLocation();
let [state, send, service] = useMachine(formMachine, {
  context: { pathname: location.pathname },
});
Mr. Hedgehog
  • 2,644
  • 1
  • 14
  • 18
1

The other answer is almost correct, to be more precise, you can move to first state which will decide the next step dynamically, based on context assigned.

  createMachine(
    {
      id: "Inspection Machine",
      initial: 
        "decideIfNewOrExisting",
      
      states: {
        "decideIfNewOrExisting": {
          always: [
            {
              target: "initialiseJob",
              cond: "isNewJob"
            },
            {
              target: "loadExistingJob"
            }
          ]
        },
...
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Anand
  • 4,523
  • 10
  • 47
  • 72