React Redux:更多容器 v.s。更少的容器

React Redux: More containers v.s. less containers

随着我进一步将 redux + react 实现到一个相当复杂的应用程序中,该应用程序依赖于许多 API 请求来加载单个页面,我无法决定使用单个容器组件是否更好在处理所有异步内容并将道具向下传递给哑组件的页面的根部,v.s。拥有多个容器组件,这些组件只关心他们需要的数据,以及获取他们需要的数据。我在这两种模式之间来回切换,发现它们每个都有 pros/cons:

如果我将单个容器组件放在顶部:

这是一个问题的可视化示例,它显示了 props-wise 所需的关系:

<MyContainer
  data={this.props.data}
  isFetchingData={this.props.isFetchingData}
  fetchData={this.props.fetchData}
 >
   {!isFetchingData && 
     <MyModal
        someData={props.data}
        fetchData={props.fetchData}
      >
       <MyModalContent
         someData={props.data}
         fetchData={props.fetchData}
       >
          <SomethingThatDependsOnData someData={props.someData} />
          <SomeButtonThatFetchesData  onClick={props.fetchData} />
        </MyModalContent>
      </MyModal>
    }
 </MyContainer>

如你所见,<MyModal /><MyModalContent />现在需要关注与它无关的道具,看到模态应该能够被重复使用并且只关注模态的风格品质。

一开始上面的看起来不错,但是当我达到 100 多个组件时,一切都变得非常混乱,而且我发现这些顶级容器组件的复杂性对我来说太高了,因为它们中的大多数都是如此(在我正在开发的应用程序中)取决于来自 3+ API 个请求的响应。

然后我决定尝试多个容器:

所以主要的困境是,如果我使用 一个容器 ,我会在根容器和叶组件之间的组件中获得这种不必要的复杂性。如果我使用 多个容器 ,我会在叶组件中获得更多的复杂性,并最终得到需要担心 isFetching 的按钮,即使按钮不应该关心那个.

我想知道是否有人找到了避免这两种缺点的方法,如果找到了,您遵循的 "rule of thumb" 是什么来避免这种情况?

谢谢!

我一直看到的方式是让您的容器位于逻辑组件组的最顶层组件而不是您的 root/app 组件。

因此,如果我们有一个显示结果的简单搜索应用程序,并假设组件层次结构是这样的

<Root> <- setup the app
  <App>
    <Search/> <- search input
    <Results/> <- results table
  </App>
</Root>

我会制作 SearchResults redux 感知容器。因为 React 组件被认为是 可组合的 。您可能有其他组件或页面需要显示 ResultsSearch。如果将数据获取和存储感知委托给根或应用程序组件,它会使组件变得依赖于每个 other/app。当您必须实施更改时,这使得线下变得更加困难,现在您必须更改所有使用它们的地方。

例外情况可能是组件之间确实有紧密耦合的逻辑。即便如此,我还是会说你应该创建一个容器来包装你的紧密耦合的组件,因为它们将无法在没有彼此的情况下实际使用。

我什至不会考虑使用单一容器方法。它几乎完全否定了 redux 的所有优势。如果所有状态都在一个地方并且所有回调都在一个地方(根组件),则根本不需要状态管理系统。

不过,我认为要走一条细线。我正在制作一个应用程序,我已经用了大约 5 周(兼职),现在最多 3000 行。它有 3 层视图嵌套,我自己实现的路由机制,以及 10 层以上嵌套深度需要修改状态的组件。我基本上每个大屏幕都有一个 redux 容器,而且效果非常好。

但是,如果我单击我的 "clients" 视图,我会得到一个很好的客户列表,因为我的客户视图位于 redux 容器内,并获取作为道具传递的客户列表。然而,当我点击一个客户端时,我真的很犹豫是否为单个客户端的配置文件做另一个 redux 容器,因为它只是传递道具的一个额外级别。似乎根据应用程序的范围,您可能希望将 props 传递到 redux 容器之外的 1-2 级,如果不止于此,则只需创建另一个 redux 容器。话又说回来,如果它是一个更复杂的应用程序,那么有时使用 redux 容器而有时不使用它们的混合对于可维护性来说可能会更糟。简而言之,我的观点是尽可能减少 redux 容器,但绝对不能以复杂的 prop 链为代价,因为这是开始使用 redux 的要点。

Redux 作者 Dan Abramov 建议您在需要时使用容器组件。也就是说,一旦你在组件之间上下连接了太多的道具,就该使用容器了。

他称之为"ongoing process of refactoring"。

查看这篇文章:https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0

所以我发布这个问题已经两年多了,而且一直都是 我一直在与 React/Redux 合作。我现在的一般经验法则 是以下内容:使用更多的容器,但尝试以不需要了解 isFetching.

的方式编写组件

例如,这是一个典型的例子,说明我之前是如何建立待办事项列表的:

  function Todos({ isFetching, items }) {
    if (isFetching) return <div>Loading...</div>

    return (
      <ul>
        {items.map(item => 
          <li key={item.id}>...</li>
        )}
      </ul>
    )
  }

现在我会做更多类似的事情:

  function Todos({ items }) {
    if (!items.length) return <div>No items!</div>

    return (
      <ul>
        {items.map(item => 
          <li key={item.id}>...</li>
        )}
      </ul>
    )
  }

这样,你只需要连接数据,组件不关心异步API调用的状态。

大部分东西都可以这样写。我很少需要 isFetching,但当我这样做时通常是因为:

  1. 例如,我需要防止第二次单击提交按钮,这会调用 API,在这种情况下,prop 应该被调用 disableSubmit 而不是 isFetching,或

  2. 我想在等待异步响应时显式显示加载程序。

现在,您可能会想,"wouldn't you want to show a loader when items are being fetched in the above todos example?" 但实际上,我不会。

原因是在上面的示例中,假设您正在轮询新的待办事项,或者当您添加待办事项时,您 "refetch" 待办事项。在第一个示例中会发生的情况是,每次发生这种情况时,待办事项都会消失并经常被 "Loading..." 替换。

然而,在与 isFetching 无关的第二个示例中,新项目只是 appended/removed。在我看来,这是更好的用户体验。

事实上,在发布之前,我检查了我编写的交换接口的所有 UI 代码,该代码非常复杂并且没有找到必须连接 isFetching 的单个实例我写的一个容器组件。

您不必在同一个地方调度和加载您的状态。

换句话说,您的按钮可以分派异步请求,而另一个组件可以检查您是否正在加载。

例如:

// < SomeButtonThatFetchesData.js>

const mapDispatchToProps = (dispatch) => ({
  onLoad: (payload) => 
    dispatch({ type: DATA_LOADED, payload })
});

您需要一些中间件来处理加载状态。当您传递异步负载时,它需要更新 isFetching

例如:

const promiseMiddleware = store => next => action => {
  if (isPromise(action.payload)) {
    store.dispatch({ type: ASYNC_START, subtype: action.type });

然后你就可以在任何地方使用它了:

// <MyContainer.js>

const mapStateToProps = (state) => ({
  isFetching: state.isFetching
});

并在您的内部嵌套组件中加载数据:

// <SomethingThatDependsOnData.js>

const mapStateToProps = (state) => ({
  someData: state.someData
});