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.
FixedPageViewsshould be displaying the correct count of views for each page you clicked.
- Click on the "home" link to go back to the home page
FixedPageViewsshould 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
PageViewsshould 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:
|
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.
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
useMachineinvocation withcreateSharedComposable, 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/vueas 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) :Since our machine only tracks page views for the child routes of the
PagesToDiscoverpage, currentlyFixedPageViewsshould only display its name, with no counter of page views below.FixedPageViewsshould now be displaying that the page you just visited has been viewed once.FixedPageViewsshould be displaying the correct count of views for each page you clicked.FixedPageViewsshould correctly have reset to its initial state, since we unmounted thePagesToDiscovercomponent before going back toHome.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) :Since our machine only tracks page views for the child routes of the
PagesToDiscoverpage, currentlyPageViewsshould only display its name, with no counter of page views below. This is the correct behavior.PageViewsshould now be displaying that the page you just visited has been viewed once. This is the correct behavior.PageViewsdoes 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 :PageViewsshould correctly have reset to its initial state, since we unmounted thePagesToDiscovercomponent before going back toHome. 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
useActorRefin@xstate/vue:xstate/packages/xstate-vue/src/useActorRef.ts
Lines 23 to 33 in ca7f090
The use of
onBeforeUnmounthere 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 theonMounted/onBeforeUnmountbyeffectScope/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:
effectScopeAPI vuejs/rfcs#212The 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.