NgRx Effects无响应问题排查及代码规范咨询
First: Fix the Effects Not Running Issue
The root cause your Effects aren't executing is that you haven't registered them in your AppModule. NgRx Effects require EffectsModule to be imported and configured to run. Here's how to fix this first:
Step 1: Update AppModule.ts
Add EffectsModule.forRoot([CarEffects]) to your imports array:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { CarComponent } from './car/car.component'; import { StoreModule } from '@ngrx/store'; import { carReducer } from './car/car.reducer'; import { HttpClientModule } from '@angular/common/http'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; // Add this import import { EffectsModule } from '@ngrx/effects'; // Import your Effects class import { CarEffects } from './car/car.effects'; @NgModule({ declarations: [ AppComponent, CarComponent ], imports: [ BrowserModule, StoreModule.forRoot({ car: carReducer }), HttpClientModule, StoreDevtoolsModule.instrument({ maxAge: 5 }), // Register your Effects here EffectsModule.forRoot([CarEffects]) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Step 2: Fix Other Breaking Issues in Your Code
Even after registering Effects, there are other bugs that will prevent your code from working correctly:
1. Mismatched AppState and Reducer Type
Your carReducer returns a Car[] array, but your AppState defines car as a single Car object. This causes type errors and will break Store functionality. Update AppState to match:
import { Car } from './../../models/car.model'; export interface AppState { readonly car: Car[]; // Change from Car to Car[] }
2. Broken Effects Data Stream
In your carAdd Effect, the map operator doesn't return an Action, which breaks the Observable stream. If you don't need to dispatch an Action after the POST request, use tap instead of map (and add dispatch: false to the @Effect() decorator). Or return a success Action if you want to handle that case:
// Option 1: No success Action needed @Effect({ dispatch: false }) // Add dispatch: false carAdd = this.actions$.pipe( ofType(CarActions.ADD_CAR), switchMap((carData: CarActions.AddCar) => { console.log('true'); return this.http.post('http://myapi.com/api', { brand: carData.payload.brand, model: carData.payload.model }).pipe( tap(resData => { // Use tap instead of map for side effects localStorage.setItem('test', 'asdasdasd'); }), catchError(errorRes => { console.log(errorRes); const errorMessage = 'An unknown error occurred!'; if (!errorRes.error || !errorRes.error.error) { return of(new CarActions.AddCarFail(errorMessage)); } console.log(errorRes.error.error.message); return of(new CarActions.AddCarFail(errorRes.error.error.message)); }) ); }) ); // Option 2: Add a success Action (better for tracking state) // First define an ADD_CAR_SUCCESS action in your actions file, then: @Effect() carAdd = this.actions$.pipe( ofType(CarActions.ADD_CAR), switchMap((carData: CarActions.AddCar) => { console.log('true'); return this.http.post('http://myapi.com/api', { brand: carData.payload.brand, model: carData.payload.model }).pipe( map(resData => { localStorage.setItem('test', 'asdasdasd'); return new CarActions.AddCarSuccess(resData); // Return success Action }), catchError(errorRes => { // ... existing error handling }) ); }) );
3. Reducer Error Handling Bug
Your ADD_CAR_FAIL case returns an object instead of an array, which will corrupt your state (since initial state is an array). Update the reducer to handle errors properly (you should add an error property to your state type first):
// First update your state type to include error export interface CarState { cars: Car[]; error: string | null; } const initialState: CarState = { cars: [], error: null }; export function carReducer(state = initialState, action: CarActions.Actions): CarState { switch (action.type) { case CarActions.ADD_CAR: return { ...state, cars: [...state.cars, action.payload], error: null }; case CarActions.ADD_CAR_FAIL: return { ...state, error: action.payload }; default: return state; } } // Then update AppState to use the new CarState export interface AppState { readonly car: CarState; }
Best Practices for Your NgRx Code
Here are some improvements to align with NgRx best practices:
Separate State into Feature Modules
As your app grows, move car-related state (reducers, actions, effects) into a feature module usingStoreModule.forFeature()andEffectsModule.forFeature()instead offorRoot(). This keeps your code organized.Use Action Creators
Instead of manually creating Action classes, use NgRx'screateActionutility (available in NgRx 8+) for cleaner, more concise action definitions:import { createAction, props } from '@ngrx/store'; import { Car } from './../../models/car.model'; export const addCar = createAction( '[CAR] Add', props<{ payload: Car }>() ); export const addCarFail = createAction( '[CAR] Fail', props<{ payload: string }>() ); // Add a success action if needed export const addCarSuccess = createAction( '[CAR] Add Success', props<{ payload: any }>() // Replace "any" with your API response type );Use createReducer for Reducers
ThecreateReducerutility reduces boilerplate and makes reducers easier to read:import { createReducer, on } from '@ngrx/store'; import * as CarActions from './car.actions'; const initialState: CarState = { cars: [], error: null }; export const carReducer = createReducer( initialState, on(CarActions.addCar, (state, { payload }) => ({ ...state, cars: [...state.cars, payload], error: null })), on(CarActions.addCarFail, (state, { payload }) => ({ ...state, error: payload })) );Strongly Type Effects
Use typed Actions to avoid type casting in Effects. With action creators, you can useofType(addCar)directly without casting:@Effect() carAdd = this.actions$.pipe( ofType(CarActions.addCar), switchMap(({ payload }) => { // No need to cast, payload is typed return this.http.post('http://myapi.com/api', payload).pipe( map(res => CarActions.addCarSuccess(res)), catchError(err => of(CarActions.addCarFail(err.message))) ); }) );Avoid Side Effects in Reducers
Your current reducer is pure (good!), but make sure you never do async operations, API calls, or modify localStorage in reducers—those belong in Effects.Use Selectors
Create typed selectors to access state in components, instead of directly accessing the Store. This makes your code more maintainable and reduces duplication:import { createFeatureSelector, createSelector } from '@ngrx/store'; import { CarState } from './car.reducer'; const selectCarState = createFeatureSelector<CarState>('car'); export const selectCars = createSelector( selectCarState, (state) => state.cars ); export const selectCarError = createSelector( selectCarState, (state) => state.error );
内容的提问来源于stack exchange,提问作者PawelC

