假设我有一个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的设计原则:单向数据流。
这是我应该怎么做,还是有一个更反应自然的解决方案?
第二个解决方案:
把Foo放到P中。
但是,当我构造我的应用程序——把所有表单元素放在最高级别类的渲染中——时,这是我应该遵循的设计原则吗?
就像在我的例子中,如果我有一个C1的大渲染,我真的不想仅仅因为C1有一个表单元素就把C1的整个渲染放到P的渲染中。
我该怎么做呢?
那么,如果我理解正确的话,您的第一个解决方案是建议您将状态保存在根组件中?我不能代表React的创造者说话,但总的来说,我觉得这是一个合适的解决方案。
维护状态是React被创建的原因之一(至少我认为)。如果你曾经实现过自己的状态模式客户端来处理有很多相互依赖的移动部件的动态UI,那么你会喜欢React,因为它减轻了很多状态管理的痛苦。
By keeping state further up in the hierarchy, and updating it through eventing, your data flow is still pretty much unidirectional, you're just responding to events in the Root component, you're not really getting the data there via two way binding, you're telling the Root component that "hey, something happened down here, check out the values" or you're passing the state of some data in the child component up in order to update the state. You changed the state in C1, and you want C2 to be aware of it, so, by updating the state in the Root component and re-rendering, C2's props are now in sync since the state was updated in the Root component and passed along.
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中的思考
通量
第一篇文章对于理解如何构建React应用非常有帮助。
Flux回答了为什么要这样构造React应用程序(而不是如何构造它)的问题。React只占整个系统的50%,而Flux可以让你看到整个系统,看到它们如何构成一个连贯的系统。
回到刚才的问题。
至于我的第一个解决方案,完全可以让处理程序走相反的方向,因为数据仍然是单向的。
但是,让处理程序触发P中的setState是否正确取决于您的情况。
如果应用程序是一个简单的Markdown转换器,C1是原始输入,C2是HTML输出,那么让C1触发P中的setState是可以的,但有些人可能会认为这不是推荐的方法。
然而,如果应用程序是一个待办事项列表,C1是创建新待办事项的输入,C2是HTML中的待办事项列表,你可能想要处理程序比P高两级——到调度程序,它让存储更新数据存储,然后将数据发送给P并填充视图。请参阅Flux的文章。这里有一个例子:Flux - TodoMVC
一般来说,我更喜欢todo列表示例中描述的方式。应用程序的状态越少越好。
正确的做法是把状态放在父组件中,避免ref之类的
一个问题是在输入字段时避免不断更新所有子字段
因此,每个子组件都应该是一个组件(而不是PureComponent),并实现shouldComponentUpdate(nextProps, nextState)
这样,当输入表单字段时,只有该字段更新
下面的代码使用来自ES的@bound注释。下一个babel-plugin-transform-decorators- BabelJS 6的遗产和class-properties(注释在成员函数上设置这个值,类似于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} />
}
}
解释了将数据从父节点传递到子节点的概念,反之亦然。
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的设计原则:单向数据流。
任何受控输入(React中使用表单的惯用方式)在onChange回调中更新父状态,仍然不会泄露任何信息。
例如,仔细看看C1组件。在C1和内置输入组件处理状态变化的方式上,您看到有什么显著的区别吗?你不应该,因为根本就没有。提升状态并传递value/onChange对是原始React的惯用方法。而不是像一些答案所暗示的那样使用裁判。
使用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的详细信息,请参见Refs和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);
您可以在代码沙盒编辑器中运行此示例。
最近的回答有一个例子,它使用React.useState
将状态保存在父组件中是推荐的方法。父组件需要访问它,因为它跨两个子组件管理它。不建议将其移动到全局状态,就像由Redux管理的状态一样,原因与在软件工程中全局变量通常不如局部变量的原因相同。
当状态在父组件中时,如果父组件在props中给了子组件值和onChange处理程序,子组件就可以改变它(有时它被称为值链接或状态链接模式)。下面是你如何用钩子做这件事:
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将保留状态,但不会在状态改变时重新呈现,只有Child1和Child2会。
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>
}
附注:这里还有很多类似的例子,它们涵盖了更复杂的场景,包括深度嵌套数据、状态验证、使用setState钩子的全局状态等等。网上还有一个完整的示例应用程序,它使用了Hookstate和上面解释的技术。