宣布 NgRx v17:介绍 NgRx 信号、操作符、性能改进、研讨会等!
我们很高兴地宣布 NgRx 框架的最新主要版本,其中包含一些令人兴奋的新功能、错误修复和其他更新。
介绍 NgRx 信号
此前,我们已为一款新的状态管理解决方案提交了一份RFC,该方案将为 Angular Signals 提供一流的响应式支持。我们很高兴能隆重推出这个@ngrx/signals
库。
NgRx Signals 库完全基于 Angular Signals 构建,支持 RxJS 互操作,并包含开箱即用的实体管理功能。非常感谢Marko Stanimirović提出这个新库,并结合 NgRx 团队其他成员和社区的反馈不断构建和改进。Signals 库开启了使用 NgRx 和 Angular Signals 构建的新一代 Angular 应用。
入门
要安装该@ngrx/signals
软件包,请使用您选择的软件包管理器:
npm install @ngrx/signals
您还可以使用以下ng add
命令:
ng add @ngrx/signals@latest
定义状态
并非每个状态都需要单独的存储。针对此用例,@ngrx/signals
我们提供了一个signalState
实用函数,可以快速创建和操作小部分状态。它可以直接在组件类、服务或独立函数中使用。
import { Component } from '@angular/core';
import { signalState, patchState } from '@ngrx/signals';
@Component({
selector: 'app-counter',
standalone: true,
template: `
Count: {{ state.count() }}
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset</button>
`,
})
export class CounterComponent {
state = signalState({ count: 0 });
increment() {
patchState(this.state, (state) => ({ count: state.count + 1 }));
}
decrement() {
patchState(this.state, (state) => ({ count: state.count - 1 }));
}
reset() {
patchState(this.state, { count: 0 });
}
}
实用patchState
函数提供了一种类型安全的方式来对状态片段执行不可变的更新。
创建商店
为了管理具有更复杂状态的大型商店,您可以使用signalStore
实用函数以及patchState
和其他函数来管理状态。
import { computed } from '@angular/core';
import { signalStore, withState } from '@ngrx/signals';
export const CounterStore = signalStore(
withState({ count: 0 })
);
该withState
函数采用存储的初始状态并定义状态的形状。
导出计算值
计算属性也可以使用该函数从存储中现有的状态中派生出来withComputed
。
import { computed } from '@angular/core';
import { signalStore, patchState, withComputed } from '@ngrx/signals';
export const CounterStore = signalStore(
withState({ count: 0 }),
withComputed(({ count }) => ({
doubleCount: computed(() => count() * 2),
})),
);
该doubleCount
属性作为存储的属性公开,可对 的更改做出反应count
。
定义存储方法
您还可以定义公开的方法,以使用明确定义的 API 对商店进行操作。
import { computed } from '@angular/core';
import { signalStore, patchState, withComputed, withMethods } from '@ngrx/signals';
export const CounterStore = signalStore(
withState({ count: 0 }),
withComputed(({ count }) => ({
doubleCount: computed(() => count() * 2),
})),
withMethods(({ count, ...store }) => ({
increment() {
patchState(store, { count: count() + 1 });
},
decrement() {
patchState(store, { count: count() - 1 });
},
}))
);
定义生命周期钩子
您还可以创建在创建和销毁存储时调用的生命周期挂钩,以初始化获取数据、更新状态等。
import { computed } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
import {
signalStore,
withState,
patchState,
withComputed,
withHooks,
withMethods,
} from '@ngrx/signals';
export const CounterStore = signalStore(
withState({ count: 0 }),
withComputed(({ count }) => ({
doubleCount: computed(() => count() * 2),
})),
withMethods(({ count, ...store }) => ({
increment() {
patchState(store, { count: count() + 1 });
},
decrement() {
patchState(store, { count: count() - 1 });
},
})),
withHooks({
onInit({ increment }) {
interval(2_000)
.pipe(takeUntilDestroyed())
.subscribe(() => increment());
},
onDestroy({ count }) {
console.log('count on destroy', count());
},
}),
);
在上面的例子中,onInit
钩子订阅了一个间隔可观察对象,并调用increment
store 上的方法每 2 秒增加一次计数。生命周期方法还可以访问注入上下文,以便使用 进行自动清理takeUntilDestroyed()
。
提供并注入 Store
要使用CounterStore
,请将其添加到providers
组件的数组中,然后使用依赖注入进行注入。
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { CounterStore } from './counter.store';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<h1>Counter (signalStore)</h1>
<p>Count: {{ store.count() }}</p>
<p>Double Count: {{ store.doubleCount() }}</p>
<button (click)="store.increment()">Increment</button>
<button (click)="store.decrement()">Decrement</button>
`,
providers: [CounterStore],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class CounterComponent {
readonly store = inject(CounterStore);
}
选择与 RxJS 互操作
RxJS 仍然是 NgRx 和 Angular 生态系统的重要组成部分,并且 NgRx Signals 包提供了可选用法,可以使用该rxMethod
函数与 RxJS 可观察对象进行交互。
该rxMethod
函数允许您在 signalStore 上定义一种方法,该方法可以接收信号或可观察对象、读取其最新值并使用可观察对象执行其他操作。
import { inject } from '@angular/core';
import { debounceTime, distinctUntilChanged, pipe, switchMap, tap } from 'rxjs';
import {
signalStore,
patchState,
withHooks,
withMethods,
withState,
} from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { tapResponse } from '@ngrx/operators';
import { User } from './user.model';
import { UsersService } from './users.service';
type State = { users: User[]; isLoading: boolean; query: string };
const initialState: State = {
users: [],
isLoading: false,
query: '',
};
export const UsersStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withMethods((store, usersService = inject(UsersService)) => ({
updateQuery(query: string) {
patchState(store, { query });
},
async loadAll() {
patchState(store, { isLoading: true });
const users = await usersService.getAll();
patchState(store, { users, isLoading: false });
},
loadByQuery: rxMethod<string>(
pipe(
debounceTime(300),
distinctUntilChanged(),
tap(() => patchState(store, { isLoading: true })),
switchMap((query) =>
usersService.getByQuery(query).pipe(
tapResponse({
next: (users) => patchState(store, { users }),
error: console.error,
finalize: () => patchState(store, { isLoading: false }),
}),
),
),
),
),
})),
withHooks({
onInit({ loadByQuery, query }) {
loadByQuery(query);
},
}),
);
上面的例子UserStore
使用rxMethod
运算符创建了一个方法,该方法根据查询字符串在商店初始化时加载用户。
然后可以UsersStore
在组件中使用它及其附加方法,提供一种干净、结构化的方式来使用信号管理状态,并结合 RxJS 可观察流的强大功能来实现异步行为。
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { SearchBoxComponent } from './ui/search-box.component';
import { UserListComponent } from './ui/user-list.component';
import { UsersStore } from './users.store';
@Component({
selector: 'app-users',
standalone: true,
imports: [SearchBoxComponent, UserListComponent],
template: `
<h1>Users (RxJS Integration)</h1>
<app-search-box
[query]="store.query()"
(queryChange)="store.updateQuery($event)"
/>
<app-user-list [users]="store.users()" [isLoading]="store.isLoading()" />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class UsersComponent {
readonly store = inject(UsersStore);
}
该@ngrx/signals
软件包还包括管理实体、组成共享功能、共享全局存储的功能,并且可以扩展到许多不同的用例。
查看NgRx 文档以获取有关新@ngrx/signals
包的更多示例和用法。
NgRx Signals 软件包目前处于开发者预览阶段,我们正在收集用户反馈并改进其 API。我们也在为 NgRx Signals 软件包征集一些 logo 设计灵感!欢迎查看未解决的问题并/或提供建议!
非常感谢Angular Architects 团队的Manfred Steyer和Rainer Hahnekamp对 NgRx Signals 库提供的宝贵反馈和演示。
NgRx 库的未来
随着 NgRx Signals 库的推出,您可能想知道其他 NgRx 库会发生什么。
NgRx Store 周围的生态系统继续很好地扩展到大型企业应用程序,并且我们将继续改进与 Signals 的集成,而不会破坏其自然工作流程。
NgRx ComponentStore 的构建是为了在 Angular Signals 推出之前,在本地层面填补 RxJS 在状态管理方面响应性和结构化的空白。它也被应用于企业应用,并持续提供价值。由于它存在时间更长,并且如今已久经考验,有些人会继续使用它。目前没有弃用它的计划。
NgRx Signals 是一种全新的响应式状态管理方法,其根源在于 ComponentStore API,并且支持 RxJS 的使用。它还提供了其他一些实用程序,可以结构化地使用 Angular Signals,帮助开发者进行扩展。因此,它并非 ComponentStore 的替代品。我们会看到越来越多的人开始使用 NgRx Signals,并提升其使用体验。它继承了我们多年来维护这些库的经验。
每个包在 NgRx 生态系统中都有其用途,并且将继续如此。
NgRx 研讨会🎓
随着 NgRx 的使用率与 Angular 的持续增长,许多开发者和团队仍然需要关于如何架构和构建企业级 Angular 应用的指导。我们非常高兴地宣布即将推出由 NgRx 团队直接提供的研讨会!
从一月份开始,我们将提供一到三场全天研讨会,涵盖从 NgRx 的基础知识到最高级的主题。无论您的团队是刚开始使用 NgRx,还是已经使用一段时间,都能在这些研讨会中学习到新的概念。
研讨会涵盖了使用 NgRx Store 和库的全局状态,以及使用 NgRx ComponentStore 和 NgRx Signals 管理本地状态。
请访问我们的研讨会页面,从即将举行的研讨会列表中进行报名。
新的 NgRx 操作符包
随着 NgRx 框架包的扩展,一些 RxJS 操作符逐渐出现在许多不同的领域。这些操作符位于tapResponse
NgRx ComponentStore 包和concatLatestFrom
NgRx Effects 包中。
该tapResponse
操作符提供了一种简单且安全的方式处理 Observable 的响应。它强制处理错误情况,并确保即使发生错误,effect 仍会继续运行。
该concatLatestFrom
运算符的功能与 类似,但withLatestFrom
有一个重要的区别 - 它惰性地评估所提供的 Observable 工厂。
我们引入这个包是为了向任何@ngrx/operators
Angular 应用程序提供这些通用操作符。NgRx ComponentStore 和 NgRx Effects 包仍然提供这些操作符,因为它们是从该包重新导出的。@ngrx/operators
使用 NgRx StoreDevtools 获得更好的性能
NgRx StoreDevtools 提供了一种在使用 NgRx Store 时轻松调试、跟踪和检查整个应用程序操作流程的方法。以前,将 StoreDevtools 连接到 Redux Devtools Extension 时,此连接是在 zone.js 的 Angular 变更检测上下文中完成的。这可能会导致与 Redux Devtools 交互时出现额外的变更检测周期。
我们通过连接到 Angular Zone 上下文之外的 Redux Devtools 对此进行了改进,当一起使用 NgRx Store 和 NgRx StoreDevtools 时,为新应用程序和现有应用程序提供更好的性能。
作为 v17 升级的一部分,迁移将作为现有应用程序的选择运行:
对于基于模块的应用程序:
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
@NgModule({
imports: [
StoreDevtoolsModule.instrument({
connectInZone: true,
}),
],
bootstrap: [AppComponent],
})
export class AppModule {}
对于使用独立 API 的应用程序:
import { provideStoreDevtools } from '@ngrx/store-devtools';
bootstrapApplication(AppComponent, {
providers: [
provideStoreDevtools({
maxAge: 25,
logOnly: !isDevMode(),
connectInZone: true
}),
],
});
connectInZone
您可以通过从 StoreDevtools 选项中移除该属性,选择退出在 Angular 变更检测上下文中使用 zone.js 进行连接。对于新应用程序,这是默认行为。
感谢Artur Androsovych的贡献!
文档的黑暗模式😎
我们的文档中添加的另一个期待已久的功能是黑暗模式!
非常感谢Mateusz Stefańczyk的贡献!
维护模式下的 NgRx 数据
Ward Bell 和 John Papa 最初创建了 NgRx Data 包,目的是使用 NgRx Store、Effects 和 Entity 以更结构化的方式处理数据集合。虽然这个包多年来为开发者提供了一些价值,但它并没有像其他 NgRx 库那样持续发展,从而变得更具可扩展性。
随着新库和新模式的出现,我们决定将 NgRx Data 移至维护模式。
这意味着:
- NgRx Data 不会在 v17 中被弃用。
- 此软件包的新功能请求将不被接受。
- NgRx Data 不适用于新项目和现有项目。
对于那些希望迁移到 NgRx Data 的用户,我们将推荐一些未来迁移的策略。
弃用和重大变更💥
此版本包含错误修复、弃用功能和重大变更。对于大多数弃用功能或重大变更,我们提供了迁移功能,该功能会在您将应用升级到最新版本时自动运行。
请参阅版本 17 迁移指南,获取有关迁移到最新版本的完整信息。完整的变更日志可在我们的 GitHub 代码库中找到。
升级到 NgRx 17
要开始使用 NgRx 17,请确保安装以下最低版本:
- Angular 版本 17.x
- Angular CLI 版本 17.x
- TypeScript 版本 5.2.x
- RxJS 版本 ^6.5.x 或 ^7.5.x
NgRx 支持使用 Angular CLIng update
命令来更新你的 NgRx 软件包。要将软件包更新到最新版本,请运行以下命令:
ng update @ngrx/store
如果您的项目使用@ngrx/component-store
,但没有@ngrx/store
,请运行以下命令:
ng update @ngrx/component-store
Swag 商店和 Discord 服务器🦺
您可以通过我们的商店购买 NgRx 官方礼品!印有 NgRx 标志的 T 恤有多种尺码、材质和颜色可供选择。我们计划未来在商店中增加贴纸、磁铁等新品。立即访问我们的商店,获取您的 NgRx 礼品吧!
想要与 NgRx 社区新老成员互动的朋友们,欢迎加入我们的Discord 服务器。
为 NgRx 做贡献
我们始终致力于改进文档,并使其始终与 NgRx 框架用户保持同步。为了帮助我们,您可以开始为 NgRx 做出贡献。如果您不确定从哪里开始,请查看我们的贡献指南,并观看Jan-Niklas Wortmann和Brandon Roberts制作的介绍视频 ,它们可以帮助您入门。
感谢我们所有的贡献者和赞助商!
NgRx 始终是一个社区驱动的项目。设计、开发、文档和测试均在社区的帮助下完成。访问我们的社区贡献者页面,查看所有为该框架做出贡献的人员。
如果您有兴趣贡献代码,请访问我们的GitHub页面,浏览我们的未解决问题,其中一些问题已标记为专门针对新贡献者。我们还在 GitHub 上积极讨论新功能和增强功能。
我们要衷心感谢我们的金牌赞助商Nx!Nx 长期以来一直大力推广 NgRx 作为构建 Angular 应用程序的工具,并致力于支持他们所依赖的开源项目。
我们还要感谢我们的铜牌赞助商House of Angular!
在Twitter和LinkedIn上关注我们,了解有关 NgRx 平台的最新更新。
文章来源:https://dev.to/ngrx/announcing-ngrx-v17-introducing-ngrx-signals-operators-performance-improvements-workshops-and-more-55e4