#v5: communication between parent and child

1 messages · Page 1 of 1 (latest)

upper stag
#

i am trying to communicate between child and parent but it doesn't seem to be working i.e i can see the log before sendParent but parent machine doesnt seem to be recieving it. what is is wrong?

childMachine

export const applicantDetailsMachine = createMachine( { id: 'applicantDetails', initial: ApplicantDetailsState.Initial, states: { [ApplicantDetailsState.Initial]: { on: { FIELD_CHANGED: ApplicantDetailsState.InProgress, }, }, [ApplicantDetailsState.InProgress]: { always: [ { target: ApplicantDetailsState.Completed, guard: 'areAllFieldsPopulated', }, ], on: { FIELD_CHANGED: { actions: 'assignFieldValue', }, }, }, [ApplicantDetailsState.Completed]: { entry: 'childCompleted', on: { FIELD_CHANGED: { target: ApplicantDetailsState.Completed, actions: 'assignFieldValue', }, }, }, }, }, { guards: { areAllFieldsPopulated: ({ context }) => { return [ context.firstName, context.middleName, ].every(Boolean); }, }, actions: { assignFieldValue: assign(({ event }) => { return { [event.field]: event.value }; }), childCompleted: ({ context, event }) => { console.log('sending parent completed'); sendParent({ type: 'applicationDetails.completed' }); }, }, }, );
parent machine
export const appMachine = createMachine({ initial: 'applicantDetails', states: { applicantDetails: { invoke: { src: applicantDetailsMachine, }, on: { 'applicationDetails.completed': { target: 'assetDetails', actions: () => { console.log('Received completed event!'); }, }, }, }, assetDetails: {} }, });

upper stag
#

just to give a bit more context that i m trying to create a multi stage kind of an application i.e. at the application level following are
the states applicantDetails, assetDetails, finalDetails etc.
within the applicantDetails i am maintaing 3 states i.e. initial, inprogress and completed
applicantDetails would move to inprogress as soon as user update any input like firstName, lastName etc. when all the inputs are completed then applicantDetails would move to completed and also the main application should move to assetDetails and show the assetDetails screen (which would only be visible once the applicantDetails is completed)

ruby plaza
#

Can you put this in a CodeSandbox (or at least a minimal repro)?

#

Oh wait I see what's happening

#
      childCompleted: ({ context, event }) => {
        console.log('sending parent completed');
        sendParent({ type: 'applicationDetails.completed' });
      },

The sendParent function is an action creator; it is not imperative

#

So you should do:

childCompleted: sendParent({ ... })
upper stag
#

Thanks for your help, updating it is now causing me this error

ruby plaza
#

Interesting. Can you make a minimal repro?

upper stag
ruby plaza
#

I see the problem!

  const [state, send] = useMachine(applicantDetailsMachine, { devTools: true });

^ Here you are using the child machine standalone. It doesn't have a parent

#

Remember, a machine is just a "blueprint"; it isn't the actual actor, but it's used to create the actor.

Another analogy: it's the class "definition", but not the class instance

#

To fix this, you'd want to do something like this:

export function App() {
  const [state] = useMachine(appMachine);
  const applicantDetailsActor = state.children.applicantDetails
  console.log('appState:', state.value);
  return (
    <div>
      {applicantDetailsActor && <ApplicantDetails actor={applicantDetailsActor} />}
    </div>
  );
}
#

And then in that component, instead of useMachine, you'd just useSelector

#
const state = useSelector(actor, s => s); // just grab everything, or what you need
// send is actor.send
upper stag
#

Thanks a lot. That fixed the issue and now i can see that the parent state is moved to assetDetails.
but as soon as it moves to assetDetails application crashes. is it because the next state assetDetails is empty object?

ruby plaza
#

No that should be fine

#

Are you checking for the existence of applicantDetailsActor?

upper stag
#

hmm i can see that the actor becomes undefined so should i check fot its existence in useSelector and if it becomes undefined then fall back to what?

ruby plaza
#

You can do useSelector(actor ?? createEmptyActor(), ...)

#

In the near future, we will support undefined as an actor in useSelector

upper stag
#

