-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
[@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
base: main
Are you sure you want to change the base?
Conversation
|
sub = actorRef.subscribe(toObserver(observerOrListener)); | ||
} | ||
|
||
actorRef.start(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ping @negezor
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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();
}
There was a problem hiding this comment.
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.
This would be a canonical use case for |
useSelector already gets a reactive value from actorRef
@Andarist Sorry for commit spam, it was wrong to use github interface first. I just checked in my SSR and I am using KeepAlive component for chat :)
The thing is that the JUMP_TO_CURSOR shown in the example is used not only for the initial load, but also if we, for example, go to some message via search. It would be strange not to use the already written flow. |
@Andarist I found another problem that existed before. Besides the fact that XState basically always remained in initial state in SSR, |
sub = actorRef.subscribe(toObserver(observerOrListener)); | ||
} | ||
|
||
if (typeof window !== 'undefined' && typeof document !== 'undefined') { | ||
actorRef.start(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now the code still won't quite work exactly the same bewteen client and the server. In the thread you have described that the current inner workings suffer from:
It makes the actor unusable during SSR;
And now... they will still work the same way on the server. So it seems one of the things you have wanted to address with this PR just can't be addressed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least it will definitely work now if you call actor.start()
in SSR. Before this, watch
had no sync. This problem cannot be solved until Vue officially adds some kind of hook for the component to finish working in SSR. Basically, all SSRs are somehow related to some specific life cycles of implementations. Nuxt has its own, my SSR has a slight mimicry of Nuxt.
I can say that I achieved the most important thing with this PR, it is the ability to run XState during setup, either on the client or on the server.
I started migrating the chat logic in a Vue application using XState. After some time, I encountered an issue where the actor does not work during the execution of the
setup
hook. This makes it impossible to set any initial state based on external data. Here's a simplified example for context:Expected behavior:
Actual behavior:
After looking into the hook implementation, I realized the problem is that the actor is only started inside the
onMounted()
hook. This creates an unnecessary limitation because:setup()
function;I don't see any strong reason for delaying
.start()
untilonMounted()
. I also found an issue mentioning the same limitation: #3786 (comment). Actor start was moved to onMounted in this commit bfc9f74