Giter Site home page Giter Site logo

Comments (5)

IonianPlayboy avatar IonianPlayboy commented on May 27, 2024 2

The idiomatic way to achieve this would be to hoist your machine to some common parent and then pass down the created actorRef using the provide/inject API ( https://vuejs.org/guide/components/provide-inject ).

Indeed, this would be a suitable workaround and do exactly what I need without any changes needed in @xstate/vue. I overlooked this alternative because I'm much more used to use hooks instead when possible, and this is less ideal for me since it would force me to use two different ways of interacting with machine depending on if they should be shared or not, but it would work just fine.

useMachine/useActor starts separate instances so that's not what you are looking for.
[...]
This application doesn't run a single machine but rather two machines - each one is scoped to its own component.

I understand that and I never expected that simply using useMachine/useActor out of the box would create a shared machine/actor across components. I guess I did not make it clear in the issue that I was wrapping the useMachine hook inside a function that would make it shareable, so to better reflect my intent your React snippet would look more like this :

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 First and Second would share access to the same instance of the counter machine, instead of having each their own separate instance.

In Vue, this is possible thanks to effect scopes. To sum it up, every reactive effects (ref, computed, watch, etc...) are bound to a scope, and are disposed/collected when the scope is terminated. This is how Vue keep track of everything and knows when it should stop and garbage collects what's not needed anymore.

By default, each component define its own scope, so reactive effects are disposed when their parent component is unmounted. However, the effectScope API allows to define a custom scope that is not bounded to a particular component, and it can be used to create a shareable hook that will keep track of the places where its reactive effects are needed (i.e its suscriber), and can stop itself when all the related components have been unmounted.

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 useMachine/useActor inside a createSharedComposable utility, I would be able to reuse the same machine across components. However, because useActorRef calls directly onBeforeUnmount, its reactive effects are stopped independently from its parent scope, which breaks the shared instance when a component unmount, for example when changing routes. On the repro, the "fixed" version of the machine hook/composable uses a patched useActorRef that implements the effectScope API , which is why it is working as intended vs the out-of-the-box version.

Personally, I still consider the current behavior of @xstate/vue to be a bug, or at least unexpected behavior, but as you pointed out a workaround is available. I would understand if you considered that it makes fixing this issue a noop or at least not your priority, especially since it would require to upgrade the vue version to at least 3.2.x, which is a breaking change.

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. 😊

from xstate.

FrankySnow avatar FrankySnow commented on May 27, 2024 1

Unless I'm missing something, provide/inject is easy until you want to add type safety to it, which causes some indirections that add boilerplate and in the end force you to type-cast something (either the injection key and/or the provided value) :

// 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.

from xstate.

Andarist avatar Andarist commented on May 27, 2024

The idiomatic way to achieve this would be to hoist your machine to some common parent and then pass down the created actorRef using the provide/inject API ( https://vuejs.org/guide/components/provide-inject ). useMachine/useActor starts separate instances so that's not what you are looking for.

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.

from xstate.

davidkpiano avatar davidkpiano commented on May 27, 2024

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.

from xstate.

Andarist avatar Andarist commented on May 27, 2024

Unless I'm missing something, provide/inject is easy until you want to add type safety to it, which causes some indirections that add boilerplate and in the end force you to type-cast something (either the injection key and/or the provided value)

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 @xstate/react we have createActorContext that binds a couple of pieces together (type-wise). A similar approach could be used here - that would hide inject/provide dance behind the scenes.

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.

That's somewhat similar to createActorContext idea. I'm not sure how the @IonianPlayboy's idea would be implemented though. It refers to createSharedHook but it doesn't go into its details and - from what I can tell - this is not something that Vue provides.

from xstate.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.