Marinho Brandão
Marinho Brandão

Reputation: 661

Redux initial state full of null values in Angular

I have worked on a project for about 10 months now, which is based on Angular 4+ and Redux ( through angular-redux/store ). This project has been mostly successful, it's live in production since January and it doesn't experience any serious issue.

But... I have been gradually disappointed with Redux on one issue I consider at least annoying, but potentially bad for maintenance: the initial state with null values, so, I would like to find out what I'm doing wrong - or if that's a problem on Redux design per se.

How to reproduce it

Here is a simple and good example of it: https://github.com/marinho/example-app

You just clone, npm install it, then run npm run ng serve and load localhost:4200 on browser. Then go to Console and filter for "111" to see two lines:

11111 undefined
11112 null

That's printed by two observable subscriptions which are respectively watching values on myUndefined and myObjects.country. The first is an invalid key (wasn't initialised on the global state), the second was initialised with null.

The more precise location of these subscriptions is here: https://github.com/marinho/example-app/blob/master/src/app/component.ts#L15

Why it happens

As we know, on Redux, one must configureStore with an initial state. Such initial state is a big tree with all possible keys as initialised by their reducers. By the initialisation time, no action was ran, neither reducer had any payload to resolve, so they default to null in most cases.

Until here, standard Redux/Angular, AFAIK.

Now, my two subscriptions (myUndefined$ and myObjects$) observe AnonymousSubject instances, which are the channel provided by angular-redux/store to emit new values updated on a key path (i.e. @select(['myObjects', 'city']) means <AppState>.myObjects.city).

The problem is, as the initial state is initialised, there are new values on all known state keys, but almost all of them are assigned with null.

Solution or hack?

Once there are subscriptions listening to any of these keys, such null will be emitted as the first value, which makes me need null checks such as .filter(x => x !== null) or similar in too many places. That's really ugly.

The alternative is to use Epics ( from redux-observables ), but they must be well designed and the result smells a mix of fragile with a not-so-reliable stack.

My impression is that this is not just a problem on angular-redux/store itself but actually a side effect of having a global state.

A simple solution could be to fix angular-redux/store to only start emitting values once the key isn't undefined anymore and so, avoid initialising keys with null and just prefer to not initialise them at all. But again, if that's a problem of Redux, I'm not sure this would be the right way to go.

So, is there anyone experiencing the same or has an elegant solution for it?

Upvotes: 2

Views: 3872

Answers (1)

Seth Davenport
Seth Davenport

Reputation: 389

To my mind this is inherent to Redux. Conceptually, Redux is an architecture which encourages you to think of your UI as a pure computation of the entire state at any given time. Your whole app is one big state machine. So if you set an initial value of null, the first value of that selection will be null. If you don't set an initial value for a field at all, then the first value of the corresponding selection will be undefined.

This way, Redux kind of 'flattens time' in your app and factors it out. There's only the current state and the next action. This fundamental assumption is what makes the debugging tools so powerful.

We could have our Observables 'wait until the first non-nil value' until firing, but that's exactly the kind of temporal dependency I personally use Redux to avoid. My feeling is that making this change in the Angular-Redux/store library is probably not the right call, given that other people are probably not expecting it. I'm also not sure what effect it would have on time travel and other debug tools.

So in your case, what are some cleaner ways to handle these situations?

Generally I would consider one of the following:

1) make sure your reducers always have an initial state.

This works for cases where you do in fact have a reasonable up-front value for a key. So for whatever reducer controls state.myUndefined, you do

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

This way you're encoding the default value in the reducer itself.

2) Handle it on selection

This works for selections that aren't directly tied to a store property (e.g. they are the result of some RxJS computations). It also works for cases where the initial value of the key is dynamic, e.g. a value that's undefined until an API call fills it in or similar.

As you have noted, this essentially boils down to doing filters in several places, which can be verbose. However in your example, you could implement it more cleanly using select$ and a Transformer:

Some Utils file:

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>;

Upvotes: 4

Related Questions