-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Bug: @xstate/vue
can't be used reliably to create a shared machine across components
#4754
Comments
The idiomatic way to achieve this would be to hoist your machine to some common parent and then pass down the created I'll use a React code snippet here, since that's what I'm familiar with: const counterMachine = setup({}).createMachine({ /* ... */ })
const First = () => {
useActor(counterMachine)
return null
}
const Second = () => {
useActor(counterMachine)
return null
}
const App = () => {
return (
<>
<First />
<Second />
</>
)
} This application doesn't run a single machine but rather two machines - each one is scoped to its own component. |
Indeed, this would be a suitable workaround and do exactly what I need without any changes needed in
I understand that and I never expected that simply using const counterMachine = setup({}).createMachine({ /* ... */ })
const useSharedCounterActor = () =>
createSharedHook(() => useActor(counterMachine));
const First = () => {
useSharedCounterActor();
return null;
};
const Second = () => {
useSharedCounterActor();
return null;
};
const App = () => {
return (
<>
<First />
<Second />
</>
);
}; Now In Vue, this is possible thanks to effect scopes. To sum it up, every reactive effects ( By default, each component define its own scope, so reactive effects are disposed when their parent component is unmounted. However, the There is an example of how it can be implemented in the RFC that introduced this API if you want to learn more about it, but what's important here is that creating shareable hooks is possible in Vue 3.2.x and beyond. There is even an utility function provided by a popular Vue library that allow to do exactly that, without having to code the logic yourself. That's why I expected at first that by wrapping Personally, I still consider the current behavior of I hope that I was able to clear up any misunderstanding and that the explanations I gave were clear enough, if that's not the case feel free to ask me any precisions you need. Thank you for your time and for maintaining this project. 😊 |
Unless I'm missing something, // keys.ts
import type { InjectionKey } from 'vue'
import { Actor } from 'xstate'
import { authMachine } from './actors/authMachine'
export const authKey = Symbol('auth') as InjectionKey<Actor<typeof authMachine>> // Parent component
import { authMachine } from 'src/actors/authMachine'
import { authKey } from './keys'
import { provide } from 'vue'
const { actorRef } = useActor(authMachine.provide(/* ... */)
provide(authKey, actorRef) // Child component
import { authKey } from 'src/keys'
import { inject } from 'vue'
const throwError = () => { throw new Error() }
// typeof inject(authKey) is Actor<typeof authMachine> | undefined
// which is not usable, so I have to narrow it like this :
const authActor = inject(authKey) || throwError() Please correct me if I'm doing something wrong here, but IMHO an API using shared composables would be better suited and more appealing to existing Vue developers, as proposed by @IonianPlayboy. |
I would gladly welcome a contribution for this. |
To be fair, that's kinda on Vue's APIs. I agree that this is somewhat awkward and I prefer how the same thing is done in React. In
That's somewhat similar to |
@IonianPlayboy First of all thank you for your vueuse suggestion. I was just about to give up on xstate vue. Sharing a machine across components seems like a real necessity, at least for me. I'm just trying to think about the future though and if I will eventually run into the same issues as you. Can you explain why your machine stop prematurely? I'm new to xstate. Is that something that can happens as part of the normal lifecycle. Thanks again |
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 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/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
) :Since our machine only tracks page views for the child routes of the
PagesToDiscover
page, currentlyFixedPageViews
should only display its name, with no counter of page views below.FixedPageViews
should now be displaying that the page you just visited has been viewed once.FixedPageViews
should be displaying the correct count of views for each page you clicked.FixedPageViews
should correctly have reset to its initial state, since we unmounted thePagesToDiscover
component 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
PagesToDiscover
page, currentlyPageViews
should only display its name, with no counter of page views below. This is the correct behavior.PageViews
should now be displaying that the page you just visited has been viewed once. This is the correct behavior.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 :PageViews
should correctly have reset to its initial state, since we unmounted thePagesToDiscover
component 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
useActorRef
in@xstate/vue
:xstate/packages/xstate-vue/src/useActorRef.ts
Lines 23 to 33 in ca7f090
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 theonMounted
/onBeforeUnmount
byeffectScope
/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:
effectScope
API 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.
The text was updated successfully, but these errors were encountered: