React - 将表单元素状态传递给兄弟/父元素的正确方法?

时间:2014-06-10 17:27:52

标签: reactjs

  • 假设我有一个React类P,它呈现两个子类C1和C2。
  • C1包含输入字段。我将此输入字段称为Foo。
  • 我的目标是让C2对Foo的变化做出反应。

我想出了两个解决方案,但他们都没有感觉到。

第一个解决方案:

  1. 分配P状态state.input
  2. 在P中创建一个onChange函数,该函数接收一个事件并设置state.input
  3. 将此onChange作为props传递给C1,然后让C1将this.props.onChange绑定到Foo的onChange
  4. 这很有效。每当Foo的值发生变化时,它会在P中触发setState,因此P将输入传递给C2。

    但出于同样的原因,它感觉不太正确:我正在从子元素设置父元素的状态。这似乎背叛了React的设计原则:单向数据流 这是我应该怎么做的,还是有更多的React-natural解决方案?

    第二个解决方案:

    将Foo放入P

    但这是我在构建我的应用时应该遵循的设计原则 - 将所有表单元素放在最高级别的render中吗?

    就像我的例子中一样,如果我有一个大的C1渲染,我真的不想把C1的整个render放到P的render只是因为C1有一个表单元素。

    我该怎么做?

10 个答案:

答案 0 :(得分:181)

所以,如果我正确理解你,你的第一个解决方案是建议你在你的根组件中保持状态?我不能代表React的创造者,但一般来说,我觉得这是一个合适的解决方案。

维持状态是React创建的原因之一(至少我认为)。如果您曾经实现过自己的状态模式客户端来处理具有大量相互依赖的移动块的动态UI,那么您会喜欢React,因为它可以减轻很多状态管理的痛苦。

通过将状态保持在层次结构中并通过事件更新它,您的数据流仍然是单向的,您只是响应Root组件中的事件,您实际上并没有通过两个事件获取数据方式绑定,你告诉Root组件“嘿,发生在这里的事情,检查值”或者你正在传递子组件中的一些数据的状态以更新状态。您在C1中更改了状态,并且您希望C2知道它,因此,通过更新Root组件中的状态并重新渲染,C2的props现在处于同步状态,因为状态在Root组件中更新并传递

class Example extends React.Component {
  constructor (props) {
    super(props)
    this.state = { data: 'test' }
  }
  render () {
    return (
      <div>
        <C1 onUpdate={this.onUpdate.bind(this)}/>
        <C2 data={this.state.data}/>
      </div>
    )
  }
  onUpdate (data) { this.setState({ data }) }
}

class C1 extends React.Component {
    render () {
      return (
        <div>
          <input type='text' ref='myInput'/>
          <input type='button' onClick={this.update.bind(this)} value='Update C2'/>
        </div>
      )
    }
    update () {
      this.props.onUpdate(this.refs.myInput.getDOMNode().value)
    }
})

class C2 extends React.Component {
    render () {
      return <div>{this.props.data}</div>
    }
})

ReactDOM.renderComponent(<Example/>, document.body)

答案 1 :(得分:33)

现在使用React构建应用程序,我想与半年前我提出的这个问题分享一些想法。

我建议你阅读

第一篇文章对于了解如何构建React应用非常有帮助。

Flux回答问题为什么你应该以这种方式构建你的React应用程序(而不是如何来构建它)。 React只占系统的50%,通过Flux,您可以看到整个画面,看看它们是如何构成一个连贯的系统。

回到问题。

至于我的第一个解决方案,让处理程序反向运行是完全可以的,因为数据仍然是单向的。

但是,根据您的情况,是否允许处理程序在P中触发setState可能是对还是错。

如果应用程序是一个简单的Markdown转换器,C1是原始输入,C2是HTML输出,可以让C1在P中触发setState,但有些人可能认为这不是推荐的方法。< / p>

但是,如果应用程序是待办事项列表,C1是用于创建新待办事项的输入,C2是HTML中的待办事项列表,您可能希望处理程序比P更高两级到{{1} },让dispatcher更新store,然后将数据发送到P并填充视图。看到Flux文章。以下是一个示例:Flux - TodoMVC

通常,我更喜欢todo list示例中描述的方式。您在应用中的状态越少越好。

答案 2 :(得分:4)

使用keeping the state in parent component的第一个解决方案是正确的。但是,对于更复杂的问题,您应该考虑一些状态管理库redux是最常用的反应。

答案 3 :(得分:1)

您应该学习Redux和ReactRedux库。它将在一个商店中构建您的状态和道具,您可以稍后在组件中访问它们。

答案 4 :(得分:1)

我很惊讶,在我撰写本文时,没有一个简单的惯用React解决方案的答案。所以这是一个(将大小和复杂度与其他进行比较):

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>
);
  

我正在从子元素中设置父元素的状态。这似乎违反了React的设计原则:单向数据流。

任何controlled input(React中使用表单的惯用方式)都会在其onChange回调中更新父状态,但仍然不会出卖任何东西。

例如,仔细查看C1组件。在C1和内置input组件处理状态更改的方式上,您是否看到任何显着差异?您不应该,因为没有。 提升状态并传递值/ onChange对对原始React来说是惯用的。并非像某些答案所暗示的那样使用refs。

答案 5 :(得分:1)

使用示例的最新答案,该示例使用React.useState

建议在父组件中保持状态。父级需要访问它,因为它需要在两个子级组件之间进行管理。出于相同的原因,通常在软件工程中,全局变量比局部变量要弱于本地,因此不建议将其移至全局状态,例如由Redux管理的状态。 >

