You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NGRX最佳实践:如何避免状态属性被重复值更新?

NGRX中重复触发视图更新的相等性检查责任划分与最佳实践

问题场景回顾

定义状态接口:

interface State {
    toto: string[]
}

触发动作:setToto(["value1", "value2"])
Reducer返回新状态:

{...state, toto: ["value1", "value2"]}

Selector定义:

const selectToto = () => createSelector(selectRootState, state => state.toto);

视图通过selectToto() | async订阅数据时,重复触发相同值的setToto动作,会导致视图重复更新,这是需要避免的。

你当前在Reducer中用Lodash的isEqual做深相等检查来规避这个问题,但不确定这是否是最优方案,下面针对「相等性检查该由Selector/Reducer/组件谁负责」的问题,结合NGRX最佳实践分析:


各层级的职责与可行性分析

1. Reducer层(推荐作为首要处理点)

Reducer的核心职责是根据动作返回新的不可变状态,或原状态。从状态变更的源头控制不必要的更新,是最合理的选择:

  • 当动作携带的 payload 与当前状态中的数据内容完全一致时,直接返回原状态引用,避免创建新的状态对象/数组。
  • 这样后续的Selector、组件订阅都会因为状态引用未变更(浅对比即可识别),不会触发更新。
  • 你当前的实现逻辑是符合Reducer职责的,但可以根据数据复杂度优化对比逻辑:
    • 如果是简单数组(如string[]),可以先对比长度,再逐个元素对比,比isEqual更高效:
      case Action.SetToto:
        const current = state.toto;
        const payload = action.payload;
        if (current.length === payload.length && current.every((val, idx) => val === payload[idx])) {
          return state;
        }
        return { ...state, toto: payload };
      
    • 对于复杂对象,再考虑使用isEqual这类深对比工具,但要注意大数据量下的性能开销。

2. Selector层(推荐作为兜底补充)

NGRX的Selector自带**记忆化(Memoization)**功能,默认通过浅对比判断输入是否变化。如果Reducer因为某些原因返回了内容相同但引用不同的状态(比如第三方库生成的新对象),可以通过自定义Selector的相等性检查来拦截重复值:

  • 使用createSelector的第三个参数,配置resultEqualityCheck为深对比函数:
    import { createSelector } from '@ngrx/store';
    import isEqual from 'lodash/isEqual';
    
    export const selectToto = createSelector(
      selectRootState,
      (state) => state.toto,
      {
        memoizeOptions: {
          resultEqualityCheck: isEqual
        }
      }
    );
    
  • 这样即使状态引用变更,只要内容相同,Selector不会重新发射新值,视图就不会触发更新。
  • 注意:Selector的记忆化是基于输入参数的浅对比,如果根状态引用发生变化(即使toto未变),默认还是会重新计算,所以配合Reducer的检查能获得最优效果。

3. 组件层(不推荐)

组件的职责是展示数据、处理用户交互,将状态相等性检查放在组件层会带来以下问题:

  • 打破职责单一原则,组件逻辑变得臃肿,难以维护。
  • 需要在组件中手动过滤重复值,比如在订阅时添加distinctUntilChanged(isEqual),增加了组件的复杂度。
  • 不符合NGRX「状态逻辑集中管理」的设计理念,状态相关逻辑应该统一放在Store层(Reducer/Selector)。

总结最佳实践

  1. 优先在Reducer层处理:从源头避免不必要的状态引用变更,减少后续所有订阅的无效更新。
  2. 配合Selector层兜底:针对Reducer无法覆盖的场景(如第三方数据生成的新引用),用自定义记忆化相等检查拦截重复值。
  3. 绝对避免组件层处理:保持组件简洁,专注于UI展示和交互逻辑。
  4. 性能优化补充:如果处理复杂大对象,可使用Immer库来自动实现结构共享——Immer在修改状态时,会复用未变化的部分引用,这样浅对比就能识别内容未变更,无需深对比。

内容的提问来源于stack exchange,提问作者The Segfault

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 09:20:31