使用重新选择来计算派生状态

时间:2016-03-15 10:44:47

标签: reactjs redux immutable.js reselect

我在我的应用程序中使用了React + Redux + Reselect + Immutable.js的最终组合。我喜欢重新选择的想法,因为它让我保持我的状态(由减速器维护)尽可能简单。我使用一个选择器来计算我需要的实际状态,然后将其输入到React组件。

这里的问题是,一次reducers的小改变会导致选择器重新计算整个派生输出,结果整个React UI也会更新。我的纯组件不会工作。这很慢。

典型示例:我的数据的第一部分来自服务器,基本上是不可变的。第二部分由客户端维护,并使用redux操作进行变异。它们由单独的减速器维护。

我使用选择器将两个部分合并到一个记录列表中,然后传递给React组件。但显然,当我在其中一个对象中更改单个内容时,将重新生成整个列表并创建新的Records实例。用户界面完全重新呈现。

显然每次都运行选择器效率不高但仍然相当快,我愿意做出这样的交易(因为它确实使代码更简单,更清晰)。问题是实际渲染很慢。

我需要做的是将新选择器输出与旧选择器输出深度合并,因为Immutable.js库非常智能,在没有任何更改时不会创建新实例。但由于选择器是无法访问先前输出的简单功能,我想这是不可能的。

我认为我目前的做法是错误的,我想听听其他想法。

可能的方法是在这种情况下摆脱重新选择并将逻辑移动到reducers的层次结构中,该层次结构将使用增量更新来维持所需的状态。

3 个答案:

答案 0 :(得分:6)

我解决了我的问题,但我想没有正确的答案,因为它实际上取决于具体的情况。就我而言,我决定采用这种方法:

原始选择器处理得很好的挑战之一是最终信息是从以任意顺序传递的许多部分编译而来的。如果我决定逐步在Reducer中构建最终信息,我必须确保计算所有可能的场景(信息块可能到达的所有可能的顺序)并定义所有可能状态之间的转换。然而,通过重新选择,我可以简单地拿走我现在拥有的东西并从中取出它。

为了保持此功能,我决定将选择器逻辑移动到包装父减速器

好吧,假设我有三个减速器,A,B和C,以及相应的选择器。每个处理一条信息。该部分可以从服务器加载,也可以来自客户端的用户。这将是我原来的选择器:

const makeFinalState(a, b, c) => (new List(a)).map(item => 
  new MyRecord({ ...item, ...(b[item.id] || {}), ...(c[item.id] || {}) });

export const finalSelector = createSelector(
  [selectorA, selectorB, selectorC],
  (a, b, c) => makeFinalState(a, b, c,));

(这不是实际的代码,但我希望它有意义。请注意,无论各个reducer的内容可用的顺序如何,选择器最终都会生成正确的输出。)

我希望我的问题现在很明确。如果任何这些reducers的内容发生变化,将从头开始重新计算选择器,生成所有记录的全新实例,最终导致React组件的完全重新呈现。

我目前的解决方案看起来很简单:

export default function finalReducer(state = new Map(), action) {
  state = state
    .update('a', a => aReducer(a, action))
    .update('b', b => bReducer(b, action))
    .update('c', c => cReducer(c, action));

  switch (action.type) {
    case HEAVY_ACTION_AFFECTING_A:
    case HEAVY_ACTION_AFFECTING_B:
    case HEAVY_ACTION_AFFECTING_C:
      return state.update('final', final => (final || new List()).mergeDeep(
        makeFinalState(state.get('a'), state.get('b'), state.get('c')));

    case LIGHT_ACTION_AFFECTING_C:
      const update = makeSmallIncrementalUpdate(state, action.payload);
      return state.update('final', final => (final || new List()).mergeDeep(update))
  }
}

export const finalSelector = state => state.final;

核心理念是:

  • 如果发生了重大事件(即我从服务器获得了大量数据),我会重建整个派生状态。
  • 如果发生了一些小的事情(即用户选择了一个项目),我只是在原始的reducer和包装父减速器中进行快速增量更改(存在一定的重复性,但是必须实现一致性和良好性性能)。

与选择器版本的主要区别在于我总是将新状态与旧状态合并。 Immutable.js库非常智能,不会用新的Record实例替换旧的Record实例如果他们的内容完全相同。因此保留了原始实例,因此不会重新呈现相应的纯组件。

显然,深度合并是一项代价高昂的操作,因此这对于非常大的数据集不起作用。但事实是,与React重新渲染和DOM操作相比,这种操作仍然很快。因此,这种方法可以在性能和代码可读性/简洁性之间做出很好的折衷。

最后注意事项:如果不是单独处理轻量级操作,则此方法基本上等同于将shallowEqual替换为deepEqual shouldComponentUpdate纯组分的方法。

答案 1 :(得分:1)

这种情况通常可以通过重构如何 UI连接到状态来解决。假设您有一个显示项目列表的组件:您可以将其连接到一个简单的ID列表,而不是将其连接到已建立的项目列表,并通过id将每个项目连接到其记录。这样,当记录发生变化时,id列表本身不会改变,只会重新呈现相应的连接组件。

如果在您的情况下,如果记录是从州的不同部分汇编的,那么产生单个记录的选择器本身可以连接到该特定记录的州的相关部分。

现在,关于使用带有重新选择的immutable.js:如果你的状态的原始部分已经是immutable.js对象,这种组合效果最好。这样您就可以利用它们使用持久数据结构的事实,并且重新选择的默认memoization函数效果最佳。您总是可以覆盖此memoization函数,但感觉选择器应该访问其先前的返回值,如果经常表明它负责应该保持在状态中的数据/或者它一次收集太多数据,并且也许更细粒度的选择器可以提供帮助。

答案 2 :(得分:0)

看起来你正在描述一个非常接近我编写re-reselect的原因的场景。

re-reselect是一个小reselect包装器,它使用备忘录工厂即时初始化选择器。

(免责声明:我是re-reselect)的作者。