当状态位于父级组件中时,如果父级在道具中给子级valueonChange处理程序,则子级可以对其进行更改(有时称为值链接状态链接模式)。这是使用钩子的方法:


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>
}

整个子级组件将在子级中的输入更改时重新渲染,如果父级组件较小/快速重新渲染,则这可能不是问题。在一般情况下(例如大型表单),父组件的重新渲染性能仍然可能是个问题。根据您的情况,这已经解决了问题(见下文)。

使用第三方库更容易实现

状态链接模式没有父级重新渲染,例如Hookstate-增强React.useState涵盖各种用例,包括您自己的用例。 (免责声明:我是该项目的作者)。

这里是Hookstate的样子。 Child1将更改输入,Child2将对其作出反应。 Parent将保留状态,但不会在状态更改时重新呈现,只有Child1Child2会保留。

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:有许多more examples here涵盖了相似和更复杂的场景,包括深度嵌套的数据,状态验证,带有setState钩子的全局状态等。还有complete sample application online,其中使用了Hookstate和上面说明的技术。

答案 6 :(得分:0)

  1. 正确的做法是在父组件中设置状态,以避免引用
  2. 问题是在输入字段时避免不断更新所有孩子
  3. 因此,每个子项应该是一个Component(不是PureComponent)并实现shouldComponentUpdate(nextProps, nextState)
  4. 这样,在表单字段中键入时,只有该字段更新
  5. 以下代码使用来自 BabelJS 6 ES.Next @bound的{​​{1}}注释和类属性(注释设置此值成员函数类似于bind):

    babel-plugin-transform-decorators-legacy

答案 7 :(得分:0)

使用React> = 16.3,您可以使用ref和forwardRef来从其父级访问子级DOM。不再使用旧的引用方式。
这是使用您的案例的示例:

import React, { Component } from 'react';

export default class P extends React.Component {
   constructor (props) {
      super(props)
      this.state = {data: 'test' }
      this.onUpdate = this.onUpdate.bind(this)
      this.ref = React.createRef();
   }

   onUpdate(data) {
      this.setState({data : this.ref.current.value}) 
   }

   render () {
      return (
        <div>
           <C1 ref={this.ref} onUpdate={this.onUpdate}/>
           <C2 data={this.state.data}/>
        </div>
      )
   }
}

const C1 = React.forwardRef((props, ref) => (
    <div>
        <input type='text' ref={ref} onChange={props.onUpdate} />
    </div>
));

class C2 extends React.Component {
    render () {
       return <div>C2 reacts : {this.props.data}</div>
    }
}

有关参考和forwardRef的详细信息,请参见RefsForwardRef

答案 8 :(得分:0)

五年后,随着React Hooks的引入,现在有了使用useContext挂钩的更优雅的方式。

您可以在全局范围内定义上下文,在父组件中导出变量,对象和函数,然后在提供的上下文中将子组件包装在App中,并在子组件中导入所需的内容。以下是概念证明。

import React, { useState, useContext } from "react";
import ReactDOM from "react-dom";
import styles from "./styles.css";

// Create context container in a global scope so it can be visible by every component
const ContextContainer = React.createContext(null);

const initialAppState = {
  selected: "Nothing"
};

function App() {
  // The app has a state variable and update handler
  const [appState, updateAppState] = useState(initialAppState);

  return (
    <div>
      <h1>Passing state between components</h1>

      {/* 
          This is a context provider. We wrap in it any children that might want to access
          App's variables.
          In 'value' you can pass as many objects, functions as you want. 
           We wanna share appState and its handler with child components,           
       */}
      <ContextContainer.Provider value={{ appState, updateAppState }}>
        {/* Here we load some child components */}
        <Book title="GoT" price="10" />
        <DebugNotice />
      </ContextContainer.Provider>
    </div>
  );
}

// Child component Book
function Book(props) {
  // Inside the child component you can import whatever the context provider allows.
  // Earlier we passed value={{ appState, updateAppState }}
  // In this child we need the appState and the update handler
  const { appState, updateAppState } = useContext(ContextContainer);

  function handleCommentChange(e) {
    //Here on button click we call updateAppState as we would normally do in the App
    // It adds/updates comment property with input value to the appState
    updateAppState({ ...appState, comment: e.target.value });
  }

  return (
    <div className="book">
      <h2>{props.title}</h2>
      <p>${props.price}</p>
      <input
        type="text"
        //Controlled Component. Value is reverse vound the value of the variable in state
        value={appState.comment}
        onChange={handleCommentChange}
      />
      <br />
      <button
        type="button"
        // Here on button click we call updateAppState as we would normally do in the app
        onClick={() => updateAppState({ ...appState, selected: props.title })}
      >
        Select This Book
      </button>
    </div>
  );
}

// Just another child component
function DebugNotice() {
  // Inside the child component you can import whatever the context provider allows.
  // Earlier we passed value={{ appState, updateAppState }}
  // but in this child we only need the appState to display its value
  const { appState } = useContext(ContextContainer);

  /* Here we pretty print the current state of the appState  */
  return (
    <div className="state">
      <h2>appState</h2>
      <pre>{JSON.stringify(appState, null, 2)}</pre>
    </div>
  );
}

const rootElement = document.body;
ReactDOM.render(<App />, rootElement);

您可以在代码沙箱编辑器中运行此示例。

Edit passing-state-with-context

答案 9 :(得分:-1)

解释了将数据从父级传递到子级,反之亦然的概念。

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"));