Issue
I'm learning how to use ngrx/entity and I developed a 'simple' application that can manage some entities:
I have a list of Departments, every one of which can have one or more Areas.
So I created everything is needed to manage multiple entities and their relations:
index.ts
export const mainFeatureKey = 'main';
export interface MainState {
[fromDepartment.departmentFeatureKey]: fromDepartment.DepartmentState;
[fromArea.areaFeatureKey]: fromArea.AreaState;
[fromArea.departmentAreaFeatureKey]: fromArea.DepartmentAreaState;
}
export const reducers: ActionReducerMap<MainState> = {
[fromDepartment.departmentFeatureKey]: fromDepartment.reducer,
[fromArea.areaFeatureKey]: fromArea.reducer,
[fromArea.departmentAreaFeatureKey]: fromArea.departmentAreaReducer,
};
export const selectMainState = createFeatureSelector<MainState>(mainFeatureKey);
export const selectDepartmentState = createSelector(
selectMainState,
state => state[fromDepartment.departmentFeatureKey]
);
export const selectAreaState = createSelector(
selectMainState,
state => state[fromArea.areaFeatureKey]
);
export const selectDepartmentAreaState = createSelector(
selectMainState,
state => state[fromArea.departmentAreaFeatureKey]
);
export const selectDepartments = createSelector(selectMainState, state =>
fromDepartment.adapter.getSelectors().selectAll(state[fromDepartment.departmentFeatureKey])
);
export const selectSelectedDepartment = createSelector(
selectDepartmentState,
selectDepartments,
(departmentState, departments) => {
return departments.find(c => c.id === departmentState.selectedCoxtextId);
}
);
export const selectAreas = createSelector(selectMainState, state =>
fromArea.adapter.getSelectors().selectAll(state[fromArea.areaFeatureKey])
);
export const selectSelectedArea = createSelector(
selectAreaState,
selectAreas,
(areaState, areas) => {
return areas.find(a => a.id === areaState.selectedAreaId);
}
);
export const selectDepartmentAreas = createSelector(
selectDepartmentAreaState,
selectSelectedDepartment,
selectAreas,
(state, selectedDepartment, areas) => {
const departmentArea = fromArea.departmentAreaAdapter
.getSelectors()
.selectAll(state)
.find(ca => ca.departmentId === selectedDepartment?.id);
return areas.filter(a => departmentArea?.areaIds.includes(a.id));
}
);
department.reducer.ts
export const departmentFeatureKey = 'departments';
export interface DepartmentState extends EntityState<Department> {
selectedCoxtextId: number | undefined;
}
// In questo caso sarebbe facoltativo perché l'identificativo di Department si chiama già id
const selectId = (department: Department) => department.id;
const sortByid = (a: Department, b: Department) => a.id - b.id;
export const adapter = createEntityAdapter<Department>({ selectId, sortComparer: sortByid });
const initialState: DepartmentState = adapter.getInitialState({
selectedCoxtextId: undefined,
});
export const reducer = createReducer(
initialState,
on(DepartmentActions.saveDepartments, (state, action): DepartmentState => {
return adapter.setAll(action.departments, {
...state,
selectedCoxtextId: state.selectedCoxtextId,
});
}),
on(
DepartmentActions.selectDepartment,
(state, action): DepartmentState => ({ ...state, selectedCoxtextId: action.departmentId })
)
);
department.action.ts
export const DepartmentActions = createActionGroup({
source: 'MainModule',
events: {
'Load departments': emptyProps(),
'Save departments': props<{ departments: Department[] }>(),
'Select department': props<{ departmentId: number }>(),
},
});
department.effect.ts
@Injectable()
export class DepartmentEffects {
loadDepartment$ = createEffect(() =>
this.actions$.pipe(
ofType(DepartmentActions.loadDepartments),
concatLatestFrom(() => this.state.select(selectDepartments)),
filter(([, departments]) => departments.length === 0),
tap(() => debug('Loading departments')),
exhaustMap(() =>
this.dataService
.loadDepartments()
.pipe(map(departments => DepartmentActions.saveDepartments({ departments })))
)
)
);
constructor(
private actions$: Actions,
private state: Store<MainState>,
private dataService: DataService
) {}
}
area.reducer.ts (which also includes the department-area relation reducers)
export const areaFeatureKey = 'areas';
export interface AreaState extends EntityState<Area> {
selectedAreaId: number | undefined;
}
const sortByName = (a: Area, b: Area) => a.name.toLowerCase().localeCompare(b.name.toLowerCase());
export const adapter = createEntityAdapter<Area>({ sortComparer: sortByName });
const initialState: AreaState = adapter.getInitialState({ selectedAreaId: undefined });
export const reducer = createReducer(
initialState,
on(AreaActions.saveAreas, (state, action): AreaState => {
return adapter.addMany(action.areas, { ...state, selectedAreaId: state.selectedAreaId });
})
);
// Relations
export const departmentAreaFeatureKey = 'departmentAreas';
export interface DepartmentAreaState extends EntityState<DepartmentArea> {}
export const departmentAreaAdapter = createEntityAdapter<DepartmentArea>();
export const departmentAreaReducer = createReducer(
departmentAreaAdapter.getInitialState(),
on(AreaActions.saveDepartmentAreas, (state, action): DepartmentAreaState => {
return departmentAreaAdapter.addOne(
{
id: createId(),
departmentId: action.departmentId,
areaIds: action.areas.map(a => a.id),
},
state
);
})
);
area.action.ts
export const AreaActions = createActionGroup({
source: 'MainModule',
events: {
'Load areas': props<{ departmentId: number }>(),
'Save areas': props<{ departmentId: number; areas: Area[] }>(),
'Save department areas': props<{ departmentId: number; areas: Area[] }>(),
'Select area': props<{ areaId: number }>(),
},
});
area.effect.ts
@Injectable()
export class AreaEffects {
loadAreas$ = createEffect(() =>
this.actions$.pipe(
ofType(AreaActions.loadAreas),
tap(() => debug('Loading areas')),
exhaustMap(action =>
this.dataService
.loadAreas(action.departmentId)
.pipe(map(areas => AreaActions.saveAreas({ departmentId: action.departmentId, areas })))
)
)
);
saveDepartmentAreas$ = createEffect(() =>
this.actions$.pipe(
ofType(AreaActions.saveAreas),
tap(() => debug('Saving department areas')),
map(action =>
AreaActions.saveDepartmentAreas({ departmentId: action.departmentId, areas: action.areas })
)
)
);
constructor(
private actions$: Actions,
private store: State<MainState>,
private dataService: DataService
) {}
}
When i perform the dispatch af an action (any action, even from an other feature state) it is managed twice while the actions dispatched by the effetcs are managed correctly (But they too were performed twice before I switched to exhaustMap
from mergeMap
which I don't know if is the right thing to do).
I know the problem is somewhere in the reducers o the selectors, because when there were only the departments they were triggered once, but I can understand where.
This is the console log:
BUT, the redux log is very different:
Before you ask:
- Yes, I checked if the
dispatch()
is executed multiple times. It isn't. - No, the dispatch is not inside a subscribe or a loop that can cause it to be triggered multiple times.
- Observables generated from selector ar managed only with async pipe inside the template.
Solution
After 3 days of debbugging without finding anything, today i noticed someting in the stack trace of a breakpoint inside the global metareducer I use to log actions:
It was the dev-tool.
Commenting it out solved the problem.
This raises a number of questions, but for now I'm happy.
EDIT:
Knowing the problem I found this issue on github, but it doesn't seem to be solved.
However, ther is a fix that (as for versione 17) seems to solve the problem:
When using DevTools add this in the providers array of the module that host the StoreModule.forRoot():
{ provide: State, useValue: null }
Remember to remove this for production environment!
Answered By - Federico Xella
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.