Skip to content
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

[Feature] Expose Invoked Services #424

Closed
davidkpiano opened this issue Apr 12, 2019 · 17 comments
Closed

[Feature] Expose Invoked Services #424

davidkpiano opened this issue Apr 12, 2019 · 17 comments
Labels
enhancement 💬 RFC Request for comments

Comments

@davidkpiano
Copy link
Member

Bug or feature request?

Feature

Description:

When services are invoked in a parent service, their state is currently kept hidden to their machine. This is by design, we can traverse this invocation tree and "query" the state of each invoked service by subscribing.

(Feature) Potential implementation:

Consider a parent todosMachine like this:

const todosMachine = Machine({
  id: 'todos',
  initial: 'active',
  states: {
    active: {
      invoke: { src: 'todosService', id: 'todos' },
      on: { ADD_TODO, DELETE_TODO, UPDATE_TODO }
    },
    inactive: {}
  }
});

Then, assuming todosService is implemented as a Machine, we can subscribe to its state the same way you would do with any other service:

const todosService = interpret(todosMachine)
  .onInvoke(childService => {
    childService.onTransition(state => {
      // current state of child service
    });
  })
  .start();

You can imagine that this would be useful in a React app:

function TodosApp() {
  const [current, send, service] = useMachine(todosMachine);
  
  // listen to state from the todosService, whenever it's invoked
  // undefined if service is not invoked or stopped
  const [currentTodos] = useChildService(service, 'todos');

  const todos = currentTodos ? currentTodos.context.todos : [];

  return current.matches('active') ? (
    <ul>
      {todos.map(todo => <li key={todo.id}>{todo.message}</li>}
    </ul>
  ) : null;
}

The new APIs for xstate (core) would be:

  • service.onInvoke(childService => { ... })
  • service.children - already exists, but mapping would provide the actual services instead of thin interfaces. Only to be used for debugging.

The new APIs for @xstate/react would be:

  • useService(service) - same as useMachine() but with a running service
  • useChildService(service, id) - same as useMachine() but with a running child service whenever the child is invoked

This is tentative and I'm still researching to see how this works with the Actor model. Would love feedback!

@davidkpiano davidkpiano added enhancement 💬 RFC Request for comments labels Apr 12, 2019
@LunarLanding
Copy link

Coming from this conversation, I see that this RFC helps with the issue of getting the state of invoked services out of the parent machine so that external behavior might be derived from them (such as rendering todos in the example above).
I had assumed till now that that was already possible.
However is does not address the main problem, which is managing a dynamic number of invocations,i.e. creating on demand.

@davidkpiano
Copy link
Member Author

@LunarLanding In your view, how would the API look if XState were capable of managing dynamic invocations, created on demand, etc.?

For example, how would you want to represent the TodoMVC example with an alternative architecture (something less complicated than the game you presented)?

@LunarLanding
Copy link

I don't know the specifics of the todoMVC, but the key change would be invoking as an action.

const singleTodoMachine = Machine({
  id: 'todo',
	initial: 'active',
	context:{text:''},
	onEntry:'linkRenderer',
  states: {
    active: {
	on:{
	commit:assign({text:(ctx,event)=>event.text}),
			deleted:'deleted'
		}
	},
    deleted: {
			type:'final'
		}
  }
});

const todosMachine = Machine({
	id: 'todos',
	initial: 'active',
	context:{todos:[]}
	states: {
		active: {
			on: {
				addTodo:{
					cond:???
					internal:true,
					actions:[
						invoke({
							src:'todo'
							id:(ctx,event)=>event.id
						}),
						assign({todos:(ctx,event)=>ctx.todos.append(event.id)})
					]
				}
			}
		},
		inactive: {}
	}
})

Rendering:
todosMachineInterpreter.state.children
: map id->children interpreter

inside callback onTransition for todosMachine, setup onTransition for new singleTodoMachine 

Adding invoke as an action:
So what this ends up adding to xstate is a way to wire up events with sendTo and sendToParent to actors, which are other machines.

A limitation:
Suppose I want to limit the number of todos.
Then todoMachine needs a condition that depends on the number of services invoked.
So guards either need access to state.children, or they need to keep track in todosMachine context of the number of existing todos, or keep a reference to each todo Machine and then use those references to access each instance via a global registry and from there get their state.
I.e. anytime the machine needs to do computation that uses it's own state (for instance involving multiple orthogonal states substates) it is going to need to look at itself, i.e. what it does will depend not only on the context or current state where the transition is taking place, but on the states of multiple substates.

In my game code right now I have a class User, with a static registry of instances, and each instance contains a interpreter for a UserMachine. The gameMachine keeps in its context a set of references to User machines, and uses User.registry to get their interpreters and send them messages.

@davidkpiano
Copy link
Member Author

@LunarLanding Related proposal: #428 (please comment, would love to see your thoughts re: a potential API!)

@hnordt
Copy link
Contributor

hnordt commented Jul 26, 2019

@davidkpiano I think you can close this issue, no?

In v4.6 you can do::

let [state, send] = useMachine(fooMachine)
let [childState, childSend] = useService(state.children.get("someChildService"))

@andrejohansson
Copy link

@hnordt is that really working? For me, I have no children property on my state (using typescript, men even casting to any gives no children property). There is however a children property on the service instance.

  const [currentState, send, service] = useMachine(parentMachine);
  let [childState, childSend] = useService((service as any).children.get("childMachineId"));

Trying to access an invoked child machine like this gives an error in all the states for which the child machine is not invoked.

I am using "xstate": "^4.6.7" .

Any idea of how I can listen to invoked child machine transitions? I opened an issue with the question here: #637

@davidkpiano
Copy link
Member Author

There is no children property on state yet, but there will be.

@andrejohansson
Copy link

@davidkpiano ....still trying to wrap my head around this. Perhaps you can enlighten me conceptually.

If I have nested machines, which invokes other machines and so on. This is a very nice setup to break down complex interactions and UIs.

But I cant seem to grasp how I actually connect the machines to actual ui rendering.

Using the react hooks to get the current state works nicely, as long as I don´t want to query a nested machines state (as discussed in this issue).

A couple of questions that comes to my mind are:

  • If you design an ui, with nested machines. How do you render parts of the nested machine states and context?
  • Are you sending the information that needs to be rendered up using events?
  • Is there some other method to access the information except for what is used in this issue?
  • Is it not supposed to be used in this way?
  • Using state.matches(), how do I match on an invoked machines state (I tried the dot notation but I think it only applies to nested states within the same machine and not invoked ones)?
  • Maybe my problem is that I invoke machines, and I should just "include" them in the tree hierarchy instead?

As a concrete excample, we could take your example with the text format state machines.

Say I have one root machine and three paralell machines invoked from the root machine (bold, italic, underline). Now I would want to render a toolbar with buttons toggled or not depending on the states in the child machines. How would I do that?

Sorry for throwing it all out there but my mind hurts right now. :-)

