How to perform two way binding in react (child sending data, parent raising notifications)?

119 Views Asked by At

I have a wizard component, that displays page with next and back buttons. Here is the sample:

function Wizard(props){
  return (
    <Grid container>
      <Grid item xs={12}>
        {RenderPage(props)}
      </Grid>
      <Grid item xs={12}>
          <WizardButton title="Back" onClick={previousPage} />
          <WizardButton title="Next" onClick={previousPage} />
      </Grid>
  </Grid>)
}

export function App(props) {
  return (
    <Wizard className='App'>
      <Page>
        <UserInfo />
      </Page>
      <Page>
        <PaymentInfo />
      </Page>
    </Wizard>
  );
}

the components <UserInfo/> and <PaymentInfo/> have their own forms implemented using formik. and these components are used in other parts of the application also.

enter image description here

my problem is that I want to perform validation of formik as soon as the next button is clicked. If the validation fails, then the wizard should stay on the current page. If there are no validation errors, then the wizard should continue.

so I need to notify the components to perform validation when next button is clicked. and I need to notify wizard about validation result to allow or disallow moving to next page.

As much as I understand this is reverse of how react works i.e. parent notifying child about events and child sending data to parent.

I'm trying a few ways. like for instance PubSub-js which is making code too much complex and also is not the react way. I'm also thinking that this surely is not the first time someone has encountered this problem but I'm unable to find out a way to solve this problem.

2

There are 2 best solutions below

0
Egehan Dülger On

You can hold a state in your Wizard component to represent the validity. You can also pass a function to the child components to indicate how they can inform the parent about their form's validity status.

I don't know how your RenderPage function looks like but I swapped it with a render prop. You can adapt it to your needs.

function Wizard(props){
  const [isValid, setIsValid] = useState(false)

  const setValid = () => setIsValid(true)
  const setInvalid = () => setIsValid(false)

  return (
    <Grid container>
      <Grid item xs={12}>
        {(setValid, setInvalid) => props.children}
      </Grid>
      <Grid item xs={12}>
          <WizardButton title="Back" onClick={previousPage} />
          <WizardButton title="Next" onClick={previousPage} disabled={!isValid} />
      </Grid>
  </Grid>)
}

As for your child Page components, you can now access these validity status related functions and pass them into the respective form components.

export function App(props) {
  return (
    <Wizard className='App'>
      {(setValid, setInvalid) => (
        <Page>
          <UserInfo setValid={setValid} setInvalid={setInvalid} />
        </Page>
        <Page>
          <PaymentInfo setValid={setValid} setInvalid={setInvalid} />
        </Page>
      )}
    </Wizard>
  );
}

Inside the form components whenever the form becomes valid, you can call setValid and accordingly when it becomes invalid, you can call setInvalid. Then the state in the Wizard component will change and the WizardButton component inside will be enabled/disabled.

If you wish to pass the functions in another way, then Context is another option.

0
Simple Fellow On

I was finally able to have the desired functionality. it was too much of code but using useImperativeHandle() forwardRef() useRef() I was able to have it implemented. This is how you would use it in the page suppose if you have two components, <UserInfo/> and <PaymentInfo/>

export function Registration(){

const userRef = usePageRef()
const paymentRef = usePageRef()    

const handleFinish = (arg: Map<string, unknown>) =>
    console.log(`handleFinish(): ${JSON.stringify(arg)}`)

return (

    <Book title="Registration" onDone={handleFinish}>
      <Page id="person" handle={userRef} component={<UserInfo ref={userRef}/>}/>
      <Page id="payment" handle={paymentRef} component={<PaymentInfo ref={paymentRef}/>}/>
    </Book>

  )
}

In the component where you want to implement the functionality you do it like this

export function UserInfo(props: {}, ref: Ref<IPageEvents>){
 const formik = useFormik(/* formik stuff */)

 usePageEvents(ref, 
   class MyEvents extends PageEventsBase{
      override async validate(){
          const await res = formik.validateForm() // or call api
          return true  // or false based on your validation
       }
   }
 )

 return (
        <> {/*your actual component*/ } </>
        )
} 

export default React.forwardRef(UserInfo)

All the back and next button logic is in Book, when you press the Next button, it calls onValidate() of the component through ref the PageEvent. The event is implemented as override class class MyEvents extends PageBaseEvents

How it works

  1. When you click the Next button, the onClick() in Book is called
  2. it gets the IPageEvents of that page which is bound to that component.
  3. it calls the onValidate() of the page which is implemented in the component. Component performs validation in whatever way. it returns true or false
  4. the Book checks the return value and if true it moves to the next page otherwise it stays.
  5. additionally it provides the components a way to save onSave() and onLoad() to store and restore their states thru page events.

How to use

  1. add a Ref<IPageEvents> to your components
  2. use usePageEvent() to implement your page events like onValidate(): Promise<boolean> in your components. (It is not necessary to handle page events, you can have a regular component and the wizard would not call any page events on it. In this case forwarding ref is also not required at all)
  3. have other logic in your components
  4. when exporting, forwardRef() your components
  5. where you use <Book/> use usePageRef() to bind the component to the page in which it is
  6. handle onDone() props of the book.

that's it. I tried so that for users, they have to do as little as possible and all logic should be built in. you can find more details here React Components