angularngrxngrx-storengrx-effectsangular-standalone-components

NgRx does not trigger selector after state change


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?


Solution

  • 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
    );