I am setting up NgRx in an Angular 16 project using Standalone components. I'm going crazy trying to have a piece of state automatically update the component after an API call is made.
I have defined actions:
export const SchoolApiActions = createActionGroup({
source: 'Schools API',
events: {
getSchoolPreviews: emptyProps(),
getSchoolPreviewsSuccess: props<{ schoolPreviews: SchoolPreview[] }>(),
getSchoolPreviewsFail: props<{ error: any }>(),
},
});
I have an effect set up that will trigger when the getSchoolPreviews
action is dispatched:
loadSchoolPreviews$ = createEffect(() => {
return this.actions$.pipe(
ofType(SchoolApiActions.getSchoolPreviews),
switchMap(() => {
return this.schoolsService.getSchoolPreviews().pipe(
map((schoolPreviews) =>
SchoolApiActions.getSchoolPreviewsSuccess({
schoolPreviews,
})
),
catchError((error) =>
of(SchoolApiActions.getSchoolPreviewsFail({ error }))
)
);
})
);
});
Finally, I have a reducer and a selector to modify the state and provide a slice of state:
export interface SchoolsState {
schoolPreviews: SchoolPreview[];
loading: boolean;
error: any;
}
export const initialState: SchoolsState = {
schoolPreviews: [],
loading: false,
error: null,
};
export const schoolsReducer = createReducer(
initialState,
// School Previews
on(SchoolApiActions.getSchoolPreviews, (state) => ({
...state,
loading: true,
})),
on(SchoolApiActions.getSchoolPreviewsSuccess, (state, payload) => {
return {
...state,
schoolPreviews: [...payload.schoolPreviews],
loading: false,
};
}),
on(SchoolApiActions.getSchoolPreviewsFail, (state, { error }) => ({
...state,
loading: false,
error,
})),
);
export const getSchoolPreviews = (state: SchoolsState) => state.schoolPreviews;
In the component, I then have the following setup in my onInit
:
ngOnInit() {
this.schoolPreviews$ = this.store.select(getSchoolPreviews);
this.store.dispatch(SchoolApiActions.getSchoolPreviews());
}
And in my template, I am using an async pipe operator to subscribe to the state:
<div class="col-4 mt-4">
<select class="form-select" aria-label="School select" (change)="selectSchool($event)">
<option selected [value]="-1">Select a School</option>
<option *ngFor="let schoolPreview of schoolPreviews$ | async" [value]="schoolPreview.id">
{{ schoolPreview.name }}
</option>
</select>
</div>
I have validated that the action/effect is getting triggered, the API call is successful, and the state is updated via the Redux dev tools.
But no matter what I do, I cannot get the async pipe to show the updated state. It fires once, then never again. I've tried changing the change detection strategy, changing how I am setting up the new array in the state, and nothing has made any difference.
Am I missing something simple? Why is the state change not reflected?
After going through the code more carefully, I realized I was missing a feature selector. Modifying my selectors as seen below fixed it.
export const SCHOOLS_FEATURE_KEY = 'schools';
export const schoolsFeatureState =
createFeatureSelector<SchoolsState>(SCHOOLS_FEATURE_KEY);
export const getSchoolPreviews = createSelector(
schoolsFeatureState,
(state) => state.schoolPreviews
);