Skip to content

onChange callback called twice for same state when using immediate transitions#243

Description

@QuentinFchx

Hello,

First of all thank you for your library.

The context

We're trying to implement a multi-step form and we're using a FSM (robot) as the backbone of the form.
One of the features of that form is that data filled in a given step may skip the next one.

For instance :

  • User fills the first step of the form
  • The user clicks "Next", we use the data to fetch some objects
  • if there are multiple objects, we display step2 to select one of these objets
  • otherwise, if there are 1 or 0 objects, no need for step2 we go to step3 directly
    We implemented this with a mix of invokes and immediate transitions (which we'll call intermediary steps), that always end up onto a concrete step (a "final ui state").

Also since we're evolving in a web environment, we're synchronizing the current step of the form with the url.
To achieve this, we're listening to the onChange callback and whenever a new state is reached, if it is a concrete step (not an intermediary one used to fetch information) we change the url to that step's one (ie: /step1, /step2).

The issue

However, it seems that the onChange callback is called multiple times for the same concrete step and we're struggling to understand why.

import { createMachine, guard, immediate, invoke, reduce, state, transition, interpret, d } from 'https://unpkg.com/robot3';

async function loadOptions() {
    return false ? [] : [{ id: 1 }, { id: 2 }];
}

const machine = createMachine(
    {
        step1: state(
            transition(
                'next',
                'step1loading',
                guard(ctx => ctx.form.valid)
            )
        ),
        // Loading available options
        step1loading: invoke(
            loadOptions,
            transition(
                'done',
                'step1decider',
                reduce((ctx, evt) => ({ ...ctx, options: evt.data }))
            ),
            transition('error', 'error')
        ),
        // Deciding which step to go to next
        step1decider: state(
            immediate(
                'step2',
                guard(ctx => ctx.options.length > 1)
            ),
            immediate('step3')
        ),
        step2: state(),
        step3: state(),
        error: state()
    },
    context => context
);

const service = interpret(
    machine,
    serv => {
        console.log(serv.machine.current);
    },
    {
        form: {
            valid: true
        },
        options: []
    }
);

service.send('next');

The following is logged

"step1loading" <-- OK
"step2" <-- Expected (even if step1decider is not there)
"step2" <-- Unexpected

Is it normal behaviour ? If so, by any chance, would you have guidance on how we should handle this ?
Is it an issue and the first onChange call should be done with the step1decider state instead of step2 ? Or should one of the occurence not happen at all ?

The "investigation"

Some work has been done around onChange and immediate transitions here and within the transitionTo call here but we're not familiar enough with the library to point a problem there.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions