I am having troubles understanding how NgRx works. I have a user list that is loaded via a service from a Firebase store like this:
getAllUsers():Observable<object>{
console.log('getAllUsers');
return this.afs.collection('users', (ref:any) => ref.where("role", '!=', 'admin')).valueChanges();
}
Then I have a Store setup in the module that is storing the user list, which works great and loads the user list once as the admin logs into the app, as it is dispatched in the dashboard which is the container of the admin router-outlet. A part from that the admin can click on every user and update their information which fires this function from the services that is not connected to the store:
async updateUserData(data:any, id:any){
await this.afs.collection('users').doc(id).update(data);
}
I was expecting when user data is updated the stored list of users to not update until it is downloaded again, but it updates accordingly. How is this happening? I am logging the getAllUsers function and it is not called again. Instead the action: LoadUsersSuccess is called when the updateUserData is called, but they are not connected anywhere in the effects. I really wish to know how is this happening and I can't find the answer anywhere. This is my store setup-
actions.ts:
import { Action } from "@ngrx/store";
export enum AdminActionTypes{
LOAD_USERS = '[ADMIN] Load Users',
LOAD_USERS_SUCCESS = '[ADMIN] Load Users Success',
LOAD_USERS_FAILURE = '[ADMIN] Load Users Failure',
}
export class LoadUsersAction implements Action {
readonly type = AdminActionTypes.LOAD_USERS;
}
export class LoadUsersSuccessAction implements Action {
readonly type = AdminActionTypes.LOAD_USERS_SUCCESS;
constructor(public payload: any){}
}
export class LoadUsersFailureAction implements Action {
readonly type = AdminActionTypes.LOAD_USERS_FAILURE;
constructor(public payload: Error){}
}
export type AdminAction = LoadUsersAction | LoadUsersFailureAction | LoadUsersSuccessAction;
reducer.ts:
import { AdminActionTypes, AdminAction} from './admin.actions';
import { createFeature, createFeatureSelector, createSelector } from '@ngrx/store';
export interface AdminState {
list: any[],
loading: boolean,
error: Error
}
const initialState: AdminState = {
list:[],
loading:false,
error: {name: "", message: ""}
}
export function AdminReducer(
state: AdminState = initialState,
action: AdminAction,
){
switch (action.type){
case AdminActionTypes.LOAD_USERS:
return {...state, loading: true};
case AdminActionTypes.LOAD_USERS_SUCCESS:
return {...state, list:action.payload, loading:false};
case AdminActionTypes.LOAD_USERS_FAILURE:
return {...state, error:action.payload, loading:false};
default:
return state;
}
}
export const getAdminState = createFeatureSelector<AdminState>('admin');
export const loadUsers = createSelector(getAdminState, (state: AdminState) => state.loading);
export const loadUsersSuccess = createSelector(getAdminState, (state: AdminState) => state.list);
export const loadUsersFailure = createSelector(getAdminState, (state: AdminState) => state.error);
effects.ts:
@Injectable()
export class AdminEffects {
loadUsers$ = createEffect(
() => this.actions$
.pipe(
ofType<LoadUsersAction>(AdminActionTypes.LOAD_USERS),
mergeMap(
() => this.adminService.getAllUsers()
.pipe(
map(data => new LoadUsersSuccessAction(data)),
catchError(error => of (new LoadUsersFailureAction(error)))
),
),
),
)
constructor(
private actions$: Actions,
private adminService: AdminService
){}
}
I think this happens because you return an Observable
from getAllUsers
that emits a value when there is a change in the Firebase collection. Then the returned Observable
is internally chained with the Observable of the loadUsers$
effect by mergeMap
, which is mapped to the LoadUsersSuccessAction
action when a new value comes from the source Observable (this.adminService.getAllUsers()
), and then the LoadUsersSuccessAction
is dispatched after that.
So if you want to change this behavior, you need to complete the getAllUsers()
observable as soon as the first value comes from the Firebase collection, like the following:
getAllUsers(): Observable<object> {
console.log('getAllUsers');
return this.afs
.collection('users', (ref: any) => ref.where('role', '!=', 'admin'))
.valueChanges()
.pipe(take(1));
}
OR You need to extract the logic (which gets the valuesChanges
from firebase) from the effect and use it elsewhere.