13
votes

I want to write a Form component that can export a method to validate its children. Unfortunately a Form does not "see" any methods on its children.

Here is how I define a potential children of Form:

var Input = React.createClass({
  validate: function() {
    ...
  },
});

And here is how I define Form class:

var Form = React.createClass({
  isValid: function() {
    var valid = true;
    this.props.children.forEach(function(component) {
      // --> This iterates over all children that I pass
      if (typeof component.validate === 'function') {
        // --> code never reaches this point
        component.validate();
        valid = valid && component.isValid();
      }
    });
    return valid;
  }
});

I noticed that I can call a method on a child component using refs, but I cannot call a method via props.children.

Is there a reason for this React behaviour?

How can I fix this?

2

2 Answers

17
votes

The technical reason is that at the time you try to access the child component, they do not yet really exist (in the DOM). They have not been mounted yet. They have been passed to your<Form> component as a constructor prop or method as a react class. (hence the name class in React.createClass()).

As you point out, this can be circumvented by using refs, but I would not recommend it. In many cases, refs tend to be shortcuts for something that react wasn't intended for, and therefore should be avoided.

It is probably by design that react makes it hard/ impossible for parents to access a child's methods. They are not supposed to. The child's methods should be in the child if they are private to the child: they do something inside the child that should not directly be communicated upward to the parent. If that were the case, than handling should have been done inside the parent. Because the parent has at least all info and data the child has.

Now in your case, I imagine each input (child) component to have some sort of specific validation method, that checks the input value, and based on outcome, does some error message feedback. Let's say a red outline around incorrect fields.

In the react way, this could be achieved as follows:

  • the <Form> component has state, which includes a runValidation boolean.
  • as soon as runValidation is set to true, inside a setState( { runValidation: true }); react automatically re-renders all children.
  • if you include runValidation as a prop to all children.
  • then each child can check inside their render() function with something like if (this.props.runValidation) { this.validate() }
  • which will execute the validate() function in the child
  • the validate function can even use the child's state (state is not changed when new props come in), and use that for the validation message (e.g. 'please add more complicated symbols to your password`)

Now what this does not yet fix, is that you may want to do some checking at form level after all children have validated themselves: e.g. when all children are OK, submit the form.

To solve that, you could apply the refs shortcut to the final check and submit. And implement a method in your <Form> inside a componentDidUpdate() function, to check if each child is OK (e.g. has green border) AND if submit is clicked, and then submit. But as a general rule, I strongly recommend against using refs.

For final form validation, a better approach is:

  • add a non-state variable inside your <Form> which holds booleans for each child. NB, it has to be non-state, to prevent children from triggering a new render cycle.
  • pass a validateForm function as a (callback) prop to each child.
  • inside validate() in each child, call this.props.validateForm(someChildID) which updates the corresponding boolean in the variable in the Form.
  • at the end of the validateForm function in the Form, check if all booleans are true, and if so, submit the form (or change Form state or whatever).

For an even more lengthy (and way more complicated) solution to form validation in react (with flux) you could check this article.

1
votes

I'm not sure if i'm missing something, but after trying what @wintvelt suggested i ran into a problem whenever i called the runValidation method inside the render method of React, since in my case runValidation changes the state by calling setState in it, thus triggering the render method which obviously is a bad practice since render method must be pure, and if i put the runValidation in willReceiveProps it won't be called the first time because the if condition is not true yet (this condition is changed in the parent component using setState, but in the first call of willReceiveProps it's still false).