Angular13报StoreModule.forRoot() called twice错误如何解决
在Angular 13项目中为实现布局自定义功能,在CoreModule中接入@ngrx/store,项目模块结构如下:
除AppModule外,项目包含以下业务模块:
- CoreModule
- AdminModule
布局相关代码存放在CoreModule中,@ngrx/store的相关实现也位于CoreModule内,同时CoreModule被AdminModule导入。
相关代码片段
state.ts
import {UiState} from './ui/state'; export interface AppState { auth: any; ui: UiState; }
ui/reducer.ts
import { NAVBAR_DARK_VARIANTS, NAVBAR_LIGHT_VARIANTS, SIDEBAR_DARK_SKINS, SIDEBAR_LIGHT_SKINS } from '../../utils/themes'; import * as Actions from './actions'; import {UiAction} from './actions'; import initialState, {UiState} from './state'; export function uiReducer(state: UiState = initialState, action: UiAction) { switch (action.type) { case Actions.TOGGLE_SIDEBAR_MENU: return { ...state, menuSidebarCollapsed: !state.menuSidebarCollapsed }; case Actions.TOGGLE_CONTROL_SIDEBAR: return { ...state, controlSidebarCollapsed: !state.controlSidebarCollapsed }; case Actions.TOGGLE_DARK_MODE: let variant: string; let skin: string; if (state.darkMode) { variant = NAVBAR_LIGHT_VARIANTS[0].value; skin = SIDEBAR_LIGHT_SKINS[0].value; } else { variant = NAVBAR_DARK_VARIANTS[0].value; skin = SIDEBAR_DARK_SKINS[0].value; } return { ...state, navbarVariant: variant, sidebarSkin: skin, darkMode: !state.darkMode }; case Actions.SET_NAVBAR_VARIANT: let navbarVariant: string; if (state.darkMode) { navbarVariant = action.payload || NAVBAR_DARK_VARIANTS[0].value; } else { navbarVariant = action.payload || NAVBAR_LIGHT_VARIANTS[0].value; } return { ...state, navbarVariant }; case Actions.SET_SIDEBAR_SKIN: let sidebarSkin: string; if (state.darkMode) { sidebarSkin = action.payload || SIDEBAR_DARK_SKINS[0].value; } else { sidebarSkin = action.payload || SIDEBAR_LIGHT_SKINS[0].value; } return { ...state, sidebarSkin }; default: return state; } }
core.module.ts
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { AdminHeaderComponent } from './components/layouts/admin-header/admin-header.component'; import { AdminSidebarComponent } from './components/layouts/admin-sidebar/admin-sidebar.component'; import { PageNotFoundComponent } from './components/errors/page-not-found/page-not-found.component'; import { StoreModule } from '@ngrx/store'; import { authReducer } from './store/auth/reducer'; import { uiReducer } from './store/ui/reducer'; @NgModule({ declarations: [ AdminHeaderComponent, AdminSidebarComponent, PageNotFoundComponent, ], imports: [ CommonModule, RouterModule, FormsModule, ReactiveFormsModule, StoreModule.forRoot({ui: uiReducer}), ], exports: [ AdminHeaderComponent, AdminSidebarComponent, FooterComponent ] }) export class CoreModule { }
app.module.ts
import { ErrorHandler, NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; import { CommonModule } from '@angular/common'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AppComponent } from './app.component'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { CoreModule } from './core/core.module'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, BrowserAnimationsModule, CommonModule, HttpClientModule, CoreModule, FormsModule, ReactiveFormsModule ], providers: [ ], bootstrap: [AppComponent] }) export class AppModule { }
app-routing.module.ts
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { PageNotFoundComponent } from 'src/app/core/components/errors/page-not-found/page-not-found.component'; const routes: Routes = [ { path: '', redirectTo: 'admin', pathMatch: 'full' }, { path: 'page-not-found', component: PageNotFoundComponent, data: {breadcrumb: 'Page Not Found'} }, { path: 'admin', loadChildren: () => import('./features/admin/admin.module').then(m => m.AdminModule) }, { path: '**', redirectTo: 'page-not-found', pathMatch: 'full' } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
报错信息
启动Angular应用时抛出如下错误:
ERROR Error: Uncaught (in promise): TypeError: StoreModule.forRoot() called twice. Feature modules should use StoreModule.forFeature() instead.
TypeError: StoreModule.forRoot() called twice. Feature modules should use StoreModule.forFeature() instead.
at Object._provideForRootGuard [as useFactory] (ngrx-store.mjs:1294:1)
注释掉CoreModule中的StoreModule.forRoot({ui: uiReducer})代码后错误消失。
@ngrx/store 明确规定StoreModule.forRoot()只能在整个应用的根模块(即AppModule)中调用一次,作用是初始化全局Store、注册根级reducer。所有非根的特性模块(包括CoreModule、AdminModule这类被导入的模块、懒加载模块)都不能调用forRoot()方法注册Store,否则会触发重复初始化的守卫报错。
当前写法把forRoot()放在了CoreModule中,而CoreModule会被AppModule导入、同时还被懒加载的AdminModule导入,相当于多次执行了Store根初始化逻辑,直接触发报错。
按照NgRx的模块注册规则调整代码即可,分两步:
- 把
StoreModule.forRoot()的调用从CoreModule移到根模块AppModule中,全局只保留这一处forRoot调用 - CoreModule中需要注册的状态片段,改用
StoreModule.forFeature()注册特性状态
修改后的代码示例
第一步:修改core.module.ts
把原来的StoreModule.forRoot()替换为forFeature()注册ui状态:
// 其他导入保持不变 @NgModule({ declarations: [ AdminHeaderComponent, AdminSidebarComponent, PageNotFoundComponent, ], imports: [ CommonModule, RouterModule, FormsModule, ReactiveFormsModule, // 特性模块用forFeature注册自己的状态片段 StoreModule.forFeature('ui', uiReducer), // 如果auth状态属于CoreModule的特性状态,同样用forFeature注册 // StoreModule.forFeature('auth', authReducer) ], exports: [ AdminHeaderComponent, AdminSidebarComponent, FooterComponent ] }) export class CoreModule { }
第二步:修改app.module.ts
在根模块中调用一次StoreModule.forRoot()完成全局Store初始化,全局需要的根reducer、元reducer、runtime检查等配置都在这里传入:
// 其他导入保持不变 import { StoreModule } from '@ngrx/store'; // 如果有authReducer需要作为根状态注册可以在这里引入,否则forRoot可以传空对象 @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, BrowserAnimationsModule, CommonModule, HttpClientModule, // 根模块全局只调用一次forRoot初始化Store StoreModule.forRoot({ // 根级reducer在这里注册,没有的话可以传空对象 {} // auth: authReducer }), CoreModule, FormsModule, ReactiveFormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
修改完成后重新启动项目,报错就会消失,状态功能也能正常使用。
补充说明:CoreModule的常规设计是只在AppModule中导入一次,不要在懒加载的特性模块(比如AdminModule)中重复导入CoreModule,否则不仅会重复注册Store相关服务,还会导致CoreModule中声明的服务、组件重复实例化,引发其他隐性问题。
内容的提问来源于stack exchange,提问作者Ayobamilaye

