Redux 初始状态在 Angular 中充满了空值



我已经在一个项目上工作了大约10个月,它基于Angular 4+和Redux(通过angular-redux/store)。这个项目基本上是成功的,它从一月份开始投入生产,没有遇到任何严重的问题。

但。。。我逐渐对 Redux 感到失望,我认为至少很烦人,但可能对维护不利:具有null值的初始状态,所以,我想找出我做错了什么 - 或者这是否是 Redux 设计本身的问题。

如何重现它

这里有一个简单而好的例子:https://github.com/marinho/example-app

您只需克隆,npm install它,然后运行npm run ng serve并在浏览器上加载localhost:4200。然后转到控制台并筛选"111"以查看两行:

11111 undefined
11112 null

这是由两个可观察的订阅打印的,它们分别监视myUndefinedmyObjects.country的值。第一个是无效键(未在全局状态上初始化),第二个是使用null初始化的。

这些订阅的更精确位置如下:https://github.com/marinho/example-app/blob/master/src/app/component.ts#L15

为什么会发生

众所周知,在 Redux 上,必须使用初始状态configureStore。这种初始状态是一棵大树,所有可能的键都由它们的化简器初始化。到初始化时,没有运行任何操作,两个化简器都没有任何要解析的有效负载,因此在大多数情况下它们默认为null

直到这里,标准的Redux/Angular,AFAIK。

现在,我的两个订阅(myUndefined$myObjects$)观察到AnonymousSubject实例,这些实例是angular-redux/store提供的通道,用于发出在密钥路径上更新的新值(即@select(['myObjects', 'city'])表示<AppState>.myObjects.city)。

问题是,随着初始状态的初始化,所有已知的状态键上都有新值,但几乎所有这些值都分配了null

解决方案还是黑客?

一旦有订阅侦听这些键中的任何一个,这样的null将作为第一个值发出,这使得我需要空检查,例如.filter(x => x !== null)或类似的地方。这真的很丑陋。

另一种方法是使用Epics(来自redux-observables),但它们必须设计得很好,结果闻起来很脆弱,不那么可靠的堆栈。

我的印象是,这不仅仅是角度冗余/存储本身的问题,而且实际上是具有全局状态的副作用。

一个简单的解决方案可能是修复angular-redux/store,使其仅在密钥不再undefined时才开始发出值,因此,避免使用null初始化密钥,而根本不打算初始化它们。但同样,如果这是 Redux 的问题,我不确定这是否是正确的方法。

那么,有没有人遇到同样的事情或有一个优雅的解决方案?

在我看来,这是 Redux 固有的。从概念上讲,Redux 是一种架构,它鼓励您将 UI 视为任何给定时间整个状态的纯计算。您的整个应用程序是一个大型状态机。 因此,如果将初始值设置为null,则该选择的第一个值将为null。如果您根本没有为字段设置初始值,则相应选择的第一个值将undefined

这样,Redux 就可以在您的应用程序中"扁平化时间"并将其分解掉。只有当前状态和下一个操作。这个基本假设是使调试工具如此强大的原因。

我们可以让我们的可观察量"等到第一个非零值"直到触发,但这正是我个人使用 Redux 来避免的那种时间依赖。我的感觉是,在 Angular-Redux/store 库中进行此更改可能不是正确的选择,因为其他人可能没有预料到它。我也不确定它会对时间旅行和其他调试工具产生什么影响。

那么就您而言,有哪些更清洁的方法来处理这些情况?

通常,我会考虑以下其中一种:

1) 确保您的减速器始终具有初始状态。

这适用于您实际上具有合理的键前期值的情况。因此,对于任何减速器控制state.myUndefined,您都可以

const INITIAL_VALUE = 'some-reasonable-value';
const myUndefinedReducer = (state = 'some-reasonable-value', action) => {
// etc.
};

这样,您就可以在化简器本身中对默认值进行编码。

2)在选择时处理它

这适用于不直接绑定到存储属性的选择(例如,它们是某些 RxJS 计算的结果)。它也适用于密钥的初始值是动态的情况,例如,在 API 调用填充它或类似值之前undefined的值。

正如您所指出的,这基本上归结为在几个地方进行过滤器,这可能很冗长。但是,在您的示例中,您可以使用select$Transformer更干净地实现它:

一些实用程序文件:

export const skipNil = (obs$: Observable<any>): Observable<any> =>
obs$.filter(v => v !== null && v !== undefined);

Component.ts:

@select$('myUndefined', skipNil) myUndefined$: Observable<any>;
@select$(['myObjects', 'country'], skipNil) country$: Observable<any>;

最新更新