@davidkpiano
Copy link
Member Author

davidkpiano commented Aug 29, 2019

If you design an ui, with nested machines. How do you render parts of the nested machine states and context?

When this feature goes in, you pass the service as props:

const App = () => {
  const [current, send] = useMachine(...);

  const { someService } = current.children;

  return (
    <SomeComponent service={someService} />
  );
}

const SomeComponent = ({ service }) => {
  const [current, send] = useService(...);

  // ...
}

Are you sending the information that needs to be rendered up using events?

Yes, you can do that, or you can idiomatically use callback props in actions (e.g., onClick, onChange, etc.)

Is there some other method to access the information except for what is used in this issue?

Currently, you can grab child services via the parent service:

const [current, send, service] = useMachine;

service.children; // Map()
service.children.get('someService'); // child service

Is it not supposed to be used in this way?

Right, that's more meant for internal use, but nothing's stopping you right now.

Using state.matches(), how do I match on an invoked machines state (I tried the dot notation but I think it only applies to nested states within the same machine and not invoked ones)?

With useService like the first example.

Maybe my problem is that I invoke machines, and I should just "include" them in the tree hierarchy instead?

Depends how you want to model it. If the nested state is closely tied to the parent state, then make it one machine. If it should be loosely coupled, make it an invoked machine.

EDIT: I'm making a Subreddit example (same one as the Redux docs) to highlight how these invoked machines play together.

@andrejohansson
Copy link

Thank you so much for these answers and your time. I will try to get a bit further with the answers.

One thing though, grabbing the service by using your example does not work for me. It seems like the grabbed service is not updated or I'm looking at a previous instance or something.

  const [currentState, send, service] = useMachine(rootMachine);

  const childMachine= (service as any).children.get('childMachineId');
  const childMachineStatev= childMachinev? childMachine.state : undefined;

  console.log(currentState); // Logs correct state, the one that invokes childmachine
  console.log(childMachineState); // Logs wrong state, child machine have progressed further that this shows. Its like its another instance or not receiving updates.

@davidkpiano
Copy link
Member Author

Can you make a reproducible example? Also note the typos in your code snippet (childMachinev etc.)

@andrejohansson
Copy link

andrejohansson commented Aug 29, 2019

Here is an example attached (an empty create-react-app + xstate). Where you can see that the label does not render at all. Even though I can see the object if I log them.
xstate-424.zip

image

This also surprises me....is it some typescript thing going on here?
image

@andrejohansson
Copy link

I managed to get a bit further with the following steps:

I installed xstate 4.7.0-rc2 then I could get things working using your instructions above and the following code:

  const [current, send, service] = useMachine(rootMachine);
  const childService = service.children.get('childMachineId');

  return (
      <div className="center">

        { childService &&
          <StateLabel service={childService} />
        }

      </div>
    )

@kyleconkright
Copy link

@davidkpiano Any plans to add the onInvoke method? Currently working on an implantation of xstate and this is the exact thing I need.

@davidkpiano
Copy link
Member Author

@davidkpiano Any plans to add the onInvoke method? Currently working on an implantation of xstate and this is the exact thing I need.

Not yet, it might not be necessary, since you can accomplish the same thing by checking for the existence of state.child[someId] in .subscribe(...) to detect if something has been invoked.

@Andarist
Copy link
Member

@kyleconkright it would also be nice if u could tell us more about your use case. Maybe we'd have some recommendations on how you could do what you want

@kyleconkright
Copy link

kyleconkright commented Oct 18, 2021

@kyleconkright it would also be nice if u could tell us more about your use case. Maybe we'd have some recommendations on how you could do what you want

Thanks. I'm using xstate to manage a flow in Angular. I have an outer module with xstate initializing some values and putting them into context. Then, I conditionally invoke one of two possible machines. Each of these machines are in their own Angular module as well. First time users will go through machine A and then be placed into machine B. Returning users will be placed straight into machine B.

If I listen to a child machine in a child module, I get a new instance of the machine and not the version invoked by the xstate parent.

I think I can use @davidkpiano's suggestion for getting the correct machine and not a new instance. Am I making sense here?

Edit: Looks like I need the concept of useService in react, in my rxjs implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement 💬 RFC Request for comments
Projects
None yet
Development

No branches or pull requests

6 participants