Reputation: 101
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
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