反应-正确的方式传递表单元素的状态兄弟/父元素?

  • 假设我有一个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

这个作品。每当Foo的值发生变化时,它都会触发P中的setState,因此P将有要传递给C2的输入。

但是由于同样的原因,感觉不太对:我正在从子元素设置父元素的状态。这似乎违背了React的设计原则:单向数据流。< br / > 这是我应该怎么做,还是有一个更反应自然的解决方案?< / >强

第二个解决方案:

把Foo放到P中。

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

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

我该怎么做呢?

180923 次浏览

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

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

通过在层次结构中保持状态,并通过事件更新,你的数据流仍然是单向的,你只是响应根组件中的事件,你并没有真正通过双向绑定获得数据,你告诉根组件“嘿,这里发生了一些事情,检查值”或者你在子组件中传递一些数据的状态以更新状态。您更改了C1中的状态,并且希望C2知道它,因此,通过更新Root组件中的状态并重新呈现,C2的道具现在是同步的,因为状态是在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构建了一个应用程序,我想分享一些关于半年前我问过的问题的想法。

我建议你读一读

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

Flux回答了为什么的问题,你应该这样构造你的React应用程序(而不是如何来构造它)。React只占整个系统的50%,而Flux可以让你看到整个系统,看到它们如何构成一个连贯的系统。

回到刚才的问题。

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

但是,让处理程序触发P中的setState是否正确取决于您的情况。

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

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

一般来说,我更喜欢todo列表示例中描述的方式。应用程序的状态越少越好。

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

  1. 正确的做法是使用父组件中的状态,以避免ref等
  2. 一个问题是在输入字段时避免不断更新所有的孩子
  3. 因此,每个子元素都应该是一个Component(不是PureComponent),并实现shouldComponentUpdate(nextProps, nextState)
  4. 这样,当输入表单字段时,只有该字段更新

下面的代码使用了来自ES。下一个babel-plugin-transform-decorators-legacy@bound注释和类属性(注释在成员函数上设置此值类似于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} />
}
}

第一个解决方案是将状态保存在父组件中,即正确的答案。然而,对于更复杂的问题,你应该考虑一些状态管理库回来的是react最常用的一个。

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

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

我很惊讶,在我写作的时候,没有一个简单的惯用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组件处理状态变化的方式有什么显著的不同吗?你不应该,因为根本就没有。提升状态并传递value/onChange对是原始React的惯用方法。没有使用refs,一些答案建议。

使用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的详细信息,请参见参考文献ForwardRef

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

你在全局范围内定义context,在父组件中导出变量、对象和函数,然后在应用中提供的上下文中包装子组件,并在子组件中导入任何你需要的东西。下面是概念的证明。

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

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

< a href = " https://codesandbox.io/s/green-dew-ymmbi?fontsize=14" rel="noreferrer">Edit passing-state-with-context .

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

将状态保存在父组件中是推荐的方法。父组件需要访问它,因为它跨两个子组件管理它。将它移动到全局状态,就像由Redux管理的状态一样,建议使用,原因与软件工程中一般使用全局变量不如局部变量的原因相同。

当状态在父组件中,如果父组件在props中给子组件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以覆盖各种用例,包括你的用例。(声明:我是该项目的作者之一)。

这是胡克州的情况。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和上面解释的技术。