将表单元素状态传递给同级/父元素的正确方法是什么?


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

我已经提出了两种解决方案,但是它们都不对。

第一个解决方案:

  1. 为P分配一个状态,state.input
  2. onChange在P中创建一个函数,该函数接受一个事件并设置state.input
  3. onChange将其作为传递给C1 props,然后让C1绑定this.props.onChangeonChangeFoo。

这可行。每当Foo的值更改时,它都会触发setStateP中的a,因此P会将输入传递给C2。

但是由于相同的原因,感觉不太正确:我正在从子元素中设置父元素的状态。这似乎背叛了React的设计原理:单向数据流。
这是我应该怎么做,还是有一个更自然的React解决方案?

第二种解决方案:

只需将Foo放在P中即可。

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

就像在我的示例中一样,如果我渲染了很大的C1,我真的不想将整个renderC1render仅仅因为C1具有表单元素 P中。

我该怎么办?


我将做完全相同的事情,尽管它工作正常,但我感觉这只是一个巨大的漏洞。
Mirko

Answers:


198

因此,如果我对您的理解正确,那么您的第一个解决方案是建议您将状态保留在根组件中?我不能代表React的创建者,但是总的来说,我发现这是一个合适的解决方案。

保持状态是创建React的原因之一(至少在我看来)。如果您曾经实现过自己的状态模式客户端来处理具有很多相互依赖的动态部件的动态UI,那么您会喜欢React的,因为它减轻了很多这种状态管理的麻烦。

通过在层次结构中进一步保持状态,并通过事件对其进行更新,您的数据流仍然几乎是单向的,您只是在响应Root组件中的事件,实际上并没有通过两种方式绑定那里的数据,您要告诉Root组件“嘿,这里发生了什么事,请检查值”,或者您要向上传递子组件中某些数据的状态以更新状态。您在C1中更改了状态,并希望C2知道它,因此,通过更新Root组件中的状态并重新渲染,由于C2的道具在Root组件中进行了更新并传递了,因此C2的道具现在处于同步状态。

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)

5
没问题。实际上,写完这篇文章后我回过头来重新阅读了一些文档,这似乎与他们的想法和最佳实践保持一致。React拥有非常出色的文档,每当我最终想知道应该去哪里时,他们通常都会在文档的某个地方覆盖它。在此处查看状态部分,facebook.github.io
react / docs /…

@captray但是如果C2getInitialState用于数据的数据又在render其内部使用this.state.data呢?
Dmitry Polushkin 2014年

