Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reactjs - correct way of inherit props to first level children and nested children

Tags:

reactjs

In my case I try to create a simple Form Component - mostly for "testing" reactjs and work with it.

To do this I work with 2 Components. The first Component is the Parent, the "Form" Component. The second Component is the field of the form - for example a simple textfield. This is the markup it would look like:

<Form
  schema={MyFormSchema}
>
   <Input name="name" />
   <Input name="age" />
</Form>

In MyFormSchema I have all information which I need for every Child of the type "Input". For this case I have done this in my "Form" Component:

Form.jsx

Form = React.createClass({
  renderChildren() {
    return React.Children.map(this.props.children, (child)=>{
      if (child.type && child.type.prototype && child.type.prototype.constructor.displayName === 'Input') {
        let additionalProps = {
            fieldSchema: this.props.schema.pick(child.props.name),
            onChange: this.onChange
        };
        return React.cloneElement(child, additionalProps);
      }
      return child;
    });
  },
  render() {
    return(
      <form>
        {this.renderChildren()}
      </form>
    );
  }
});

What I am doing here is to "clone" every "input" child and add some new props depending on the schema.

So the first question is:

Is this really the correct war in reactJs ? When I am not "cloning" every element and adding new properties I have to add the property directly in my View, right ? Something like but I am trying to prevent this because all information I need I already have as a prop in my Form Schema.

After playing around with this I found out, that this.props.children only have the first level of children. But when I have nested my Children in my Form Component it will not work anymore that my Component is replacing the Input Component with the manipulated component.

Example:

<Form
  schema={MyFormSchema}
>
   <AnotherComponent>
       <Input name="name" />
   </AnotherComponent>
   <Input name="age" />
</Form>

When I am doing it like I now doing it this code will not work anymore because in this.props.children I only have [AnotherComponent, Input[name=age]] and the Input[name=name] is missing. So I think the way I am doing it is the wrong way. But i am not sure.

So the main question is:

Like in my example: What is the correct way in ReactJs to inherit props (or what ever) to all children (also the nested one) - or is this not possible in the "react" way and I really have to pass all necessary props to all children ?

Edit:

When I am talking about "pass all necessary props to all children" I mean something like this:

<Form
  schema={MyFormSchema}
>
   <AnotherComponent>
       <Input name="name" fieldSchema={this.getFieldSchema('name')} onChange={this.onChange} />
   </AnotherComponent>
   <Input name="age" fieldSchema={this.getFieldSchema('age')} onChange={this.onChange} />
</Form>

In this example I would pass all necessary props I want to add dynamically by the parent. In my example above the next problem would be: "this" would not work for the name input because of its parent AnotherComponent. So I would have to reference to the parent - of course: its possible, but I think it would be a ugly way.

like image 791
TJR Avatar asked Dec 27 '15 20:12

TJR


Video Answer


3 Answers

There are three correct ways to deeply pass props:

1) Just actually pass them down the tree from each component to the next (this is the most readable (in terms of code logic), but can get unwieldy once you have too many props to pass and lots of levels in your tree.

Example:

import React from 'react';    

var GrandParent = React.createClass({
  render () {
    return (
      <Parent familyName={'componentFamily'} />
    );
  }
});

var Parent = React.createClass({
  render () {
    return (
      <Child familyName={props.familyName} />
    );
  }
});

var Child = React.createClass({
  render () {
    return (
      <p>{'Our family name is ' + props.familyName}</p>
    );
  }
});

2) Use a Flux-style store (I prefer Reflux, though Redux is all the rage right now) to keep a common state. All components can then access that store. For me at least, this is the current preferred method. It's clear and it keeps business logic out of the components.

Example (using Reflux):

import React from 'react';
import Reflux from 'reflux';

var MyStore = Reflux.createStore({
  // This will be called in every component that connects to the store
  getInitialState () {
    return {
      // Contents of MyFormSchema here
    };
  }
});

