Xstate: Unable to pass invoked machines (children) down to child components in React

Created on 22 May 2020  路  12Comments  路  Source: davidkpiano/xstate

Description
Children cannot be passed to child components when invoked. I'm working with React, though this probably isn't specific to React.

Expected Result
I'd like to invoke a machine, then pass it down to a child component to control what is rendered in the child component. Then I'd like to use sendParent to send events back to the parent machine to transition to other states as required.

Actual Result
The child isn't available initially and so useService can't be called on it (I think?).

Reproduction
https://codesandbox.io/s/invoke-child-bug-ivid6

bug 鈿涳笌 @xstatreact

Most helpful comment

The children are declared as Actors...

Yes; #1215 will add the useActor hook that will remedy this, so this is what that will look like:

// Note that `latest` may be `undefined` for promise/observable actors!
const [latest, send] = useActor(someActor);

Also #1202 was just merged - I will release an RC for that soon.

All 12 comments

This would be indeed very nice if it would work somehow. The problem appears to be that children get modified slightly later after the re-render of useMachine happens and we end up passing undefined to useService which cannot be reinitialized later when children arrive.

Theoretically the child component could just not render until a service is passed in I guess.

I tried to just handle the missing service in the child by checking for a missing state.

const [current, send] = useService(service);
  // hack: avoid missing service
  if (!current) {
    return <div />;
  }
  return (
    <div>
      {current.context.value}
      <button onClick={() => send("CREATE")}>Create</button>
    </div>
  );

However, while that worked I ended up in the same situation where the sendParent action didn't seem to work as reported in #1201 except here we're using invoked machines and not spawned machines.

@Blacktiger Your "hack" certainly cannot work like that, because useService hook will consider only initial value, for any subsequent renders it's effectively ignored. The better hack is to convince React reconciler to throw away that component and do it from scratch with key prop.

However, there is something else strange. In the case of spawn, we are getting Interpreter, however by invoking the children doesn't seem to be proper interpreters. That's probably a major reason why it doesn't work.

image

The problem you are having is that you have 2 separate states in which you have a different service invoked but you render 2 components (each wanting to access one of those invoked services) unconditionally. That it doesnt work seems quite natural to me as you implicitly create a bound between a component and a service so how could component render OK without its counterpart being live?

Keep in mind that invoked services stay active only when a machine stays within containing state. If you want to have manual control over their lifetime then you can use spawn instead of invoke.

The problem you are having is that you have 2 separate states in which you have a different service invoked but you render 2 components (each wanting to access one of those invoked services) unconditionally.

I'm confused, in the linked codesandbox I'm using current.matches('list') && <List service={current.children.listMachine} /> to render the list component only when we should have a child machine. How is that not conditionally rendered?

@Blacktiger I think that matches checks for the state only, but children are most likely set a bit later. When you change it to explicit current.children.listMachine && <List service={current.children.listMachine} /> it sort of works better, but then there is a problem I outlined above. The invoked children are different are something else and cannot be consumed by useService imo.

I'm confused, in the linked codesandbox I'm using current.matches('list') && to render the list component only when we should have a child machine. How is that not conditionally rendered?

Sorry, I had to misread the code in a hurry.

When you change it to explicit current.children.listMachine && it sort of works better, but then there is a problem I outlined above.

This doesn't matter - this.children keys are set immediately.


This problem is one of several caused by the latest changes to make all actions (including invoke) scheduled through React's commit phase - we are reverting this and providing means to actually schedule a particular action through React's commit phase in https://github.com/davidkpiano/xstate/pull/1202 . When it lands I will make sure to add a test covering for the scenario described here.

Thanks, I just wanted to make sure there wasn't any confusion. Sounds like this will be closed by #1202 which is fantastic.

Seems that #1202 will take more time so I wonder what would be viable workaround till then? Is there some safe way of accessing invoked services to pass them to children components for consumption?

Btw, I also noticed a TypeScript "issue" that kinda confirms the problem I outlined above.

The children are declared as Actors...

https://github.com/davidkpiano/xstate/blob/c29ab8a82b47c15d00b31e467b564420fb404b7f/packages/core/src/State.ts#L129

While useService expects Interpreter

https://github.com/davidkpiano/xstate/blob/c29ab8a82b47c15d00b31e467b564420fb404b7f/packages/xstate-react/src/index.ts#L120

Furthermore, I see that Interpreter implements Actor so there is unidirectional compatibility. I could pass Interpreter instance where Actor is expected. However, when I get Actorfrom the machine's children, it's not fully-fledged Interpreter and as such can be hardly used by useService.

Looking at the screenshot above it's clear, that Actor doesn't carry a state and as such, I don't see how it could be even possible to consume that with useService.

So I doubt this is really just a problem of delayed action execution.

The children are declared as Actors...

Yes; #1215 will add the useActor hook that will remedy this, so this is what that will look like:

// Note that `latest` may be `undefined` for promise/observable actors!
const [latest, send] = useActor(someActor);

Also #1202 was just merged - I will release an RC for that soon.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

3plusalpha picture 3plusalpha  路  3Comments

pke picture pke  路  3Comments

carlbarrdahl picture carlbarrdahl  路  3Comments

dakom picture dakom  路  3Comments

ifokeev picture ifokeev  路  3Comments