You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

迁移NgRx Effects后createEffect回调提前执行致Angular DI依赖未定义

NgRx Effects迁移后单元测试依赖注入异常问题排查与解决

问题描述

迁移NgRx Effects类到新仓库后,单元测试出现依赖注入(DI)异常:类中fetchSingleStuff$属性通过createEffect定义的回调函数,在构造函数执行前就已运行,导致注入的actions$和MyService服务未定义。旧仓库中该回调会在构造函数执行后运行,依赖均正常。

相关代码与输出

简化Effects类代码

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { MyService } from '../services/my.service';
import { stuffActions } from './stuff.actions';
import { map, switchMap } from 'rxjs/operators';

@Injectable()
export class StuffEffects {
  fetchSingleStuff$ = createEffect(() => {
    console.log('回调执行,actions$:', this.actions$, 'MyService:', this.myService);
    return this.actions$.pipe(
      ofType(stuffActions.fetchSingleStuff),
      switchMap(({ id }) => this.myService.getStuff(id)),
      map(stuff => stuffActions.fetchSingleStuffSuccess({ stuff }))
    );
  });

  constructor(
    private actions$: Actions,
    private myService: MyService
  ) {
    console.log('构造函数执行');
  }
}

单元测试代码

import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Observable } from 'rxjs';
import { StuffEffects } from './stuff.effects';
import { MyService } from '../services/my.service';
import { MockProvider } from 'ng-mocks';

describe('StuffEffects', () => {
  let effects: StuffEffects;
  let actions$: Observable<any>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        StuffEffects,
        provideMockActions(() => actions$),
        MockProvider(MyService)
      ]
    });

    effects = TestBed.inject(StuffEffects);
  });

  it('should be created', () => {
    expect(effects).toBeTruthy();
  });
});

新旧仓库控制台输出对比

  • 旧仓库输出:

    构造函数执行
    回调执行,actions$: [ActionsObservable], MyService: [MyService]

  • 新仓库输出:

    回调执行,actions$: undefined, MyService: undefined
    构造函数执行

新旧tsconfig配置差异

旧仓库(关键部分)

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": false,
    // 其他配置...
  }
}

新仓库(关键部分)

{
  "compilerOptions": {
    "target": "ES2022",
    "useDefineForClassFields": true,
    // 其他配置...
  }
}

问题原因

核心差异在于TypeScript的useDefineForClassFields配置:

  • 当该配置设为true时,类属性会遵循ECMAScript标准的类字段定义逻辑,在构造函数执行前完成求值。此时createEffect的回调会提前运行,而this还未完成依赖注入,导致依赖对象未定义。
  • 旧仓库该配置为false,类属性初始化逻辑嵌入构造函数执行流程,因此回调会在依赖注入完成后才运行。

解决办法

方案1:对齐tsconfig配置

将新仓库的useDefineForClassFields设为false,与旧仓库保持一致:

{
  "compilerOptions": {
    "useDefineForClassFields": false,
    // 其他配置...
  }
}

方案2:改用函数式Effect写法(推荐)

使用NgRx 14+推荐的函数式模式,通过inject直接获取依赖,避免依赖类的this:

@Injectable()
export class StuffEffects {
  fetchSingleStuff$ = createEffect(
    (actions$ = inject(Actions), myService = inject(MyService)) => {
      console.log('回调执行,actions$:', actions$, 'MyService:', myService);
      return actions$.pipe(
        ofType(stuffActions.fetchSingleStuff),
        switchMap(({ id }) => myService.getStuff(id)),
        map(stuff => stuffActions.fetchSingleStuffSuccess({ stuff }))
      );
    },
    { functional: true }
  );

  constructor() {
    console.log('构造函数执行');
  }
}

方案3:延迟属性初始化

将Effect属性改为getter,延迟到构造函数执行后才触发初始化:

@Injectable()
export class StuffEffects {
  get fetchSingleStuff$() {
    return createEffect(() => {
      console.log('回调执行,actions$:', this.actions$, 'MyService:', this.myService);
      return this.actions$.pipe(
        ofType(stuffActions.fetchSingleStuff),
        switchMap(({ id }) => this.myService.getStuff(id)),
        map(stuff => stuffActions.fetchSingleStuffSuccess({ stuff }))
      );
    });
  }

  constructor(
    private actions$: Actions,
    private myService: MyService
  ) {
    console.log('构造函数执行');
  }
}

内容的提问来源于stack exchange,提问作者TabsNotSpaces

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 10:55:22