Nikola
Nikola

Reputation: 101

ngrx/store state updates from within a selector

I have the following normalized state structure:

auth: {
    user: string; // this is the current logged in uid
};
entities: {
    users: { [key: string]: User } // normalized user entities (including the current user)
};

User interface (note the friends array):

{
    _id: string;
    name: string;
    friends: Array<{
        status: string;
        user: string | User; // uid or user object
    }>
}

in an AuthService, I have a selector for the current user:

this.user$ = Observable.combineLatest(
    store.select(state => state.auth.user),
    store.select(state => state.entities.users),
    (id, users) => id && users[id] || {}
);

in the FriendsService, I have a selector for the user's populated friends:

this.mates$ = this.authService.user$
    .withLatestFrom(
        this.store.select(state => state.entities.users),
        (user, users) =>
            (user.friends || []).map(f => {
                f.user = typeof f.user === 'string'
                    ? users[<string>f.user]
                    : users[f.user._id];
                return f;
            })
    );

The problem is that the projection fn from the mates$ selector is also modifying the state. As a result, I no longer have ids, but a whole user object in my friends array:

State WITHOUT mates$ selector:

{
    auth: {
        user: '5706a6de1fcf42ec245abeea'
    },
    entities: {
        users: {
            5706a6de1fcf42ec245abeea: {
                _id: '5706a6de1fcf42ec245abeea',
                name: 'Nikola',
                friends: [{
                    status: 'requested',
                    friend: '57224d106864441c32e6a5b6'
                }]
             }
        }
    }
}

State WITH mates$ selector:

{
    auth: {
        user: '5706a6de1fcf42ec245abeea'
    },
    entities: {
        users: {
            5706a6de1fcf42ec245abeea: {
                _id: '5706a6de1fcf42ec245abeea',
                name: 'Nikola',
                friends: [{
                    status: 'requested',
                    friend: {
                        _id: '57224d106864441c32e6a5b6',
                        name: 'Friend01'
                    }
                }]
             }
        }
    }
}

This is an unexpected behavior for me. Or maybe I am missing some reactive tutorials?

Upvotes: 2

Views: 1693

Answers (1)

maxime1992
maxime1992

Reputation: 23793

this.mates$ = this.authService.user$
  .withLatestFrom(
  this.store.select(state => state.entities.users),
  (user, users) =>
    // YOU SHOULDN'T UPDATE DATA FROM THE STORE OUTSIDE A REDUCER
    // -----------------------------
    (user.friends || []).map(f => {
      f.user = typeof f.user === 'string'
        ? users[<string>f.user]
        : users[f.user._id];
      return f;
    })
    // -----------------------------
  );

Instead, you should work on a new reference like that

this.mates$ = this.authService.user$
  .withLatestFrom(
  this.store.select(state => state.entities.users),
  (user, users) =>
    // NEW REF TO AVOID STATE MUTATION
    // -----------------------------
    (user.friends || [])
      .map(f => Object.assign({}, f, { updatedPropHere: null }))
    // -----------------------------
  );

To make sure you don't mutate data from your store, you may want to take a look into redux freeze library which will throw an error if the store is mutated.

Upvotes: 3

Related Questions