宣布 NgRx v16:与 Angular Signals、Functional Effects、Standalone Schematics 等集成!

2025-06-07

宣布 NgRx v16:与 Angular Signals、Functional Effects、Standalone Schematics 等集成!

我们很高兴地宣布 NgRx 框架的最新主要版本,其中包含一些令人兴奋的新功能、错误修复和其他更新。


NgRx 🤝 信号

作为 v16 版本的一部分,Angular 引入了一种由信号驱动的全新反应式模型。信号 API 提供了一种简单的状态管理方式,并将在未来显著提升 Angular 应用程序的渲染性能。

在 v16 中,NgRx Store 新增了一个方法 ,selectSignal用于与 Angular Signals 集成。该方法接受一个选择器作为输入参数,并返回所选状态切片的信号。它的签名与 类似select,但与 不同selectselectSignal它返回的是信号而不是可观察对象。

import { Component, inject } from '@angular/core';
import { NgFor } from '@angular/common';
import { Store } from '@ngrx/store';

import { selectUsers } from './users.selectors';

@Component({
  standalone: true,
  imports: [NgFor],
  template: `
    <h1>Users</h1>

    <ul>
      <li *ngFor="let user of users()">
        {{ user.name }}
      </li>
    </ul>
  `
})
export class UsersComponent {
  private readonly store = inject(Store);

  // type: Signal<User[]>
  readonly users = this.store.selectSignal(selectUsers);
}
Enter fullscreen mode Exit fullscreen mode

ComponentStore 还提供了该selectSignal方法,该方法具有两个签名。第一个签名根据提供的状态投影函数创建一个信号;第二个签名通过组合提供的信号来创建一个信号,类似于select组合提供的可观察对象的方法。

import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';

import { User } from './user.model';

type UsersState = { users: User[]; query: string };

@Injectable()
export class UsersStore extends ComponentStore<UsersState> {
  // type: Signal<User[]>
  readonly users = this.selectSignal((s) => s.users);
  // type: Signal<string>
  readonly query = this.selectSignal((s) => s.query);
  // type: Signal<User[]>
  readonly filteredUsers = this.selectSignal(
    this.users,
    this.query,
    (users, query) =>
      users.filter(({ name }) => name.includes(query))
  );
}
Enter fullscreen mode Exit fullscreen mode

与函数类似computed,该selectSignal方法也接受相等函数,如果确定两个值相等,则停止更深层依赖链的重新计算。

信号state是 ComponentStore 的另一个新增功能。除了使用 之外selectSignal,它还可以与computed函数一起使用来创建派生信号。

import { computed, Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';

import { User } from './user.model';

type UsersState = { users: User[]; query: string };

@Injectable()
export class UsersStore extends ComponentStore<UsersState> {
  readonly users = computed(() => this.state().users);
  readonly query = computed(() => this.state().query);

  readonly filteredUsers = computed(() =>
    this.users().filter(({ name }) => name.includes(this.query()))
  );
}
Enter fullscreen mode Exit fullscreen mode

功能效果🔥

过去,我们无法在类之外创建 NgRx 效果,因为 Angular 仅支持基于构造函数的依赖注入。然而,随着inject函数的出现,情况发生了变化,我们现在可以在类构造函数之外注入令牌和服务。这使得我们能够将 NgRx 效果定义为效果类之外的函数。

让我们看看实际的功能效果!

要创建函数式效果,请将相应functional: true标志添加到效果配置中。然后,要将服务注入到效果中,请使用inject相应函数。

import { inject } from '@angular/core';
import { catchError, exhaustMap, map, of, tap } from 'rxjs';
import { Actions, createEffect, ofType } from '@ngrx/effects';

import { UsersService } from './users.service';
import { UsersPageActions } from './users-page.actions';
import { UsersApiActions } from './users-api.actions';

export const loadUsers = createEffect(
  (
    actions$ = inject(Actions),
    usersService = inject(UsersService)
  ) => {
    return actions$.pipe(
      ofType(UsersPageActions.opened),
      exhaustMap(() =>
        usersService.getAll().pipe(
          map((users) =>
            UsersApiActions.usersLoadedSuccess({ users })
          ),
          catchError((error) =>
            of(UsersApiActions.usersLoadedFailure({ error }))
          )
        )
      )
    );
  },
  { functional: true }
);

export const displayErrorAlert = createEffect(
  () => {
    return inject(Actions).pipe(
      ofType(UsersApiActions.usersLoadedFailure),
      tap(({ error }) => alert(error.message))
    );
  },
  { functional: true, dispatch: false }
);
Enter fullscreen mode Exit fullscreen mode

💡 为了便于测试,建议将所有依赖项作为 effect 函数的参数注入。当然,也可以在 effect 函数体中注入依赖项。在这种情况下,inject必须在注入上下文中调用该函数。

要注册功能效果,请将效果字典传递给provideEffects函数。

import { bootstrapApplication } from '@angular/platform-browser';
import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';

import { AppComponent } from './app.component';
import * as usersEffects from './users.effects';

bootstrapApplication(AppComponent, {
  providers: [
    provideStore(),
    provideEffects(usersEffects),
  ],
});
Enter fullscreen mode Exit fullscreen mode

💡 在基于模块的 Angular 应用程序中,可以使用EffectsModule.forRootEffectsModule.forFeature方法注册功能效果。

函数效果可以像其他函数一样进行测试。如果所有依赖项都作为效果函数的参数注入,TestBed则无需模拟依赖项。相反,可以将伪实例作为输入参数传递给函数效果。

import { of } from 'rxjs';

import { loadUsers } from './users.effects';
import { UsersService } from './users.service';
import { usersMock } from './users.mock';
import { UsersPageActions } from './users-page.actions';
import { UsersApiActions } from './users-api.actions';

it('loads users successfully', (done) => {
  const usersServiceMock = {
    getAll: () => of(usersMock),
  } as UsersService;
  const actionsMock$ = of(UsersPageActions.opened());

  loadUsers(actionsMock$, usersServiceMock).subscribe((action) => {
    expect(action).toEqual(
      UsersApiActions.usersLoadedSuccess({ users: usersMock })
    );
    done();
  });
});
Enter fullscreen mode Exit fullscreen mode

功能创建器增强功能🤩

createFeature函数现在允许使用工厂向功能对象添加额外的选择器extraSelectors

import { createFeature, createReducer } from '@ngrx/store';

import { User } from './user.model';

type State = { users: User[]; query: string };

const initialState: State = {
  users: [],
  query: '',
};

export const usersFeature = createFeature({
  name: 'users',
  reducer: createReducer(initialState, /* case reducers */),
  // 👇 adding extra selectors to the `usersFeature` object
  extraSelectors: ({ selectUsers, selectQuery }) => ({
    selectFilteredUsers: createSelector(
      selectUsers,
      selectQuery,
      (users, query) =>
        users.filter((user) => user.name.includes(query)),
    ),
  }),
});
Enter fullscreen mode Exit fullscreen mode

extraSelectors工厂还可以用于以简单而优雅的方式将派生的实体选择器添加到特征对象。

import { createFeature, createReducer } from '@ngrx/store';
import { createEntityAdapter } from '@ngrx/entity';

import { User } from './user.model';

const adapter = createEntityAdapter<User>();
const initialState = adapter.getInitialState();

export const usersFeature = createFeature({
  name: 'users',
  reducer: createReducer(initialState, /* case reducers */),
  // 👇 adding entity selectors to the `usersFeature` object
  extraSelectors: ({ selectUsersState }) =>
    adapter.getSelectors(selectUsersState)
});
Enter fullscreen mode Exit fullscreen mode

特别感谢Armen Vardanyan为此功能编写文档!


行动小组创建者改进👌

到目前为止,我们收到了关于该函数的大量反馈createActionGroup。它提供了更好的开发者体验,并显著减少了操作文件中的代码行数。但是,当事件名称以标题大小写格式定义时,搜索未使用的操作创建器可能会很困难,因为它们的名称是通过驼峰式命名事件名称自动生成的。

在 v16 中,该createActionGroup函数还提供了以驼峰格式定义事件名称的功能,因此动作创建器将具有与事件相同的名称。这使得在代码库中搜索它们的用法更加容易。

import { createActionGroup } from '@ngrx/store';

import { User } from './user.model';

export const UsersApiActions = createActionGroup({
  name: 'Users API',
  events: {
    usersLoadedSuccess: props<{ users: User[] }>(),
    usersLoadedFailure: props<{ errorMsg: string }>(),
  },
});

// generated action creators:
const {
  usersLoadedSuccess, // type: "[Users API] usersLoadedSuccess"
  usersLoadedFailure, // type: "[Users API] usersLoadedFailure"
} = UsersApiActions;
Enter fullscreen mode Exit fullscreen mode

增强的原理图和独立支持🚀

在最近的几个主要版本中,NgRx 框架添加了许多增强功能和新功能。然而,该@ngrx/schematics软件包尚未收到重大更新。

在版本 16 中,NgRx Schematics 得到了改进,现在使用最新的功能,例如createFeature和,感谢Marco EndrescreateActionGroup的贡献

但这还不是全部!除了这些改进之外,版本 16 还引入了使用ng add命令将 NgRx 包添加到独立 Angular 应用程序的功能。例如,我们现在可以@ngrx/store通过运行以下命令将具有初始配置的包添加到独立 Angular 应用程序中:

ng add @ngrx/store
Enter fullscreen mode Exit fullscreen mode

类似地,我们可以使用命令将其他 NgRx 包(例如@ngrx/effects@ngrx/store-devtools和)添加@ngrx/router-store到独立应用程序中ng add

非常感谢Dmytro Mezhenskyi为此功能做出的贡献!


简化的视图模型选择器🪄

createSelector函数现在接受一个选择器字典作为输入,并返回由提供的选择器选择的状态切片字典。此功能通过减少投影函数中的重复代码,简化了视图模型选择器的创建。

import { createSelector } from '@ngrx/store';

// before:
export const selectUsersPageViewModel = createSelector(
  selectFilteredUsers,
  selectActiveUser,
  selectQuery,
  selectIsLoading,
  (users, activeUser, query, isLoading) => ({
    users,
    activeUser,
    query,
    isLoading,
  })
);

// after:
export const selectUsersPageViewModel = createSelector({
  users: selectFilteredUsers,
  activeUser: selectActiveUser,
  query: selectQuery,
  isLoading: selectIsLoading,
});
Enter fullscreen mode Exit fullscreen mode

新的 tapResponse 签名 😎

tapResponse操作符拥有一个新的签名,允许我们将观察者对象作为输入参数与finalize回调函数一起提供。使用这个新的签名,我们可以简化代码并使其更具可读性。

import { inject, Injectable } from '@angular/core';
import { exhaustMap } from 'rxjs';
import { ComponentStore, tapResponse } from '@ngrx/component-store';

import { UsersService } from './users.service';
import { User } from './user.model';

type UsersState = { users: User[]; isLoading: boolean };

@Injectable()
class UsersStore extends ComponentStore<UsersState> {
  private readonly usersService = inject(UsersService);

  readonly loadUsers = this.effect<void>(
    exhaustMap(() => {
      this.patchState({ isLoading: true });

      return this.usersService.getAll().pipe(
        tapResponse({
          next: (users) => this.patchState({ users }),
          error: console.error,
          // 👇 set loading to false on complete or error
          finalize: () => this.patchState({ isLoading: false }),
        })
      );
    })
  );
}
Enter fullscreen mode Exit fullscreen mode

独立组件 API 😍

LetDirectivePushPipe在版本 16 中是独立的,因此我们可以直接将它们添加到imports独立组件或 NgModule 的 中,而不必使用LetModulePushModule

import { Component } from '@angular/core';
import { LetDirective, PushPipe } from '@ngrx/component';

@Component({
  // ... other metadata
  standalone: true,
  imports: [
    // ... other imports
    LetDirective,
    PushPipe,
  ],
})
export class UsersComponent {}
Enter fullscreen mode Exit fullscreen mode

我们要感谢Stefanos Lignos为此功能做出的贡献!


新的路由器选择器🤘

我们添加了一个新的路由器选择器,可以按名称选择路由数据属性。

import { createSelector } from '@ngrx/store';
import { getRouterSelectors } from '@ngrx/router-store';

const { selectRouteDataParam } = getRouterSelectors();

export const selectFullNameFromRoute = createSelector(
  selectRouteDataParam('firstName'),
  selectRouteDataParam('lastName'),
  (firstName, lastName) => `${firstName} ${lastName}`
);
Enter fullscreen mode Exit fullscreen mode

非常感谢Samuel Fernández为此功能做出的贡献!


NgRx 信号存储

我们最近为一个新的状态管理解决方案提交了一份RFC,该方案将为 Angular Signals 提供一流的支持。SignalStore API 的设计仍在考虑中,我们期待听到您的想法!如果您感兴趣,请浏览并告诉我们您的想法。


弃用和重大变更💥

此版本包含错误修复、弃用功能和重大变更。对于大多数弃用功能或重大变更,我们提供了迁移功能,该功能会在您将应用升级到最新版本时自动运行。

请参阅版本 16 迁移指南,获取有关迁移到最新版本的完整信息。完整的变更日志可在我们的 GitHub 代码库中找到。


升级到 NgRx 16

要开始使用 NgRx 16,请确保安装以下最低版本:

  • Angular 版本 16.x
  • Angular CLI 版本 16.x
  • TypeScript 版本 5.x
  • RxJS 版本 ^6.5.x 或 ^7.5.x

NgRx 支持使用 Angular CLIng update命令来更新你的 NgRx 软件包。要将软件包更新到最新版本,请运行以下命令:

ng update @ngrx/store
Enter fullscreen mode Exit fullscreen mode

如果您的项目使用@ngrx/component-store,但没有@ngrx/store,请运行以下命令:

ng update @ngrx/component-store
Enter fullscreen mode Exit fullscreen mode

企业支持👨‍🏫

NgRx 库集合将始终保持开源、遵循 MIT 许可证并免费使用。与此同时,我们也听到一些公司表示,设计高质量的架构可能颇具挑战性,需要额外的帮助和指导。

NgRx 团队的参与成员现在为对 NgRx 研讨会、最佳实践培训、架构审查等感兴趣的人提供付费支持。

请访问我们的企业支持页面以了解更多信息。


Swag 商店和 Discord 服务器🦺

您可以通过我们的商店购买 NgRx 官方礼品!印有 NgRx 标志的 T 恤有多种尺码、材质和颜色可供选择。我们计划未来在商店中增加贴纸、磁铁等新品。立即访问我们的商店,获取您的 NgRx 礼品吧!

想要与 NgRx 社区新老成员互动的朋友们,欢迎加入我们的Discord 服务器。


为 NgRx 做贡献

我们始终致力于改进文档,并使其始终与 NgRx 框架用户保持同步。为了帮助我们,您可以开始为 NgRx 做出贡献。如果您不确定从哪里开始,请查看我们的贡献指南,并观看Jan-Niklas WortmannBrandon Roberts制作的介绍视频 ,它们可以帮助您入门。


感谢我们所有的贡献者和赞助商!

NgRx 始终是一个社区驱动的项目。设计、开发、文档和测试均在社区的帮助下完成。我们最近在 NgRx 团队页面上添加了一个社区版块,列出了所有为该平台做出贡献的人员。

我们要感谢帮助发布 NgRx 的贡献者:Andreas Pramendorfer、André Escocard、Andrés Villanueva、Armen Vardanyan、Ben Lesh、briannarenni、Cameron Maunder、coder925、CoranH、Cédric Duffournet、Daniel Sogl、Dmytro Mezhenskyi、Fabien Wautriche、Fábio Englert Moutinho、 Jerome Wirth、Jobayer Ahmed、jonz94、Jovan Mitrović、Kyler Johnson、Laforge Thomas、Manuel A Martin Callejo、Manuel Geier、Marco Endres、Michael Egger-Zikes、Mike Drakoulelis、naticaceres、Samruddhi Khandale、Samuel Fernández、Soumya Kushwaha、Stefanos Lignos、Taylor Ackley、Thomas Waldbillig、扎卡里亚-巴厘岛、兹巴尔布托。

如果您有兴趣贡献代码,请访问我们的GitHub页面,浏览我们的未解决问题,其中一些问题已标记为专门针对新贡献者。我们还在 GitHub 上积极讨论新功能和增强功能。

我们要衷心感谢我们的银牌赞助商Nx!Nx 长期以来一直大力推广 NgRx 作为构建大型 Angular 应用程序的工具,并致力于支持他们所依赖的开源项目。

我们还要感谢我们的铜牌赞助商House of Angular


赞助 NgRx 💰

我们正在寻找下一位金牌赞助商,因此如果您的公司想要赞助 NgRx 的持续开发,请访问我们的GitHub 赞助商页面了解不同的赞助选项,或者直接联系我们讨论其他赞助机会。

在TwitterLinkedIn上关注我们,了解有关 NgRx 平台的最新更新。

文章来源:https://dev.to/ngrx/announcing-ngrx-v16-integration-with-angular-signals-function-effects-standalone-schematics-and-more-5gk6
PREV
使用编辑距离查找相似字符串
NEXT
选择 JavaScript 构建工具:配置还是不配置