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

Posted

技术标签:

【中文标题】将表单元素状态传递给兄弟/父元素的正确方法是啥?【英文标题】:What's the right way to pass form element state to sibling/parent elements?将表单元素状态传递给兄弟/父元素的正确方法是什么? 【发布时间】:2014-07-31 14:09:56 【问题描述】: 假设我有一个 React 类 P,它呈现两个子类 C1 和 C2。 C1 包含一个输入字段。我将此输入字段称为 Foo。 我的目标是让 C2 对 Foo 的变化做出反应。

我想出了两个解决方案,但都感觉不太对。

第一个解决方案:

    为 P 分配一个状态,state.input。 在 P 中创建一个onChange 函数,该函数接受一个事件并设置state.input。 将此onChange 作为props 传递给C1,并让C1 将this.props.onChange 绑定到Foo 的onChange

这行得通。每当 Foo 的值发生变化时,它都会在 P 中触发 setState,因此 P 将有输入传递给 C2。

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

第二种解决方案:

只需将 Foo 放在 P 中即可。

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

就像在我的示例中一样,如果我有 C1 的大型渲染,我真的不想仅仅因为 C1 有一个表单元素,就将 C1 的整个 render 放入 P 的 render

我该怎么做?

【问题讨论】:

我即将做同样的事情,尽管它工作正常,但我觉得它只是一个巨大的 hack 【参考方案1】:

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

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

通过在层次结构中进一步保持状态并通过事件更新它,您的数据流仍然几乎是单向的,您只是响应 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)

【讨论】:

没问题。实际上,我写完这篇文章后又回去重读了一些文档,这似乎符合他们的想法和最佳实践。 React 有真正优秀的文档,每次我最终想知道应该去哪里时,他们通常都会在文档的某个地方进行介绍。在此处查看有关状态的部分,facebook.github.io/react/docs/… @captray 但是,如果C2 有一个getInitialState 用于数据并且在render 内部它使用this.state.data,那又如何呢? @DmitryPolushkin 如果我正确理解了您的问题,您希望将数据从根组件传递到 C2 作为道具。在 C2 中,该数据将被设置为初始状态(即 getInitialState: function() return someData: this.props.dataFromParentThatWillChange 并且您需要实现 componentWillReceiveProps 并使用新的 props 调用 this.setState 来更新C2 中的状态。自从我最初回答以来,我一直在使用 Flux,并且强烈建议您也查看它。它使您的组件更清洁,并会改变您对状态的看法。 @DmitryPolushkin 我想将其发布以进行跟进。 facebook.github.io/react/tips/… 只要您知道自己在做什么并且知道数据会发生变化,就可以了,但是在许多情况下,您可能可以四处移动。同样重要的是要注意,您不必将其构建为层次结构。您可以将 C1 和 C2 挂载在 DOM 中的不同位置,它们都可以侦听某些数据的更改事件。我看到很多人在不需要分层组件时推动他们。 上面的代码有 2 个错误 - 这两个错误都涉及在正确的上下文中没有绑定“this”,我已经在上面进行了更正,也适用于需要 codepen 演示的任何人:codepen.io/anon/pen/wJrRpZ?editors=0011跨度> 【参考方案2】:

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

我建议你阅读

Thinking in React Flux

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

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

回到问题。

至于我的第一个解决方案,完全可以让 handler 反向,因为 data 仍然是单向。

但是,让处理程序在 P 中触发 setState 是对还是错,这取决于您的情况。

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

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

一般来说,我更喜欢待办事项列表示例中描述的方式。您在应用中的状态越少越好。

【讨论】:

我经常在 React 和 Flux 的演示文稿中讨论这个问题。我试图强调的一点就是你上面描述的,那就是视图状态和应用程序状态的分离。在某些情况下,事情可能会从 View State 转变为 Application State,尤其是在您保存 UI 状态(如预设值)的情况下。我觉得一年后你带着自己的想法回来真是太棒了。 +1 @captray 那么我们可以说 redux 在所有情况下都比 react 强大吗?尽管他们的学习曲线...(来自 Java) 我不确定你的意思。它们很好地处理了两种不同的事情,并且适当地分离了关注点。 我们有一个使用 redux 的项目,经过几个月的时间,似乎所有事情都使用了动作。这就像意大利面条代码和无法理解的大混合,完全是灾难。状态管理可能很好,但可能被严重误解。假设 Flux/redux 只应用于需要全局访问的部分状态是否安全? @wayofthefuture 没有看到你的代码,我可以说我看到了很多意大利面条 React 就像好的 ole spaghetti jQuery。我能提供的最好建议是尝试遵循 SRP。让你的组件尽可能简单;如果可以的话,哑渲染组件。我也推动像 组件这样的抽象。它做好一件事。提供数据。这通常成为“根”组件,并通过利用子(和克隆)作为道具(定义的合同)来向下传递数据。最终,尝试首先考虑服务器。它会让你的 JS 更干净,数据结构更好。【参考方案3】:

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

【讨论】:

【参考方案4】:
    正确的做法是在父组件中拥有状态,避免引用和不引用 一个问题是避免在输入字段时不断更新所有子项 因此,每个孩子都应该是一个组件(而不是 PureComponent)并实现shouldComponentUpdate(nextProps, nextState) 这样,在表单域中输入时,只有该域更新

下面的代码使用了来自 BabelJS 6ES.Next babel-plugin-transform-decorators-legacy 和 class-properties 的 @bound 注解(注解在成员函数上设置了这个值,类似于绑定):

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

【讨论】:

【参考方案5】:

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

【讨论】:

同意。当大多数人用“纯 React”写东西时,我的回复被写了回来。在通量爆炸之前。【参考方案6】:

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

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

【讨论】:

我建议您通知之前的答案所有者作为评论,以在他的答案中添加您提供的描述。然后为了礼貌而提供你的名字。 @ThomasEaso 我在这里找不到他,我查了一下。有没有其他建议 点击他图标左侧的“编辑”按钮,修改他的答案并提交。它将交给版主,他们会为您宝贵的 cmets 做必要的事情。【参考方案7】:

我很惊讶在我写作的那一刻,没有一个简单的惯用 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 组件处理状态变化的方式有什么显着差异吗?你不应该,因为没有。 提升状态并传递 value/onChange 对对于原始 React 来说是惯用的。不使用 refs,正如一些答案所暗示的那样。

【讨论】:

你用的是什么版本的 react?我遇到了实验类属性问题,并且 foo 没有定义。 这与反应版本无关。组件的代码错误。已修复,立即尝试。 显然,你必须从this.state中提取状态成员,渲染中缺少返回,并且必须将多个组件包装在div中。不知道我在写原始答案时是怎么错过的。一定是编辑错误。 我喜欢这个解决方案。如果有人想修改它,这里有一个 sandbox 给你。【参考方案8】:

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

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 的详细信息,请参阅 Refs 和 ForwardRef。

【讨论】:

【参考方案9】:

五年后,随着 React Hooks 的引入,现在使用 useContext hook 可以更优雅地实现这一点。

您在全局范围内定义上下文,在父组件中导出变量、对象和函数,然后将子组件包装在应用程序中提供的上下文中,并在子组件中导入您需要的任何内容。以下是概念验证。

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

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

【讨论】:

我们如何与子组件共享const ContextContainer = React.createContext(null);,以便创建单独的文件?【参考方案10】:

最近的答案有一个例子,它使用React.useState

将状态保留在父组件中是推荐的方式。父级需要访问它,因为它跨两个子组件管理它。 建议将其移动到全局状态,就像 Redux 管理的状态一样,原因与软件工程中全局变量比本地变量差的原因相同。 p>

当状态在父组件中时,如果父在props中给子valueonChangehandler(有时称为值链接状态链接模式)。以下是使用钩子的方法:


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>

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

状态链接模式没有父级重新渲染使用 3rd 方库更容易实现,例如 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和技术上面已经解释过了。

【讨论】:

以上是关于将表单元素状态传递给兄弟/父元素的正确方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

React:将状态从孩子传递到孩子再传递给父母

局部视图表单上的 HiddenFor 不包含任何元素

在 C# Visual Studio 中更改表单元素名称的正确方法是啥?

如何将html5画布上的点击事件传递给兄弟画布元素上的事件监听器

在 JavaScript 中创建类并将其传递给 MVC 控制器的正确方法是啥

js如何给目标元素的兄弟元素更改样式