Thanks, that fixed the issue but now since the state has become undefined as well it is trying to change the input value from defined to undefined. also even though the parent state is now moved to assetDetails but i would still like applicantDetails form to be able to edit/update the values i.e. even in completed state ( i think i should rename it to edit state) i want to handle the field change event

#

const state = useSelector(actor ?? createEmptyActor(), s => s); const { title, firstName, middleName, lastName, mobilePhoneNumber, businessEmail } = state.context ?? {}; const handleChange = (field: string) => (nextValue: string) => { actor.send({ type: 'FIELD_CHANGED', field, value: nextValue }); };

also since the actor becomes undefined so i can't now use actor.send with in handleChange on input update

ruby plaza
#

This is why I recommend rendering null for that if the actor isn't available

upper stag
#

sorry didn't get you ? actually i want to continue editing form if the parent state is moved to next state as well. i.e. child state goes into edit mode, the parent state is moved to next stage i.e. next stage panel would be visible to user but he could still update the applicant Details as well (which is in edit state)

ruby plaza
#

This part:

{applicantDetailsActor && <ApplicantDetails actor={applicantDetailsActor} />}
#

Don't render the component if the actor doesn't exist

upper stag
#

yeah but as i said i still want to render the component even if the parent state moves to next stage i.e. assetDetails

ruby plaza
#

This is a matter of adding conditional checks if that actor exists

upper stag
#

so does the actor stops existing if it moves to a new parent state?
state.children.applicantDetails can i access it some other way because see i want to still keep rendering the applicantDetails component ( and keep updating its state) even if the parent state moves to next state (i.e i want to keep the applicantDetails actor)

ruby plaza
#

The invoked actor is only "alive" in the state it's invoked in

upper stag
#

hmm ok so what is the proper way to handle this scenario?

#

can i invoke multipe children i.e. invoke applicantDetails in the assetDetails state as well?

#

or invoke something like using always: ?

upper stag
#

it seems like always doesn't have any invoke property

ruby plaza
#

It is a transition

#

What are you trying to do? Always invoke something?

upper stag
#

yes

ruby plaza
#

You can move the invoke: to the root

#
createMachine({
  // ...
  invoke: { ... }
})
upper stag
#

perfect. Thanks a lot for your help that would do. one more query though how can i update the parent context i.e. every time any component local state is updated i want to notify the parent and updates application level state.

ruby plaza
#
invoke: {
  src: ...
  onSnapshot: {
    actions: assign({
      //...
    })
  }
}
upper stag
#

Great i will have a lookl !! Thanks a lot for your help.

ruby plaza
#

No problem!

upper stag
#

Hi david, need one more help, this onSnapshot seems to be triggered only for the first time the application is initiated, i thought (and what i want actually) is it to be called, everytime the invoked machine's internal state changes or an event is being emitted.

#

also is there a way that child machine keeps sending event to parent even though it is reaching to its final state for example, if my components final state is "edited", i want to keep updating the application level machine's state.

ruby plaza
ruby plaza
upper stag
#

What i mean is that when parent state moves to a next state but the previous state's machine is at its last state (edit state) i would like to still recieve some kind of event to update the parent (application level) state

#

in this example as soon as applicationDetails moves to completed state the application state moves to assetDetails state but i still want to keep update the application level state everytime the component level state changes

#

so this is what i get in onSnapShot when application loads for the first time and after that nothing else

upper stag
#

ok i think i have figured it out this is what i am doing at the root level i have added an on: { 'applicationDetails.changed' : ....} so even if application machine goes to another state, it could still listen to change event coming from the previous state and keep updating the application level state.

Please let me know if it is a valid way to do it or is there a better way to handle it.

export const appMachine = createMachine({ initial: 'applicantDetails', id: 'application', context: { applicantDetails: {}, }, on: { 'applicationDetails.changed': { actions: assign(({ event }) => { return { applicantDetails: event.data }; }), }, }, invoke: { id: 'applicantDetails', src: applicantDetailsMachine, }, states: { applicantDetails: { on: { 'applicationDetails.completed': { target: 'assetDetails', actions: assign(({ event }) => { return { applicantDetails: event.data }; }), }, }, }, assetDetails: {}, loanDetails: {}, reviewApplication: {}, completed: {}, error: {}, }, });