2
@DmitryPolushkin如果我正确理解了您的问题,则希望将根组件中的数据作为道具传递给C2。在C2中,该数据将设置为初始状态(即getInitialState:function(){return {someData:this.props.dataFromParentThatWillChange}},您将要实现componentWillReceiveProps并使用新的props调用this.setState来更新。状态C2自从我开始回答,我一直在使用助焊剂,并强烈建议你看看它,以及它使你的组件清洁,会改变你对国家的方式。
captray

3
@DmitryPolushkin我想将其发布到后续活动中。facebook.github.io/react/tips/…没关系,只要您知道自己在做什么,并且知道数据将会发生变化,但是在许多情况下,您可能可以移动一些东西。同样重要的是要注意,您不必将其构建为层次结构。您可以将C1和C2安装在DOM中的不同位置,并且它们都可以侦听某些数据的更改事件。我看到很多人在不需要分层组件时都在追求它们。
captray 2014年

5
上面的代码有2个错误-这两个错误都涉及在正确的上下文中未绑定“ this”,我已经进行了更正,也适用于需要codepen演示的任何人:codepen.io/anon/pen/wJrRpZ?editors=0011
Alex H

34

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

我建议你阅读

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

Flux回答了一个问题,为什么您应该以这种方式构建React应用程序(而不是如何构建它)。React仅占系统的50%,使用Flux,您可以看到整个图片,并了解它们如何构成一个连贯的系统。

回到问题。

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

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

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

但是,如果应用程序是待办事项列表,C1是创建新待办事项的输入,C2是HTML中的待办事项列表,则您可能希望将处理程序比P-上移两个级别dispatcher,从而storedata store,然后将数据发送到P并填充视图。看到该助焊剂文章。这是一个示例:Flux-TodoMVC

通常,我更喜欢待办事项列表示例中描述的方法。应用程序中的状态越少越好。


7
我在React和Flux的演示中经常讨论这一点。我上面要强调的是您在上面描述的内容,那就是视图状态和应用程序状态的分离。在某些情况下,事情可能会从仅视图状态过渡到应用程序状态,尤其是在保存UI状态(例如预设值)的情况下。我认为,一年后您回到自己的想法真是太棒了。+1
captray

@captray那么,我们可以说redux在所有情况下都比做出反应强大吗?尽管他们的学习曲线很...(来自Java)
Bruce

我不确定你是什么意思。他们很好地处理了两种不同的事情,并且适当地分离了关注点。
captray

1
我们有一个使用redux的项目,几个月后,似乎所有操作都使用了动作。这就像是意大利面条代码的大量混合,是无法理解的,完全是灾难。国家管理可能不错,但可能会被严重误解。是否可以安全地假设flux / redux仅用于需要全局访问的状态部分?
wayofthefuture

1
@wayofthefuture如果没有看到您的代码,我可以说我看到了很多意大利面React,就像好的ole意大利面jQuery。我可以提供的最佳建议是尝试并遵循SRP。使组件尽可能简单;如果可以的话,使用哑巴渲染组件。我还要求像<DataProvider />组件这样的抽象。它做得很好。提供数据。它通常成为“根”组件,并通过利用子级(和克隆级)作为道具(定义的合同)向下传递数据。最终,尝试首先考虑服务器。通过更好的数据结构,可以使您的JS更加整洁。
captray


5

五年后,随着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);

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

编辑上下文传递状态


2

我很惊讶,在我撰写本文时,没有一个简单的惯用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的设计原理:单向数据流。

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

例如,仔细查看C1组件。您是否看到C1内置input组件处理状态更改的方式有何重大不同?您不应该,因为没有。对于原始React来说,抬高状态并传递值/ onChange对是惯用的。如某些答案所示,不使用裁判。


1
您正在使用什么版本的react?我遇到实验类属性问题,而foo没有定义。
艾萨克·朴

那不是反应的版本。组件的代码错误。修复它,现在尝试。
盖珀顿

显然,您必须从中提取状态成员this.state,渲染中缺少返回值,并且必须将多个组件包装在div或其他内容中。不知道我写原始答案时是怎么想的。一定是编辑错误。
盖珀顿

1
我喜欢这个解决方案。如果有人想修补一下,这里有个沙箱供您选择。
艾萨克·帕克

2

使用示例的最新答案,该示例使用 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>
}

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

链路状态模式无父重新渲染更容易使用第三方库,如实行钩键状态 -增压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:这里有更多示例,涵盖了相似和更复杂的场景,包括深度嵌套的数据,状态验证,带有setState钩子的全局状态等。还有在线的完整示例应用程序,它使用了Hookstate和上述技术。



1

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

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


0
  1. 正确的做法是将状态包含在父组件中,以避免ref和其他原因
  2. 一个问题是避免在输入字段时不断更新所有子级
  3. 因此,每个子代都应该是一个Component(例如不是PureComponent)并实现 shouldComponentUpdate(nextProps, nextState)
  4. 这样,在输入表单字段时,只有该字段会更新

下面使用代码@bound从注释ES.Next babel-plugin-transform-decorators-legacyBabelJS 6和类属性(注释设置上类似于结合成员函数此值):

/*
© 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

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

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


1
我建议您通知以前的答案所有者以评论的方式,在您的答案中添加您提供的描述。其次是为了礼貌而改名。
托马斯·伊索

@ThomasEaso我在这里找不到他,我检查了一下。还有其他建议

单击其图标左侧的“编辑”按钮,然后修改其答案并提交。它将交给主持人,他们将为您提供宝贵的意见。
托马斯·伊索
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.