你不必使用 Redux

2025-05-24

你不必使用 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监听器会触发onUserInputprop 中的函数并传入用户输入。节点 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 的函数。 

这就是为什么我们使用 和 这两个函数mapStateToPropsmapDispatchToProps它们分别处理 store 的状态和调度。我们希望节点 D 组件订阅 store 的更新,也就是我们应用的状态更新。这意味着,每当应用的状态更新时,mapStateToProps都会被调用。 的结果mapStateToProps是一个对象,它将被合并到节点 D 的组件 props 中。我们的mapDispatchToProps函数允许我们创建在调用时调度的函数,并将这些函数作为 props 传递给组件。我们将利用这一点,返回一个新函数,该函数会dispatch()调用传入 action 的 。

在我们的例子中,对于函数,我们只对inputValuemapStateToProps感兴趣,因此我们返回一个 object 。对于,我们返回一个函数,该函数接受输入值作为参数,并使用该值 dispatch 一个类型的 action 。 返回的新状态对象函数被合并到我们组件的 props 中。因此,我们将组件定义如下:{ inputValue: state.inputValue }mapDispatchToPropsonUserInputUSER_INPUTmapStateToPropsonUserInput

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 类型mapStateToPropsmapDispatchToPropsreducer 与组件分开,为每个 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
PREV
为什么要使用开发人员字体
NEXT
学习在 VS Code 中成为无鼠标 Web 开发人员(更新日期:2020 年 7 月 22 日)