React应用程序从根本上来讲是一棵组件树,能够相互通讯数据。在组件之间传递数据一般是无痛的。可是,随着应用程序树的增加,在保持可读代码库的同时传递数据变得更加困难。node
假设咱们有如下树结构: react
这里有一个简单的树,有3个层次。在该树中,节点D和节点E都操纵一些相似的数据:假设用户输入节点D中的一些文本,咱们但愿在节点E中显示该文本。git
咱们如何将数据从节点D传递到节点E?github
本文介绍了三种解决此问题的可行方法:redux
本文的目的是比较这些方法,并代表,当解决一个常见问题时,例如咱们刚才所说的问题,能够坚持使用React的context API。api
这样作的方法是天真的经过props将数据从子节点传递到父节点,再将数据从父节点传递到子节点,如:D->B->A 而后 A->C->E。安全
这里的想法是使用onUserInput
从子节点到父节点触发的函数将输入数据从节点D传送到节点A的state
状态,而后咱们将该数据从节点A的state
状态传递到节点E.bash
咱们从节点D开始:架构
class NodeD extends Component {
render() {
return (
<div className="Child element">
<center> D </center>
<textarea
type="text"
value={this.props.inputValue}
onChange={e => this.props.onUserInput(e.target.value)}
/>
</div>
);
}
}
复制代码
当用户键入内容时,onChange
监听函数将从prop
的onUserInput
触发该函数并传入用户输入。节点D prop中的该函数将触发节点B prop
中onUserInput
另外一个函数,以下:app
class NodeB extends Component {
render() {
return (
<div className="Tree element">
<center> B</center>
<NodeD onUserInput={inputValue => this.props.onUserInput(inputValue)} />
</div>
);
}
}
复制代码
最后,当到达根节点A时,onUserInput
在节点B prop中触发将把节点A中的状态改变为用户输入的值。
class NodeA extends Component {
state = {
inputValue: ""
};
render() {
return (
<div className="Root element">
<center> A </center>
<NodeB
onUserInput={inputValue => this.setState({ inputValue: inputValue })}
/>
<NodeC inputValue={this.state.inputValue} />
</div>
);
}
}
复制代码
inputValue
值将经过props从节点C传递给子节点E:
class NodeE extends Component {
render() {
return (
<div className="Child element">
<center> E </center>
{this.props.inputValue}
</div>
);
}
}
复制代码
看到它已经为咱们的代码添加了一些复杂性,即便它只是一个小例子。您能够想象一下应用程序增加时的状况吗?🤔
这种方法依赖于树的深度,所以对于更大的深度,咱们须要经历更大的组件层。这可能太长而没法实现,过于重复而且会增长代码复杂性。
另一种方法是使用像Redux同样的状态管理库。
Redux is a predictable state container for JavaScript apps. The state of our whole application is stored in an object tree within a single store, which your app components depend on. Every component is connected directly to the global store, and the global store life cycle is independent of the components' life cycle.
咱们首先定义应用程序的状态:咱们感兴趣的数据是用户在节点D中输入的内容。咱们但愿将这些数据提供给节点E.为此,咱们能够在store中提供这些数据。而后,节点E能够订阅它以便访问数据。
咱们会稍微回到store。
接下里是定义咱们的reducer。咱们的reducer具体说明了应用程序的状态是如何根据传递到store的actions响应更改的。
定义的reducer以下:
const initialState = {
inputValue: ""
};
const reducer = (state = initialState, action) => {
if (action.type === "USER_INPUT") {
return {
inputValue: action.inputValue
};
}
return state;
};
复制代码
在用户输入任何内容以前,咱们知道咱们的状态数据或inputValue将是一个空字符串。所以,咱们使用空字符串inputValue
为reducer
定义默认初始状态。
这里的逻辑是:一旦用户在节点D键入内容,不管用户输入了什么,咱们“触发”或者说派发了一个action来更新state状态。这里的“更新”不是指“突变”或者改变了当前状态,而是说返回了一个新状态。
if语句将派发的action根据其类型映射到要返回的新状态。因此咱们已经知道派发的action是一个包含类型键的对象。咱们如何得到新状态的用户输入值?咱们只是在操做对象中添加另外一个名为inputValue的键,在咱们的reducer块中,咱们使新状态的inputValue具备该输入值action.inputValue
。因此咱们的应用程序的行为将遵循这种架构:
{ type: "SOME_TYPE", inputValue: "some_value" }
复制代码
最终,咱们的dispatch声明将以下所示:
dispatch({ type: "SOME_TYPE", inputValue: "some_value" })
复制代码
当咱们从任何组件调用dispatch语句时,咱们传入操做的类型和用户输入值。
好的,如今咱们知道应用程序是如何工做的:在咱们的输入节点D中,咱们dispatch一个USER_INPUT类型的动做并传入用户刚输入的任何值,在咱们的显示节点E中咱们传递当前的值即用户输入为应用程序的状态。
为了使咱们的store可用,咱们将它传递给从react-redux
import的Provider
组件,接下来把App
包裹在里面。因为咱们知道节点D和E将使用该store中的数据,所以咱们但愿Provider组件包含这些节点的公共父节点,所以要么是根节点A,要么是整个App组件。让咱们选择App
组件包含在咱们的Provider
组件中:
import reducer from "./store/reducer";
import { createStore } from "redux";
import { Provider } from "react-redux";
const store = createStore(reducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
复制代码
目前咱们已经设置了store和reducer,接下里在节点D和节点E中搞事情。
首先咱们看一下节点D,咱们感兴趣用户在textarea元素中输入了什么。这意味着两件事:
但在作任何这些以前,咱们须要设置一些东西:
咱们首先须要将节点D组件链接到咱们的store。为此,咱们使用react-redux中的connect()函数。它为链接的组件提供了store所需的数据,以及可用于将操做分派给store的功能。
This is why we use the two mapStateToProps and mapDispatchToProps which deal with the store's state and dispatch respectively. We want our node D component to be subscribed to our store updates, as in, our app's state updates. This means that any time the app's state is updated, mapStateToProps will be called. The results of mapStateToProps is an object which will be merged into our node D's component props. Our mapDispatchToProps function lets us create functions that dispatch when called, and pass those functions as props to our component. We will make use of this by returning new function that calls dispatch() which passes in an action.
在咱们的例子中,对于mapStateToProps
函数,咱们只对inputValue
感兴趣,因此咱们返回一个对象{ inputValue: state.inputValue }
。对于mapDispatchToProps
,咱们返回一个函数onUserInput
,该函数将输入值做为参数,并使用USER_INPUT
类型派发action。返回的新状态对象mapStateToProps
和onUserInput
函数合并到咱们组件的props
中。因此咱们定义咱们的组件:
class NodeD extends Component {
render() {
return (
<div className="Child element">
<center> D </center>
<textarea
type="text"
value={this.props.inputValue}
onChange={e => this.props.onUserInput(e.target.value)}
/>
</div>
);
}
}
const mapStateToProps = state => {
return {
inputValue: state.inputValue
};
};
const mapDispatchToProps = dispatch => {
return {
onUserInput: inputValue =>
dispatch({ type: "USER_INPUT", inputValue: inputValue })
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(NodeD);
复制代码
咱们完成了节点D!如今让咱们转到节点E,在那里咱们要显示用户输入。
咱们但愿在此节点上显示用户输入数据。咱们已经知道这些数据基本上是咱们应用程序的当前状态,就像咱们的store同样。因此最终,咱们但愿访问该store并显示其数据。为此,咱们首先须要使用connect()咱们以前使用的相同功能具备mapStateToProps的函数将节点E组件订阅到store的更新。以后,咱们只须要使用this.props.val
从组件的props中访问store中的数据:
class NodeE extends Component {
render() {
return (
<div className="Child element">
<center> E </center>
{this.props.val}
</div>
);
}
}
const mapStateToProps = state => {
return {
val: state.inputValue
};
};
export default connect(mapStateToProps)(NodeE);
复制代码
咱们终于完成了Redux!🎉你能够看看咱们刚刚在这里作了些什么。
在更复杂的示例的状况下,好比使用具备更多共享/操做存储的组件的树,咱们将须要在每一个组件处使用这两个mapStateToProps和mapDispatchToProps函数。在这种状况下,经过为每一个组件建立单独的文件夹,将咱们的操做类型和reducers与组件分开可能更明智。
......谁有时间?
如今让咱们使用上下文API重作相同的示例。
React Context API已经存在了一段时间,但直到React的16.3.0版才能在生产中使用它变得安全。这里的逻辑接近Redux的逻辑:咱们有一个上下文对象,它包含咱们但愿从其余组件访问的一些全局数据。
首先,咱们建立一个上下文对象,其中包含应用程序的初始状态做为默认状态 而后咱们建立一个Provider
和一个Consumer
组件:
const initialState = {
inputValue: ""
};
const Context = React.createContext(initialState);
export const Provider = Context.Provider;
export const Consumer = Context.Consumer;
复制代码
咱们的
Provider
组件具备做为子项的全部组件,咱们但愿从中访问上下文数据。就像Provider
上面的Redux
版本同样。为了提取或操纵上下文,咱们使用Consumer
至关于组件。
咱们但愿咱们的Provider组件包装整个App,就像上面的Redux版本同样。然而,这Provider与咱们见过的前一个有点不一样。在咱们的App组件中,咱们使用一些数据初始化默认状态,咱们能够经过值支持咱们的Provider组件来共享。
在咱们的示例中,咱们将共享this.state.inputValue以及操做状态的函数,如咱们的onUserInput函数。
class App extends React.Component {
state = {
inputValue: ""
};
onUserInput = newVal => {
this.setState({ inputValue: newVal });
};
render() {
return (
<Provider
value={{ val: this.state.inputValue, onUserInput: this.onUserInput }}
>
<div className="App">
<NodeA />
</div>
</Provider>
);
}
}
复制代码
如今咱们能够继续使用Consumer组件访问Provider组件的数据:)
对于用户输入数据的节点D:
const NodeD = () => {
return (
<div className="Child element">
<center> D </center>
<Consumer>
{({ val, onUserInput }) => (
<textarea
type="text"
value={val}
onChange={e => onUserInput(e.target.value)}
/>
)}
</Consumer>
</div>
);
};
复制代码
对于咱们在其中显示用户输入的节点E:
const NodeE = () => {
return (
<div className="Child element ">
<center> E </center>
<Consumer>{context => <p>{context.val}</p>}</Consumer>
</div>
);
};
复制代码
咱们完成了示例的context api版本!🎉这不是很难吧?在这里查看
若是咱们但愿可以访问更多context的组件,该怎么办?咱们可使用Provider组件包装它们,并使用Consumer组件来访问/操做上下文!简单 :)
咱们能够看到咱们的Redux版本的示例比Context版本花了更多的时间。咱们已经能够看到Redux了:
若是您正在处理一个更复杂的应用程序,并但愿查看您的应用程序的全部dispatch操做的历史记录,“点击”其中任何一个并跳转到该时间点,而后绝对考虑使用Redux的漂亮的dope devTools扩展!
可是,若是你只想让一些数据全局化以便从一堆组件中访问它,你能够从咱们的例子中看到Redux和React的Context API都作了大体相同的事情。因此在某种程度上,你没必要使用Redux!