Skip to content

[@xstate/vue] Run actor during setup, not mounted. #5311

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions packages/xstate-vue/src/useActorRef.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { onBeforeUnmount, onMounted } from 'vue';
import { onBeforeUnmount } from 'vue';
import {
Actor,
ActorOptions,
Expand Down Expand Up @@ -35,12 +35,12 @@ export function useActorRef<TLogic extends AnyActorLogic>(
const actorRef = createActor(actorLogic, options);

let sub: Subscription;
onMounted(() => {
if (observerOrListener) {
sub = actorRef.subscribe(toObserver(observerOrListener));
}
actorRef.start();
});

if (observerOrListener) {
sub = actorRef.subscribe(toObserver(observerOrListener));
}

actorRef.start();

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this change compatible with SSR and KeepAlive?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ping @negezor

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it works in SSR and KeepAlive. I already answered this below. My production currently has a fork of @xstate/vue running for chat that uses KeepAlive to switch dialogs, page is also can be rendered on the server.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't work in SSR correctly as far as I can tell. Running side-effects directly in setup will unavoidably lead to leaks and unwanted behavior. onBeforeMount (and other lifecycle hooks) are inherently not called on the server - so the actorRef instance can't be stopped correctly.

To quote Vue docs:

Since there are no dynamic updates, lifecycle hooks such as onMounted or onUpdated will NOT be called during SSR and will only be executed on the client.

You should avoid code that produces side effects that need cleanup in setup() or the root scope of <script setup>. An example of such side effects is setting up timers with setInterval. In client-side only code we may setup a timer and then tear it down in onBeforeUnmount or onUnmounted. However, because the unmount hooks will never be called during SSR, the timers will stay around forever. To avoid this, move your side-effect code into onMounted instead.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works, but it doesn't clear the timer. I still want it to work in SSR. Alternatively, we can just check that we are in SSR typeof window === undefined and just not run .start(). The user can do this themselves. In the Nuxt conditional, we can do:

const actor = actorRef(machine);

if (import.meta.env.SSR) {
    const nuxtApp = useNuxtApp();
    nuxtApp.hook('app:rendered', () => {
        actor.value.stop();
    });

    actor.value.start();
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works, but it doesn't clear the timer.

Yeah, right - I meant "it doesn't work correctly". If the timer gets started, it absolutely has to be stopped for us to consider this an appropriate solution. It's either that or not starting the actor at all within the setup function.

You also can't have a divergent behavior between the server and a client because you'd risk hydration mismatches to happen. The whole point of SSR is to deliver the same initial state of the HTML to the browser as the one that would be created client-side.

onBeforeUnmount(() => {
actorRef.stop();
Expand Down
Loading