你不必使用 Redux
React 应用本质上是一个组件树,组件之间相互通信数据。在组件之间传递数据通常很轻松。然而,随着应用树的增长,在维护代码库健全易读的同时传递数据变得越来越困难。
假设我们有以下树结构: 这里有一棵简单的三层树。在这棵树中,节点 D 和节点 E 都操作一些类似的数据:假设用户在节点 D 输入了一些文本,我们希望将其显示在节点 E 上。
我们如何将数据从节点 D 传递到节点 E?
本文提出了三种解决此问题的可能方法:
- 支柱钻井
- Redux
- React 的 context API
本文的目的是比较这些方法,并表明,当涉及到解决我们刚才提到的常见问题时,只需坚持使用 React 的上下文 API 即可。
方法一:支柱钻孔
一种方法是简单地通过 props 将数据从子级传递到父级,然后从父级传递到子级,如下所示:D->B->A,然后 A->C->E。
这里的想法是使用onUserInput
从子节点触发到父节点的函数将输入数据从节点 D 传送到节点 A 的状态,然后我们将该数据从节点 A 的状态传递到节点 E。
我们从节点 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
监听器会触发onUserInput
prop 中的函数并传入用户输入。节点 D prop 中的函数会触发onUserInput
节点 B prop 中的另一个函数,如下所示:
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>
);
}
}
瞧,即使只是一个小例子,它已经给我们的代码增加了一些复杂性。你能想象当应用程序规模扩大时会变成什么样吗?🤔
这种方法依赖于树的深度,因此,为了获得更大的深度,我们需要遍历更大层的组件。这会导致实现过程过于冗长、重复性过强,并增加代码的复杂性。
方法 2:使用 Redux
另一种方法是使用像 Redux 这样的状态管理库。
Redux 是一个适用于 JavaScript 应用的可预测状态容器。
整个应用程序的状态存储在单个 store 中的对象树中,您的应用组件都依赖该 store。每个组件都直接连接到全局 store,并且全局 store 的生命周期与组件的生命周期无关。
我们首先定义应用的状态:我们感兴趣的数据是用户在节点 D 中输入的内容。我们希望将这些数据提供给节点 E。为此,我们可以将这些数据添加到我们的 store 中。然后,节点 E 可以订阅 store 来访问这些数据。
我们稍后会回到 store 的讨论。
步骤1:定义Reducer
接下来是定义我们的 Reducer。Reducer 指定了应用程序的状态如何响应发送到 Store 的操作而发生变化。我们将 Reducer 块定义如下:
const initialState = {
inputValue: ""
};
const reducer = (state = initialState, action) => {
if (action.type === "USER_INPUT") {
return {
inputValue: action.inputValue
};
}
return state;
};
在用户输入任何内容之前,我们知道状态的数据或inputValue会是空字符串。因此,我们为 Reducer 定义了一个默认的初始状态,其inputValue为空字符串。
这里的逻辑是,一旦用户在节点 D 中输入内容,我们就会“触发”或者说调度一个action,然后我们的 reducer 会将状态更新为用户输入的内容。这里的“更新”不是指“变异”或改变当前状态,而是指返回一个新的状态。
if 语句根据已调度操作的类型将其映射到要返回的新状态。因此,我们已经知道已调度操作是一个包含 type 键的对象。那么,如何获取新状态的用户输入值呢?我们只需在操作对象中添加另一个名为inputValue的键,然后在 Reducer 块中,使用 使新状态的 inputValue 具有该输入值action.inputValue
。因此,我们应用的操作将遵循以下架构:
{ type: "SOME_TYPE", inputValue: "some_value" }
最终,我们的调度语句将如下所示:
dispatch({ type: "SOME_TYPE", inputValue: "some_value" })
当我们从任何组件调用该调度语句时,我们都会传入动作的类型和用户输入值。
好的,现在我们对应用程序的工作原理有所了解:在我们的输入节点 D 中,我们分派一个类型的动作USER_INPUT
并传入用户刚刚输入的值,在我们的显示节点 E 中,我们传入应用程序当前状态的值,即用户输入。
第 2 步:定义商店
为了使 store 可用,我们将其传入一个Provider
从 react-redux 导入的组件中。然后将我们的 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")
);
现在我们已经设置了我们的商店和减速器,我们可以对节点 D 和 E 进行操作了!
步骤 3:实现用户输入逻辑
我们先来看看节点 D。我们感兴趣的是用户在textarea
元素中输入的内容。这意味着两件事:
1- 我们需要实现onChange
事件监听器并使其存储用户在商店中输入的任何内容。2-
我们需要将 value 属性textarea
作为存储在我们商店中的值。
但在做任何事之前,我们需要设置一些东西:
首先,我们需要将 Node D 组件连接到 Store。为此,我们使用了connect()
react-redux 中的函数。它为连接的组件提供了从 Store 中获取所需的数据片段,以及可用于将 Action 分发到 Store 的函数。
这就是为什么我们使用 和 这两个函数
mapStateToProps
,mapDispatchToProps
它们分别处理 store 的状态和调度。我们希望节点 D 组件订阅 store 的更新,也就是我们应用的状态更新。这意味着,每当应用的状态更新时,mapStateToProps
都会被调用。 的结果mapStateToProps
是一个对象,它将被合并到节点 D 的组件 props 中。我们的mapDispatchToProps
函数允许我们创建在调用时调度的函数,并将这些函数作为 props 传递给组件。我们将利用这一点,返回一个新函数,该函数会dispatch()
调用传入 action 的 。
在我们的例子中,对于函数,我们只对inputValuemapStateToProps
感兴趣,因此我们返回一个 object 。对于,我们返回一个函数,该函数接受输入值作为参数,并使用该值 dispatch 一个类型的 action 。 返回的新状态对象和函数被合并到我们组件的 props 中。因此,我们将组件定义如下:{ inputValue: state.inputValue }
mapDispatchToProps
onUserInput
USER_INPUT
mapStateToProps
onUserInput
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,我们要在这里显示用户输入。
步骤 4:实现用户输出逻辑
我们希望在此节点上显示用户输入的数据。我们已经知道这些数据基本上就是我们应用的当前状态,也就是我们的 store。因此,最终我们希望访问该 store 并显示其数据。为此,我们首先需要使用connect()
与之前相同的函数,将节点 E 组件订阅到 store 的更新。之后,我们只需使用this.props.valmapStateToProps
从组件的 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!🎉 你可以在这里看看我们刚刚做了什么。
对于更复杂的例子,比如树状结构包含更多共享/操作 store 的组件,那么每个组件都需要这两个函数。在这种情况下,将 action 类型mapStateToProps
和mapDispatchToProps
reducer 与组件分开,为每个 action 类型和 reducer 创建单独的文件夹可能更明智。
……谁的时间安排得对?
方法 3:使用 React 的 context API
现在让我们使用 context API 重做相同的示例。React
Context API已经存在一段时间了,但直到 React 16.3.0版本才可以安全地在生产环境中使用。这里的逻辑与 Redux 的逻辑相似:我们有一个 context 对象,其中包含一些我们希望从其他组件访问的全局数据。
首先,我们创建一个 context 对象,其中包含应用程序的初始状态作为默认状态。然后,我们创建一个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
组件的 value prop 共享。
在我们的示例中,我们共享了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>
);
}
}
现在我们可以继续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 版本示例就完成了!🎉 是不是很简单?点击此处
查看 。如果我们有更多组件需要访问 context 怎么办?我们只需将它们用 Provider 组件包装起来,然后使用 Consumer 组件来访问/操作 context 即可!简单 :)
好的,但是我应该使用哪一个
我们可以看到,Redux 版本的示例比 Context 版本花费的时间要多一些。我们已经看到 Redux 的以下表现:
- 需要更多行代码,并且对于更复杂的示例(需要更多组件来访问商店)来说 可能过于“样板” 。
- 增加复杂性:处理许多组件时,将减速器和动作类型从组件中分离到唯一的文件夹/文件中可能更明智。
- 引入学习曲线:一些开发人员发现自己很难学习 Redux,因为它要求学习一些新概念:reducer、dispatch、action、thunk、middleware……
如果您正在开发一个更复杂的应用程序,并希望查看应用程序分派的所有操作的历史记录,单击其中任何一个并跳转到该时间点,那么一定要考虑使用 Redux 的非常棒的devTools 扩展!
但是,如果您只想将一些数据全局化以便从一堆组件中访问它,那么从我们的示例中可以看出,Redux 和 React 的 context API 的功能大致相同。所以从某种程度上来说,您不必使用 Redux!
文章来源:https://dev.to/anssamghezala/you-don-t-have-to-use-redux-32a6