-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
XState version
XState version 5
Description
I need to use the same instance of a machine across different components, because the machine serves as the source of truth for the state of a core business logic in my app.
The most idiomatic way of doing this in Vue (to my knowledge) would be to create a custom hook wrapping the base useMachine
invocation with createSharedComposable
, and then to use this custom hook everywhere I need to have this shared instance.
This is sadly not working currently in a couple of scenarios, because the machine is stopped prematurely and won't restart afterwards. It's really a huge pain point for me, since it means that currently I can't use @xstate/vue
as soon as my needs start to grow too big for its scope.
Expected result
I have created a reproduction to highlight the issue, with the expected behavior showing on the right panel (named FixedPageViews
) :
- Open the repro, landing on the "Home page".

Since our machine only tracks page views for the child routes of the PagesToDiscover
page, currently FixedPageViews
should only display its name, with no counter of page views below.
- Click on one of the link at the bottom of the page that is not "home".

FixedPageViews
should now be displaying that the page you just visited has been viewed once.
- Click on other links that are not "home" any number of times.

FixedPageViews
should be displaying the correct count of views for each page you clicked.
- Click on the "home" link to go back to the home page

FixedPageViews
should correctly have reset to its initial state, since we unmounted the PagesToDiscover
component before going back to Home
.
Since it is back to its initial state, you should be able to restart this flow from the start and observe the same results.
Actual result
The current behavior is showed in the repro on the left panel (named PageViews
) :
- Open the repro, landing on the "Home page".

Since our machine only tracks page views for the child routes of the PagesToDiscover
page, currently PageViews
should only display its name, with no counter of page views below. This is the correct behavior.
- Click on one of the link at the bottom of the page that is not "home".

PageViews
should now be displaying that the page you just visited has been viewed once. This is the correct behavior.
- Click on other links that are not "home" any number of times.

PageViews
does not update at all from this point, since the machine has already been stopped. This is not what is expected. If you open the console, you can see the warning and errors sent by XState about it :
- Click on the "home" link to go back to the home page

PageViews
should correctly have reset to its initial state, since we unmounted the PagesToDiscover
component before going back to Home
. This is the correct behavior.
Since it is back to its initial state, you should be able to restart this flow from the start and observe the same results.
Reproduction
https://stackblitz.com/edit/github-mwzbpb?file=src%2FpageViewsMachine.ts
Additional context
I have investigated to understand where the issue is coming from, and the culprit is the current implementation of useActorRef
in @xstate/vue
:
xstate/packages/xstate-vue/src/useActorRef.ts
Lines 23 to 33 in ca7f090
onMounted(() => { | |
if (observerOrListener) { | |
sub = actorRef.subscribe(toObserver(observerOrListener)); | |
} | |
actorRef.start(); | |
}); | |
onBeforeUnmount(() => { | |
actorRef.stop(); | |
sub?.unsubscribe(); | |
}); |
The use of onBeforeUnmount
here create a tight coupling between the machine instance and the lifecycle of the component that use it. In the reproduction, when we switch from a child route to another one, the first child is unmounted before we can go to the next page, which will stop this machine instance indefinitely.
The more straightforward solution to this problem to me is the one I used in the reproduction for the fixedPageViews
: replacing the onMounted
/onBeforeUnmount
by effectScope
/onScopeDispose
, which is intended to replace the explicit component lifecycle hooks when used in a composable.
More details on these APIs are available in the related implementation PR and RFC:
The main problem with this solution is that it would technically introduce a breaking change, since these APIs are only available for Vue ^3.2.0 and currently XState is supporting the ^3.0.0 range.
I guess that could be an opportunity to upgrade the installed Vue version in the repo, since I believe I have seen in the source code that the global JSX declaration from Vue was causing some issues, and it has been removed in 3.4.
If this is not a big enough concern to prevent fixing this bug, I would be interested in opening a PR to propose my solution if that's okay.