var Input = React.createClass({
  propTypes: {
    name: React.PropTypes.string.isRequired
  },
  mixins: [Reflux.connect(MyStore)],
  render () {
    // I don't know what MyFormSchema so I'm generalizing here, but lets pretend its a map that uses the name of each field a key and then has properties of that field within the map stored at the key/value
    return (
      <input type={this.state[name].type} name={this.props.name} value={this.state[name].type} />
    );
  }
});

3) Use React's context feature. As you'll see immediately from looking at the docs, this feature is still in development and is subject to possible change and even removal in future versions of React. So, while it is likely the easiest way to pass props down a tree of components, personally I'm staying away from it until it becomes more of a finalized feature.

I'm not going to write an example for this one since the docs make it very clear. However, make sure to scroll down on the doc page and take a look at Parent-child coupling, which is kind of what you're doing right now.

Another solution for you is that instead of having a single component that renders Form and its Inputs, why not just pass the prop to Form as you do currently, and then simply render the individual Input using Form's render().

like image 136
Matthew Herbst Avatar answered Nov 11 '22 13:11

Matthew Herbst


You could use react-addons-clone-with-props package this way:

import React, { Component } from 'react';
import cloneWithProps from 'react-addons-clone-with-props';

// ...

class Form extends Component {
  recursivelyMapChildren(children) {
    return React.Children.map(children, child => {
      if (!React.isValidElement(child)) {
        return child;
      }

      return React.cloneElement(child, {
        ...child.props,
        children: this.recursiveCloneChildren(child.props.children)
      });
    })
  }

  render() {
    return (
      <form>{this.recursivelyMapChildren(this.props.children)}</form>
    );
  }
}

What the code does:

  1. Gets all the children components via predefined children prop (see docs).
  2. Recursively maps the collection of children with React.Children.map method, applying a lambda function to each element.
  3. Saves the mapped (i.e. updated, but not mutated!) children elements into mappedChildren constant.
  4. Puts them within form DOM element.

It looks simple and it should be so.

But you have to keep in mind that React is great when your code is kept clean and transparent. When you explicitly pass props like

<Form
  schema={MyFormSchema}
>
   <Input
     name="name"
     schema={MyFormSchema} />
   <Input
     name="age"
     schema={MyFormSchema} />
</Form>

there's way less things to get broken when you accidentally change the underlying logic.

like image 26
rishat Avatar answered Nov 11 '22 15:11

rishat


Thankyou. Credits @Rishat Muhametshin

I have used the above to create a re-usable method. This works beautifully:

utils/recursivelyMapChildren.jsx

const recursivelyMapChildren = (children, addedProperties) => {

        return React.Children.map(children, child => {
            if (!React.isValidElement(child)) {
                return child;
            }

            return React.cloneElement(child, {
                ...child.props,
                ...addedProperties,
                children: this.recursivelyMapChildren(child.props.children, addedProperties)
            });
        })
 };
export default recursivelyMapChildren;

usecase:

Form.jsx

import recursivelyMapChildren from 'utils/recursivelyMapChildren';
class Form extends Component {

    handleValidation(evt, name, strValidationType){
/* pass this method down to any nested level input field */
    }

    render(){

        return (
            <form>
                {recursivelyMapChildren(this.props.children, {
                    handleValidation: this.handleValidation.bind(this)
                })}

                <input type="submit" value="submit" className="validation__submit"/>
            </form>
        )
    }
}
export default Form

SomeExample.jsx

const SomeExample = () => {
    return (
        <Form>
            <input type="hidden" name="_method" value="PUT"/>
            <fieldset>
                <legend>Personal details</legend>
                <div className="formRow">
                    <InputText/>    {/* This component will receive the method - handleValidation, so it is possible to update the state on the nested grand parent - form */}      
                </div>
                <div className="formRow">
                    <InputText/>{/* This component will receive the method - handleValidation, so it is possible to update the state on the nested grand parent - form */}  
                </div>  
            </fieldset>
        </Form>
    )
}

export default SomeExample;
like image 33
Steve Tomlin Avatar answered Nov 11 '22 14:11

Steve Tomlin