What's the right way to pass form element state to sibling/parent elements?

后端 未结 10 714
情话喂你
情话喂你 2020-11-29 15:04
  • Suppose I have a React class P, which renders two child classes, C1 and C2.
  • C1 contains an input field. I\'ll refer to this input field as Foo.
  • M
相关标签:
10条回答
  • 2020-11-29 15:27

    The concept of passing data from parent to child and vice versa is explained.

    import React, { Component } from "react";
    import ReactDOM from "react-dom";
    
    // taken refrence from https://gist.github.com/sebkouba/a5ac75153ef8d8827b98
    
    //example to show how to send value between parent and child
    
    //  props is the data which is passed to the child component from the parent component
    
    class Parent extends Component {
      constructor(props) {
        super(props);
    
        this.state = {
          fieldVal: ""
        };
      }
    
      onUpdateParent = val => {
        this.setState({
          fieldVal: val
        });
      };
    
      render() {
        return (
          // To achieve the child-parent communication, we can send a function
          // as a Prop to the child component. This function should do whatever
          // it needs to in the component e.g change the state of some property.
          //we are passing the function onUpdateParent to the child
          <div>
            <h2>Parent</h2>
            Value in Parent Component State: {this.state.fieldVal}
            <br />
            <Child onUpdate={this.onUpdateParent} />
            <br />
            <OtherChild passedVal={this.state.fieldVal} />
          </div>
        );
      }
    }
    
    class Child extends Component {
      constructor(props) {
        super(props);
    
        this.state = {
          fieldValChild: ""
        };
      }
    
      updateValues = e => {
        console.log(e.target.value);
        this.props.onUpdate(e.target.value);
        // onUpdateParent would be passed here and would result
        // into onUpdateParent(e.target.value) as it will replace this.props.onUpdate
        //with itself.
        this.setState({ fieldValChild: e.target.value });
      };
    
      render() {
        return (
          <div>
            <h4>Child</h4>
            <input
              type="text"
              placeholder="type here"
              onChange={this.updateValues}
              value={this.state.fieldVal}
            />
          </div>
        );
      }
    }
    
    class OtherChild extends Component {
      render() {
        return (
          <div>
            <h4>OtherChild</h4>
            Value in OtherChild Props: {this.props.passedVal}
            <h5>
              the child can directly get the passed value from parent by this.props{" "}
            </h5>
          </div>
        );
      }
    }
    
    ReactDOM.render(<Parent />, document.getElementById("root"));

    0 讨论(0)
  • 2020-11-29 15:34

    I'm surprised that there are no answers with a straightforward idiomatic React solution at the moment I'm writing. So here's the one (compare the size and complexity to others):

    class P extends React.Component {
        state = { foo : "" };
    
        render(){
            const { foo } = this.state;
    
            return (
                <div>
                    <C1 value={ foo } onChange={ x => this.setState({ foo : x })} />
                    <C2 value={ foo } />
                </div>
            )
        }
    }
    
    const C1 = ({ value, onChange }) => (
        <input type="text"
               value={ value }
               onChange={ e => onChange( e.target.value ) } />
    );
    
    const C2 = ({ value }) => (
        <div>Reacting on value change: { value }</div>
    );
    

    I'm setting the state of a parent element from a child element. This seems to betray the design principle of React: single-direction data flow.

    Any controlled input (idiomatic way of working with forms in React) updates the parent state in its onChange callback and still doesn't betray anything.

    Look carefully at C1 component, for instance. Do you see any significant difference in the way how C1 and built-in input component handle the state changes? You should not, because there is none. Lifting up the state and passing down value/onChange pairs is idiomatic for raw React. Not usage of refs, as some answers suggest.

    0 讨论(0)
  • 2020-11-29 15:34
    1. The right thing to do is to have the state in the parent component, to avoid ref and what not
    2. An issue is to avoid constantly updating all children when typing into a field
    3. Therefore, each child should be a Component (as in not a PureComponent) and implement shouldComponentUpdate(nextProps, nextState)
    4. This way, when typing into a form field, only that field updates

    The code below uses @bound annotations from ES.Next babel-plugin-transform-decorators-legacy of BabelJS 6 and class-properties (the annotation sets this value on member functions similar to bind):

    /*
    © 2017-present Harald Rudell <harald.rudell@gmail.com> (http://www.haraldrudell.com)
    All rights reserved.
    */
    import React, {Component} from 'react'
    import {bound} from 'class-bind'
    
    const m = 'Form'
    
    export default class Parent extends Component {
      state = {one: 'One', two: 'Two'}
    
      @bound submit(e) {
        e.preventDefault()
        const values = {...this.state}
        console.log(`${m}.submit:`, values)
      }
    
      @bound fieldUpdate({name, value}) {
        this.setState({[name]: value})
      }
    
      render() {
        console.log(`${m}.render`)
        const {state, fieldUpdate, submit} = this
        const p = {fieldUpdate}
        return (
          <form onSubmit={submit}> {/* loop removed for clarity */}
            <Child name='one' value={state.one} {...p} />
            <Child name='two' value={state.two} {...p} />
            <input type="submit" />
          </form>
        )
      }
    }
    
    class Child extends Component {
      value = this.props.value
    
      @bound update(e) {
        const {value} = e.target
        const {name, fieldUpdate} = this.props
        fieldUpdate({name, value})
      }
    
      shouldComponentUpdate(nextProps) {
        const {value} = nextProps
        const doRender = value !== this.value
        if (doRender) this.value = value
        return doRender
      }
    
      render() {
        console.log(`Child${this.props.name}.render`)
        const {value} = this.props
        const p = {value}
        return <input {...p} onChange={this.update} />
      }
    }
    
    0 讨论(0)
  • 2020-11-29 15:36

    More recent answer with an example, which uses React.useState

    Keeping the state in the parent component is the recommended way. The parent needs to have an access to it as it manages it across two children components. Moving it to the global state, like the one managed by Redux, is not recommended for same same reason why global variable is worse than local in general in software engineering.

    When the state is in the parent component, the child can mutate it if the parent gives the child value and onChange handler in props (sometimes it is called value link or state link pattern). Here is how you would do it with hooks:

    
    function Parent() {
        var [state, setState] = React.useState('initial input value');
        return <>
            <Child1 value={state} onChange={(v) => setState(v)} />
            <Child2 value={state}>
        </>
    }
    
    function Child1(props) {
        return <input
            value={props.value}
            onChange={e => props.onChange(e.target.value)}
        />
    }
    
    function Child2(props) {
        return <p>Content of the state {props.value}</p>
    }
    

    The whole parent component will re-render on input change in the child, which might be not an issue if the parent component is small / fast to re-render. The re-render performance of the parent component still can be an issue in the general case (for example large forms). This is solved problem in your case (see below).

    State link pattern and no parent re-render are easier to implement using the 3rd party library, like Hookstate - supercharged React.useState to cover variety of use cases, including your's one. (Disclaimer: I am an author of the project).

    Here is how it would look like with Hookstate. Child1 will change the input, Child2 will react to it. Parent will hold the state but will not re-render on state change, only Child1 and Child2 will.

    import { useStateLink } from '@hookstate/core';
    
    function Parent() {
        var state = useStateLink('initial input value');
        return <>
            <Child1 state={state} />
            <Child2 state={state}>
        </>
    }
    
    function Child1(props) {
        // to avoid parent re-render use local state,
        // could use `props.state` instead of `state` below instead
        var state = useStateLink(props.state)
        return <input
            value={state.get()}
            onChange={e => state.set(e.target.value)}
        />
    }
    
    function Child2(props) {
        // to avoid parent re-render use local state,
        // could use `props.state` instead of `state` below instead
        var state = useStateLink(props.state)
        return <p>Content of the state {state.get()}</p>
    }
    

    PS: there are many more examples here covering similar and more complicated scenarios, including deeply nested data, state validation, global state with setState hook, etc. There is also complete sample application online, which uses the Hookstate and the technique explained above.

    0 讨论(0)
提交回复
热议问题