Open lxcid opened 4 months ago
@brunocangs any thoughts?
We have the actor/service available on the internal machineAtom
variable, we could look for a way to expose access to it.
I'm not very familiar with the Jotai internals, what would be the correct way to expose this? My initial thought was to add a symbol somewhere so we could consume it from another atom. So we could do something like
const machineAtom = atomWithMachine(...)
const actorAtom = actorFromMachine(machineAtom)
If there was some way to consume the internal machineAtom
from an external atom it would also work. Maybe keep an external store/atom and read from it?
@lxcid What you need is to be able to call the .on()
listeners^1, correct?
Edit: Thinking about it some more, the best way would probably be to decouple the logic, and make each piece along the setup process available to consume separately. That way we don't lose compatibility with the current API.
atomWithLogic
=> Receives Actor logic^2 (eg: createMachine(), fromPromise()
) to setup the instance of the logic. Write function could be used to provide new init arguments.atomWithActor
=> Received either the above atom, or an instanced actor logic. This atom would call createActor
and make it available. This solves the issue by giving direct access to the actor. For the Write function, I think it should default to the send
method, but it could also be used to call .start()
and .stop()
using symbols.atomWithActorState
=> Handles subscription and has the latest snapshot data. Takes an actor or atomWithActor as first argument and an optional second argument ^4 to go along with the internal subscribe()
callatomWithMachine
=> Keeps the same api, but consumes the previous atoms to easily instantiate and send events into a state machine or generic actorIf you think this is a good path, I can look for some time during this week to try to implement it
thank you for considering. generally. a machine is like a configuration and an actor is an instance of the configuration and store the state.
sometimes we want to listen to state transition, thus the on() call or onTransition()
think of machine instance as some kind of observable. able to receive event and changes accordingly.
if i want i can listen to changes to state, but i thought naturally it might make sense to listen to transition from the machine instance.
your proposal sounds great. if i can control each step of the step, i can have more control over it.
I've been building a global shopping cart machine for an app and was having issues with reactivity when an event triggered a state change of my machine. My components were not re-rendering. I need to try to put together a simple reproduction of that issue. It may have to do with the state also invoking an actor.
Anyway... while looking at the code here and reading @xstate/react, I decided to try an alternate implementation that seems to be working for me that I called atomWithActorLogic
:
https://gist.github.com/rmarscher/2676b2b661c7119e7dd2a9449c179718
I took inspiration from @xstate/react's createActorContext code. It uses useActorRef
to create an actor and start it. It then uses useSelector
within a component to add a snapshot listener that triggers updates.
I added a useSnapshot
convenience hook that just returns the entire snapshot from useSelector
.
So instead of component code like this:
const [state, send] = useAtom(machineAtom);
It works like this with an atom created with useActorWithLogic
:
const [actorRef] = useAtom(machineAtom);
const state = useSnapshot(machineAtom);
Instead of send({ type: "some.event" })
, it's actorRef.send({ type: "some.event" })
. And you can do whatever you want with actorRef in your component too.
Sorry -- I know this is off-topic for this issue, but just to follow up to my previous post -- I found a similar issue with useSelector not triggering rerenders.
My states with an invoked actor are calling a server function in React 19 (with Waku). One of the actors works fine but the other seems to not allow react to update until the server function is finished being called and that seems to be preventing the snapshot update. I wasn't able to reproduce with a simpler example with just xstate and client side react. I need to keep working on that (and will open another issue here or upstream if I can track it down). I did find a hack to workaround the issue - using setTimeout with a 0 timeout and calling my server function within the callback function:
const res = await new Promise<
Awaited<ReturnType<typeof serverFunction>>
>((resolve) =>
setTimeout(async () => {
console.log("sending server function request");
const _res = await serverFunction(params);
console.log("finished server function request");
resolve(_res);
}, 0)
);
When I invoke my server function within my fromPromise(...)
actor that way, the snapshots update and my components are reacting to the state changes.
@lxcid @rmarscher We've just released v0.6.0
with lots of changes and improvements, would you be able to test against it and see if the problems persist?
Is it possible for services to be exposed or accessible.
This allow us to hook up listeners post initialization.