#How to clean up actor before `MyMachineContext.Provider` is unmounted

1 messages · Page 1 of 1 (latest)

jolly shard
#

Hi there, I'm currently using createActorContext but I've run into an issue where when my provider gets unmounted, the actor is stopped so any child components can no longer use it to do any sort of clean up via something like:

useEffect(() => {
  return () => {
    actorRef.send({ type: 'CLEANUP' });
  }
}, [])

If the above effect is executed in a child of the provider, the machine is already stopped. If I try to hoist that effect to where the provider is rendered, there is no available reference to the actorRef.

Any suggestion on how to get around this?

Ideally this can be defined on the machine itself. Maybe something like a onBeforeStop similar to onDone.

jolly shard
#

@hoary iron any thoughts around this?

hoary iron
#

You're right that this should be in the actor logic itself, hmm...

jolly shard
#

Awesome, yeah I tried using the xstate.stop event in my machine but it was never called. So I'll wait for this fix to be merged and released

#

Just to confirm, when listening to that event, the actor would not be stopped yet correct?

hoary iron
#

Correct; this is the internal code:

    // 1. "xstate.stop" event is processed
    this.update(nextState, event);
    if (event.type === XSTATE_STOP) {
      // 2. stop procedure is called (cancels delayed events, clears mailbox, unregisters from system, etc...)
      this._stopProcedure();
      // 3. completion handler called (this may be a mistake; actor is stopped, not completed)
      this._complete();
    }
jolly shard
#

🙌

rustic tapir
#

u probably should embed ur cleanup logic in the machine itself (or in some of its child actors)

#

coupling this with React's lifecycle is suboptimal

jolly shard
#

yeah, 100% thats the goal. I just wasn't able to due to the event not firing

rustic tapir
#

what kind of cleanup are we talking about here?

#

if u have lifecycle-related cleanup then ideally it should go into the definition of the actor. I guess that we might not have this concept for machines today because there is no API that would get called for start/stop there

#

if we want to add it - it feels to me like it should be a global "hook" (of sorts), not an event-driven transition

#

but machines dont always own-own resources like this (since their .context should be as pure/serializable as possible)

#

in the past i was just encapsulating this kind of stuff in some non-machine logic and invoke/spawnChild that

#

there u can track "presence" of something and thus u can perform cleanup

hoary iron
#

It's still an "event" conceptually

#

Although Akka distinguishes between .receive-ing events and .receiveSignal

jolly shard
#

A stop action would be great. I couldn't really find any documentation around the xstate.stop event either so that would help with surfacing that capability.

I have a main machine as well as child machines that currently don't have a way of doing any clean up unlike their fromCallback counterpart actors. Some control certain class instances, others control some sort of internal state or handle logging. I need to execute some cleanup actions from these actors if for whatever reason they are stopped which currently isn't possible.

For example:

  1. Call a cleanup method on the class instance
  2. Log something that the actor has been stopped without entering its final state
rustic tapir
#

the main problem here - i feel - is that we dont differentiate between a hard stop and a "pause"

#

this thread started with @xstate/react in mind and there... we just always should use pause semantics

#

so in this sense - it's just kinda wrong to even think in terms of stopping etc

#

and thus stop/poststop/whatever are a little bit out of place

#

currently our .stop() behaves kinda like a .pause() (but not in full!)

#

and u can kinda always persist a snapshot before calling stop() and later on rehydrate with it

#

if u plan to call anything on this .stop() call (like some post hooks, stop transitions, whatever) then u also need to have a way to "start" things on the conceptual "resume" that happens here:

const rehydrated = createActor(m, { snapshot: persistedSnapshot }).start()
#

almost any kind of cleanup is symmetric in a sense that it needs a setup phase first

rustic tapir
jolly shard
#

At the moment, there is a gap of not being able to execute any actions on a machine actor if for whatever reason it is stopped. I don't think this is really a react issue but it surfaced there first. For me, it was when the user clicks the back button in their browser. Regardless, I want to be able to execute an action no matter what when an actor stops to ensure I do the necessary cleanup.

In my case, the user gets a session and the machine is instantiated with it. There is no restarting. So no matter what, if the machine is stopped for any reason, I need to be able to execute some actions to log, call external methods, w/e.

#

Another possible way to do this, although not as intuitive is via higher-level actor logic:

function withStopHandler<T extends AnyActorLogic>(actorLogic: T) {
    const enhancedLogic = {
      ...actorLogic,
      stop: (...args) => {
        // do something before stopping here
        return actorLogic.stop(...args);
      },
    } satisfies T;
  
    return enhancedLogic;
}
rustic tapir
#

There is no restarting.

there is always restarting when it comes to React, it's baked into its model - while u might not utilize some of that right now, we need to account for it cause other users might be relying on it

#

the easiest scenario to talk about is Fast Refresh aka HMR - as that's what people are usually familiar with

#

how your component/machine will behave if it gets stopped (well, kinda paused) for a brief moment?

jolly shard
#

What do you recommend then?

hoary iron
#

You can actually do this:

createMachine({
  invoke: {
    src: fromCallback(() => {
      return () => {
        console.log('do cleanup here')
      }
    })
  },
  // ...
})
#

The cleanup will run when the actor is stopped

jolly shard
#

Thanks @hoary iron I'll give that a go. Doesn't feel very clean but better than what I have now.

jolly shard
#

@hoary iron just tried this but it didn't work:

invoke: [
            {
                src: fromCallback(({ self }) => {
                    const cleanup = () => {
                        self.send({ type: 'CLEAN_UP' });
                    };

                    // Try to cleanup before the window closes
                    window.addEventListener('beforeunload', cleanup);

                    return () => {
                        cleanup();
                        window.removeEventListener('beforeunload', cleanup);
                    };
                }),
            },
        ],
#

The CLEAN_UP event never gets emitted

#

The machine is already stopped when cleanup is called.

hoary iron
#

@rustic tapir Maybe you have an idea here? (if it's related to what was previously discussed on this topic)

rustic tapir
#

@jolly shard could you share the full runnable repro case of the problem above?

#

i can't run an incomplete snippet in my head 😉