简体   繁体   中英

React: updating parent state from nested children component

I'm working in a form with React. My idea is to create a reusable Form component that gets the state from a Page component as props, and will hold the logic for updating its own state with children data, send it to parent Page component.

The Page component is this:

class Page extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data: {
        text1: "Initial text1",
        text2: "Initial text2"
      }
    };
  }

  render() {
    return (
      <div className="Page">
        <div className="DataPreview">
          Data preview in Page component
          <div>{this.state.data.text1}</div>
          <div>{this.state.data.text2}</div>
        </div>
        <Form data={this.state.data}>
          <Input id="text1" data={this.state.data.text1} />
          <Input id="text2" data={this.state.data.text2} />
        </Form>
      </div>
    );
  }
}

This is the Form component:

class Form extends Component {
  constructor(props) {
    super(props);
    this.state = this.props.data;
  }

  render() {
    return (
      <div className="Parent">
        <div>Form component</div>
        <div className="DataPreview">
          Data preview in Form component
          <div>{this.state.text1}</div>
          <div>{this.state.text2}</div>
        </div>
        {this.props.children}
      </div>
    );
  }
}

And this the Input component:

class Input extends Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <div className="Child" id={this.props.id}>
        <div>Input component</div>
        <input id={this.props.id} type="text" value={this.props.data} />
      </div>
    );
  }
}

So Input should update Form state, and Form should update Page state. I know how to do it passing a callback when the Input is written Inside Form component, but I cant figure out how to do it when it is written inside Page component, like in this case.

I have a Sandbox for those interested: https://codesandbox.io/s/qx6kqypo09

class Input extends Component {


constructor(props) {
    super(props);
  }

  handleChange(e) {
    let data = this.props.this.state.data;
    data.text1 = e.target.value;
    this.props.this.setState({ data: data });
  }

  render() {
    return (
      <div className="Child" id={this.props.id}>
        <div>Input component {this.props.id}</div>
        <input
          id={this.props.id}
          type="text"
          value={this.props.data}
          onChange={e => this.handleChange(e)}
        />
      </div>
    );
  }
}

use your input component as specified and your page component as mentioned below-

class Page extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data: {
        text1: "Initial text1",
        text2: "Initial text2"
      }
    };
  }

  render() {
    return (
      <div className="Page">
        <div className="DataPreview">
          Data preview in Page component
          <div>{this.state.data.text1}</div>
          <div>{this.state.data.text2}</div>
        </div>
        <Form data={this.state.data}>
          <Input id="text1" this={this} data={this.state.data.text1} />
          <Input id="text2" data={this.state.data.text2} />
        </Form>
      </div>
    );
  }
}

I think this will help you Thanks

As @dashton said, I am holding the same state in different components, and that's not correct. I will look for a different approach instead using only Form component state, and sharing logic via composition. I will open a new question for this.

without using some kind of state management, you would need to create a method that handles the state change in the parent component that you would then pass down to your child component aa prop.

Once you call that method in the child component it will update the state of the parent component.

This is one way of doing what you want to achieve: passing a callback handler for onChange . But, when your app starts to get bigger things can be ugly :) If you are thinking about creating a complex reusable Form component maybe you can examine the present node packages.

An alternative to this method, if you need a simple one, you can study React Context a little bit. It can help you maybe. Other than that Redux or other global state management libraries can do this also.

 class Page extends React.Component { state = { data: { text1: "Initial text1", text2: "Initial text2", }, }; handleChange = ( e ) => { const { name, value } = e.target; this.setState( prevState => ( { data: { ...prevState.data, [ name ]: value }, } ) ); } render() { return ( <div className="Page"> <div className="DataPreview"> Data preview in Page component <div>{this.state.data.text1}</div> <div>{this.state.data.text2}</div> </div> <Form data={this.state.data}> <Input name="text1" data={this.state.data.text1} onChange={this.handleChange} /> <Input name="text2" data={this.state.data.text2} onChange={this.handleChange} /> </Form> </div> ); } } const Form = props => ( <div className="Parent"> <div>Form component</div> <div className="DataPreview"> Data preview in Form component <div>{props.data.text1}</div> <div>{props.data.text2}</div> </div> {props.children} </div> ); const Input = props => ( <div className="Child" id={props.id}> <div>Input component {props.id}</div> <input name={props.name} type="text" value={props.data} onChange={props.onChange} /> </div> ); const rootElement = document.getElementById("root"); ReactDOM.render(<Page />, rootElement); 
 .Page { border: 10px solid blue; } .Parent { border: 10px solid turquoise; } .Child { border: 3px solid tomato; } .DataPreview { border: 3px solid lightgray; } 
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script> <div id="root"></div> 

As other people have said, you are holding the same state in different components, which obviously isn't correct.

However, to answer your requirement regarding decoupling child components from the form, you could make your form handle state changes from the inputs by using a render prop which would pass a callback to the inputs, see code and link.

https://codesandbox.io/s/4zyvjm0q64

import React, { Component } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

class Input extends Component {
    constructor(props) {
        super(props);
    }
    handleChange(id, value) {
        this.props.onChange(id, value);
    }
    render() {
        return (
            <div className="Child" id={this.props.id}>
                <div>Input component {this.props.id}</div>
                <input
                    id={this.props.id}
                    type="text"
                    value={this.props.data}
                    onChange={e => this.handleChange(e)}
                />
            </div>
        );
    }
}

class Form extends Component {
    constructor(props) {
        super(props);
        this.state = this.props.data;
    }

    handleChange = (id, value) => {
        this.setState({ [id]: value });
    };
    render() {
        return (
            <div className="Parent">
                <div>Form component</div>
                <div className="DataPreview">
                    Data preview in Form component
                    <div>{this.state.text1}</div>
                    <div>{this.state.text2}</div>
                </div>
                {this.props.render(this.handleChange)}
            </div>
        );
    }
}

class Page extends Component {
    constructor(props) {
        super(props);
        this.state = {
            data: {
                text1: "Initial text1",
                text2: "Initial text2"
            }
        };
    }

    render() {
        return (
            <div className="Page">
                <div className="DataPreview">
                    Data preview in Page component
                    <div>{this.state.data.text1}</div>
                    <div>{this.state.data.text2}</div>
                </div>
                <Form
                    data={this.state.data}
                    render={(handler) => {
                        return (
                            <div>
                                <Input id="text1" onChange={e => handler("text1", e.target.value)} />
                                <Input id="text2" onChange={e => handler("text2", e.target.value)} />
                            </div>
                        );
                    }}
                />
            </div>
        );
    }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Page />, rootElement);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM