1
votes

In react best practice is Data flow from parent to child, event's will be passed from child to parent.

How to a  avoid anti pattern for this UI?

In this UI we have a parent component which contains 2 child components with forms. We now have to gather data from child components when the user clicks on the submit button in the parent.

Possible solutions (Bad solutions | anti pattern):

  1. Pass ref from child and trigger child method from parent to collect data (Accessing child method from parent not a good idea)
 <Parent>
    onFormData(data) {
     //here collect data from child
    }

    onSubmit() {
      //Trigger a method onData in child
      aRef.collectData()
    }

    <child-A ref={aRef} onData={onFormData}></Child-A>
    <Child-B ref={bRef} onData={onFormData}></Child-B>
    
    <button onClick={onSubmit}>Submit</Submit> 
</Parent>
  1. Bind a props to child, on submit click change value of prop with dummy value. Observe same prop in useEffect hook ( Really bad solution)
 <Parent>
    onFormData(data) {
     //here collect data from child
    }

    onSubmit() {
      //update randomValue, which will be observed in useEffect that calls onData method from child
      randomValue = Math.random();
    }

    <child-A triggerSubmit={randomValue} onData={onFormData}></Child-A>
    <Child-B triggerSubmit={randomValue} onData={onFormData}></Child-B>
    
    <button onClick={onSubmit}>Submit</Submit> 
</Parent>

Is there any other best approach for handling these scenario? How to a avoid anti pattern for this UI?

3
To better answer the question, could you clarify why you need two forms instead of one? - yaiks
here even it could be just one form.. This is a just a sample example to explain the problem.. The problem here is how to notify child to trigger event when submit button is in parent... - ksh

3 Answers

0
votes

What I usually do is to "lift the state up" to the parent. This means I would not break the natural React flow, that is passing props from the parent to the children. Following your example, I would put ALL the logic in the parent (submit function, form state, etc)

Const Parent = () => {
  const [formData, setFormData] = useState({})

  const onSubmitForm = () => {
      // send formData to somewhere
  }

  return (
      <ChildrenForm onChange={setFormData} formData={formData} />
      <button onSubmit={() => onSubmitForm()}>my button</button>
  )
}

Now I would use the onChange function inside the Children to update the formData everytime an input in the ChildrenForm changes. So all my state will be in the parent and I don't need to worry about having to pass everything up, from children to parent (antipattern as you mentioned)

0
votes

there is the third option (which is the standard way): you don't collect data, you pass your formData and setFormData to each Child as props. using lift state approach.

Each Child populates its input values with formData and uses setFormData to update formData that lives on Parent. Finally, at on submit you only have to set formDatato you request call.

below an example:

const ChildA = ({ formData, setFormData }) => {
  const { name, age } = formData
  const onChange = ({ target: { name, value } }) => { // destructuring 'name' and 'value'
    setFormData(formData => ({ ...formData, [name]: value })) // spread formData, update field with 'name' key
  }
  return (
    <>
      <label>Name<input type="text" onChange={onChange} name="name" value={name} /></label>
     <label>Age<input type="number" onChange={onChange} name="age" value={age} /></label>
    </>
  );
}

const ChildB = ({ formData, setFormData }) => {
  const { email, acceptTerms } = formData
  const onChange = ({ target: { name, value } }) => {
    setFormData(formData => ({ ...formData, [name]: value }))
  }

  const onClick = ({ target: { name, checked } }) => {
    setFormData(formData => ({ ...formData, [name]: checked }))
  }

  return (
    <>
      <label>email<input type="email" onChange={onChange} name="email" value={email} /></label>
      <label>Accept Terms<input type="checkbox" onChange={onClick} name="acceptTerms" checked={acceptTerms} /></label>
    </>
  );
}


const Parent = () => {
  // used one formData. you could break down into more if you prefer
  const [formData, setFormData] = useState({ name: '', age: null, acceptTerms: false, email: undefined }) 

  const onSubmit = (e) => {
    e.preventDefault()
    // here you implement logic to submit form
    console.log(formData)
  }

  return (
    <>
      <ChildA formData={formData} setFormData={setFormData} />
      <ChildB formData={formData} setFormData={setFormData} />
      <button type="submit" onClick={onSubmit}>Submit</button> 
    </>
  );
}
0
votes

Approach 2) with random is Not Really So Bad! And only this one preserves encapsulation not involving DOM/withRef round trip. Suggested Up State way leads to hard coded dependency and undermines whole idea of re-usability. The only thing I have to mention is: code as it was typed above will not work - because usual variables will not trigger. Right way to make it like that:

// Parent component

let [randomValue, setRandomValue] = React.useState(Math.random());
const onSubmit = () => { 
  setRandomValue(Math.random());
  console.log("click");
}
const onFormData  = data => {
  console.log(`${data}`);
}
...
<Child triggerSubmit={randomValue} onData={onFormData}/>

// Child component

useEffect(() => {
  console.log("child id here");
  prop.onData(`Hey Ho! Lets go!`)
}, [prop.triggerSubmit]);

At least this works for me!