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

oct*_*ref 175 reactjs

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

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

第一解决方案

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

这有效.每当Foo的值改变时,它会触发一个setStateP,所以P将输入传递给C2.

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

二解决方案:

把Foo放在P.

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

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

我该怎么办?

cap*_*ray 190

所以,如果我正确理解你,你的第一个解决方案是建议你在你的根组件中保持状态?我不能代表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)
Run Code Online (Sandbox Code Playgroud)

  • 没问题.实际上,我在写完这篇文章之后回过头来重读了一些文档,它似乎与他们的思想和最佳实践相符.React拥有真正优秀的文档,每当我最终想知道应该去哪里时,它们通常会在文档中的某处覆盖它.在这里查看有关状态的部分,http://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html#what-c​​omponents-should-have-state (4认同)
  • 上面的代码有2个错误 - 这两个错误都涉及在正确的上下文中没有绑定"this",我已经做了上面的修正,也适用于需要代码库演示的任何人:https://codepen.io/anon/pen/wJrRpZ ?编辑= 0011 (4认同)
  • @DmitryPolushkin我想发布这个跟进.http://facebook.github.io/react/tips/props-in-getInitialState-as-anti-pattern.html只要您知道自己在做什么,并且知道数据会发生变化,这是可以的,但是在许多情况下,你可能会移动东西.同样重要的是要注意,您不必将其构建为层次结构.您可以在DOM中的不同位置安装C1和C2,它们都可以监听某些数据的更改事件.我看到很多人在他们不需要分层组件的时候会推动他们. (3认同)
  • @DmitryPolushkin如果我正确理解你的问题,你想将数据从根组件传递给C2作为道具.在C2中,该数据将被设置为初始状态(即getInitialState:function(){return {someData:this.props.dataFromParentThatWillChange}}并且您将要实现componentWillReceiveProps并使用新的props调用this.setState来更新C2中的状态.自从我最初回答以来,我一直在使用Flux,并强烈建议您同时查看它.它会使您的组件更清洁,并会改变您对状态的看法. (2认同)

oct*_*ref 34

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

我建议你阅读

第一篇文章非常有助于了解如何构建React应用程序.

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

回到问题.

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

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

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

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

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

  • 我经常在React和Flux的演讲中讨论这个问题.我试图强调的一点是你上面所描述的,那就是视图状态和应用程序状态的分离.在某些情况下,事物可以从仅视图状态转换为应用程序状态,尤其是在您保存UI状态(如预设值)的情况下.我觉得一年之后你带着自己的想法回来真是太棒了.+1 (6认同)
  • @wayofthefuture 在没有看到您的代码的情况下,我可以说我看到了很多意大利面 React,就像好的 ole spaghetti jQuery 一样。我能提供的最好建议是尝试遵循 SRP。使您的组件尽可能简单;如果可以的话,愚蠢的渲染组件。我还推动抽象,如 &lt;DataProvider/&gt; 组件。它很好地完成了一件事。提供数据。这通常成为“根”组件,并通过利用子项(和克隆)作为道具(定义的合同)来传递数据。最终,尝试首先考虑服务器。有了更好的数据结构,它会让你的 JS 更简洁。 (2认同)

J. *_*ong 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);
Run Code Online (Sandbox Code Playgroud)

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

编辑带上下文的传递状态


Nes*_*ric 5

第一个解决方案,将状态保持在父组件中,是正确的。但是,对于更复杂的问题,您应该考虑一些状态管理库redux是与 react 一起使用的最流行的一个。


gap*_*ton 5

我很惊讶在我写的那一刻,没有一个简单的惯用 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>
);
Run Code Online (Sandbox Code Playgroud)

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

任何受控input(在 React 中使用表单的惯用方式)都会在其onChange回调中更新父状态,并且仍然不会泄露任何内容。

例如,仔细查看 C1 组件。您是否看到C1内置input组件和内置组件处理状态变化的方式有什么显着差异?你不应该,因为没有。提升状态并传递 value/onChange 对是原始 React 的惯用方式。正如一些答案所暗示的那样,不使用 refs。

  • 我喜欢这个解决方案。如果有人想修改它,这里有一个 [沙箱](https://codesandbox.io/embed/zxl7n356np) 适合您。 (2认同)