Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question] How to update context while running invoke #203

Open
Hurtak opened this issue Sep 5, 2023 · 1 comment
Open

[Question] How to update context while running invoke #203

Hurtak opened this issue Sep 5, 2023 · 1 comment

Comments

@Hurtak
Copy link

Hurtak commented Sep 5, 2023

Updating the context with regular state(...) is easy, you just do it inside of transition with reduce, eg.:

ready: state(
  transition('togglePanel', 'ready', reduce(togglePanel)) // update the context and stay on the same `ready` state
),

Now here is my problem (or perhaps some misunderstanding) - how do I do the same thing for invoke? What if I need some other transitions to update the context while the invoke is running?

For example I have machine which loads data to display in panel, and in the machine there is state for the data and for the panel opened/closed state - and I want to be able to toggle the panel while the data is loading. Here is how I implemented it so far:

type Context = {
  loadAsyncData: () => Promise<string[]>;
  asyncData: string[];
  panelOpened: boolean;
};

const togglePanel = (ctx: Context): Context1 => ({
  ...ctx,
  panelOpened: !ctx.panelOpened,
});

const setAsyncData = (ctx: Context, { data }: { data: string[] }): Context => ({ ...ctx, asyncData: data });

const state = {
  initial: state(
    immediate('loading')
  ),
  loading: invoke(
    ({ loadAsyncData }: Context) => loadAsyncData(),
    transition('done', 'ready', reduce(setAsyncData)),
    transition('error', 'error'),

    // Here is the problem, I am using the same approach as with the first example but
    // it does not work, because invoke is run again and it fetches the data again. 
    // Is there a way to run the invoke only once (and the done/error only once) at the beginning but not when running the `togglePanel`?
    transition('togglePanel', 'loading', reduce(togglePanel)), 
  ),
  ready: state(
    transition('togglePanel', 'ready', reduce(togglePanel))
  ),
  error: state(
    transition('togglePanel', 'error', reduce(togglePanel))
  ),
};
@Hurtak
Copy link
Author

Hurtak commented Sep 6, 2023

For anyone that would encounter the same problem, we managed to workaround this with introduction our own version of invoke

import { invoke, Machine, MachineState, Service, Transition } from 'robot3';

const invokeFnType: {
  enter: (machine2: Machine, service: Service<Machine>, event?: unknown) => Machine;
  // fn is inherited so it should always be defined
  fn?: (ctx: Machine['context'], event?: unknown) => Promise<unknown>;
} = {
  enter(machine2: Machine, service: Service<Machine>, event?: unknown) {
    // Only invoke the function if the machine state changed
    if (machine2.current !== service.machine.current) {
      this.fn
        ?.call(service, service.context, event)
        .then((data: unknown) => service.send({ type: 'done', data }))
        .catch((error: unknown) => service.send({ type: 'error', error }));
    }
    return machine2;
  },
};

// Only invoke the async function (and the done/error transition) when transitioning into the state from other state,
// Do not invoke when transitioning from the same state (this is the only difference between invoke and this function)
export const invokeExceptForSelfTransitions = <C, T>(
  fn: (ctx: C, event?: unknown) => Promise<T>,
  ...args: Transition[]
): MachineState => ({
  ...invoke(fn, ...args),
  ...invokeFnType,
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant