diff --git a/CHANGELOG.md b/CHANGELOG.md index e79e6fc84..627977543 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ $ npm install @ngxs/store@dev ### To become next patch version +- Feat(store): Add `withExperimentalNgxsPendingTasks` [#2186](https://github.com/ngxs/store/pull/2186) - Fix(store): Decouple state signal updates from synchronous changes [#2189](https://github.com/ngxs/store/pull/2189) ### 18.0.0 2024-06-10 diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index c85c3945e..e819c06e6 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -69,7 +69,7 @@ - [Module Federation](recipes/module-federation.md) - [Unit Testing](recipes/unit-testing.md) - [RxAngular Integration](recipes/intregration-with-rxangular.md) - - [Waiting For App Stability](recipes/waiting-for-app-stability.md) + - [Zoneless Server-Side Rendering](recipes/zoneless-ssr.md) ## COMMUNITY & LABS diff --git a/docs/recipes/waiting-for-app-stability.md b/docs/recipes/zoneless-ssr.md similarity index 53% rename from docs/recipes/waiting-for-app-stability.md rename to docs/recipes/zoneless-ssr.md index 1b970eea0..f7c30e19c 100644 --- a/docs/recipes/waiting-for-app-stability.md +++ b/docs/recipes/zoneless-ssr.md @@ -1,4 +1,4 @@ -# Waiting For App Stability +# Zoneless Server-Side Rendering > :warning: Note that the current recipe may be used starting from Angular 18 because the experimental API was publicly exposed in Angular 18. @@ -11,37 +11,11 @@ NGXS also executes actions during server-side rendering, and some of these actio Let's examine the recipe for updating the "pending tasks" state whenever any action is dispatched and completed: ```ts -import { ApplicationConfig, inject, ExperimentalPendingTasks } from '@angular/core'; -import { ActionStatus, Actions, provideStore, withNgxsPreboot } from '@ngxs/store'; +import { ApplicationConfig } from '@angular/core'; +import { provideStore } from '@ngxs/store'; +import { withExperimentalNgxsPendingTasks } from '@ngxs/store/experimental'; export const appConfig: ApplicationConfig = { - providers: [ - provideStore( - [], - withNgxsPreboot(() => { - const pendingTasks = inject(ExperimentalPendingTasks); - const actions$ = inject(Actions); - - const actionToRemoveTaskFnMap = new Map void>(); - - // Note that you don't have to unsubscribe from the actions stream in - // this specific case, as we complete the actions subject when the root - // view is destroyed. In server-side rendering, the root view is destroyed - // immediately once the app stabilizes and its HTML is serialized. - actions$.subscribe(ctx => { - if (ctx.status === ActionStatus.Dispatched) { - const removeTaskFn = pendingTasks.add(); - actionToRemoveTaskFnMap.set(ctx.action, removeTaskFn); - } else { - const removeTaskFn = actionToRemoveTaskFnMap.get(ctx.action); - if (typeof removeTaskFn === 'function') { - removeTaskFn(); - actionToRemoveTaskFnMap.delete(ctx.action); - } - } - }); - }) - ) - ] + providers: [provideStore([], withExperimentalNgxsPendingTasks())] }; ``` diff --git a/packages/store/experimental/ng-package.json b/packages/store/experimental/ng-package.json new file mode 100644 index 000000000..7881f5833 --- /dev/null +++ b/packages/store/experimental/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "src/index.ts", + "flatModuleFile": "ngxs-store-experimental" + } +} diff --git a/packages/store/experimental/src/index.ts b/packages/store/experimental/src/index.ts new file mode 100644 index 000000000..35959551d --- /dev/null +++ b/packages/store/experimental/src/index.ts @@ -0,0 +1 @@ +export { withExperimentalNgxsPendingTasks } from './pending-tasks'; diff --git a/packages/store/experimental/src/pending-tasks.ts b/packages/store/experimental/src/pending-tasks.ts new file mode 100644 index 000000000..12b9d604d --- /dev/null +++ b/packages/store/experimental/src/pending-tasks.ts @@ -0,0 +1,31 @@ +import { inject, ExperimentalPendingTasks } from '@angular/core'; +import { ActionStatus, Actions, withNgxsPreboot } from '@ngxs/store'; + +/** + * This is an experimental feature that contributes to app stability, + * which is required during server-side rendering. With asynchronous + * actions being dispatched and handled, Angular is unaware of them in + * zoneless mode and doesn't know whether the app is still unstable. + * This may prematurely serialize the final HTML that is sent to the client. + */ +export function withExperimentalNgxsPendingTasks() { + return withNgxsPreboot(() => { + const pendingTasks = inject(ExperimentalPendingTasks); + const actions$ = inject(Actions); + + const actionToRemoveTaskFnMap = new Map void>(); + + actions$.subscribe(ctx => { + if (ctx.status === ActionStatus.Dispatched) { + const removeTaskFn = pendingTasks.add(); + actionToRemoveTaskFnMap.set(ctx.action, removeTaskFn); + } else { + const removeTaskFn = actionToRemoveTaskFnMap.get(ctx.action); + if (typeof removeTaskFn === 'function') { + removeTaskFn(); + actionToRemoveTaskFnMap.delete(ctx.action); + } + } + }); + }); +} diff --git a/packages/store/tests/standalone-features/__snapshots__/preboot-wait-stable.spec.ts.snap b/packages/store/experimental/tests/__snapshots__/preboot-wait-stable.spec.ts.snap similarity index 100% rename from packages/store/tests/standalone-features/__snapshots__/preboot-wait-stable.spec.ts.snap rename to packages/store/experimental/tests/__snapshots__/preboot-wait-stable.spec.ts.snap diff --git a/packages/store/tests/standalone-features/preboot-wait-stable.spec.ts b/packages/store/experimental/tests/preboot-wait-stable.spec.ts similarity index 71% rename from packages/store/tests/standalone-features/preboot-wait-stable.spec.ts rename to packages/store/experimental/tests/preboot-wait-stable.spec.ts index ec3b925f7..f70624ff8 100644 --- a/packages/store/tests/standalone-features/preboot-wait-stable.spec.ts +++ b/packages/store/experimental/tests/preboot-wait-stable.spec.ts @@ -3,8 +3,6 @@ import { AfterViewInit, Component, Injectable, - inject, - ExperimentalPendingTasks, provideExperimentalZonelessChangeDetection, ChangeDetectionStrategy } from '@angular/core'; @@ -12,37 +10,15 @@ import { bootstrapApplication } from '@angular/platform-browser'; import { renderApplication } from '@angular/platform-server'; import { Action, - ActionStatus, - Actions, State, StateContext, StateToken, dispatch, provideStore, - select, - withNgxsPreboot + select } from '@ngxs/store'; import { freshPlatform } from '@ngxs/store/internals/testing'; - -function executeRecipeFromDocs() { - const pendingTasks = inject(ExperimentalPendingTasks); - const actions$ = inject(Actions); - - const actionToRemoveTaskFnMap = new Map void>(); - - actions$.subscribe(ctx => { - if (ctx.status === ActionStatus.Dispatched) { - const removeTaskFn = pendingTasks.add(); - actionToRemoveTaskFnMap.set(ctx.action, removeTaskFn); - } else { - const removeTaskFn = actionToRemoveTaskFnMap.get(ctx.action); - if (typeof removeTaskFn === 'function') { - removeTaskFn(); - actionToRemoveTaskFnMap.delete(ctx.action); - } - } - }); -} +import { withExperimentalNgxsPendingTasks } from '@ngxs/store/experimental'; describe('preboot feature + stable', () => { const COUNTRIES_STATE_TOKEN = new StateToken('countries'); @@ -92,7 +68,7 @@ describe('preboot feature + stable', () => { providers: [ provideExperimentalZonelessChangeDetection(), - provideStore([CountriesState], withNgxsPreboot(executeRecipeFromDocs)) + provideStore([CountriesState], withExperimentalNgxsPendingTasks()) ] }), { diff --git a/tsconfig.base.json b/tsconfig.base.json index 023e64337..a62e29e57 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -29,6 +29,7 @@ "@ngxs/storage-plugin": ["packages/storage-plugin/index.ts"], "@ngxs/storage-plugin/internals": ["packages/storage-plugin/internals/src/index.ts"], "@ngxs/store": ["packages/store/index.ts"], + "@ngxs/store/experimental": ["packages/store/experimental/src/index.ts"], "@ngxs/store/internals": ["packages/store/internals/src/index.ts"], "@ngxs/store/internals/testing": ["packages/store/internals/testing/src/index.ts"], "@ngxs/store/operators": ["packages/store/operators/src/index.ts"],