From cc6c2f9c59613af80ea23f9f3a4b66a970121c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Mon, 18 Jul 2022 11:40:20 +0100 Subject: [PATCH] feat(angular): deprecate DataPersistence class in favor of data persistence operators (#11183) --- docs/generated/packages/angular.json | 5 +- docs/map.json | 2 +- docs/shared/angular-plugin.md | 2 +- docs/shared/guides/misc-data-persistence.md | 2 +- .../angular/src/generators/ngrx/schema.json | 3 +- .../src/runtime/nx/data-persistence.ts | 449 ++++++++++-------- packages/angular/src/runtime/nx/nx.module.ts | 2 + 7 files changed, 267 insertions(+), 198 deletions(-) diff --git a/docs/generated/packages/angular.json b/docs/generated/packages/angular.json index 5eed4d3696..4fd3236b78 100644 --- a/docs/generated/packages/angular.json +++ b/docs/generated/packages/angular.json @@ -10,7 +10,7 @@ "id": "overview", "path": "/packages/angular", "file": "shared/angular-plugin", - "content": "![Angular logo](/shared/angular-logo.png)\n\nThe Nx Plugin for Angular contains executors, generators, and utilities for managing Angular applications and libraries within an Nx workspace. It provides:\n\n- Integration with libraries such as Storybook, Jest, Cypress, Karma, and Protractor.\n- Generators to help scaffold code quickly, including:\n - Micro Frontends\n - Libraries, both internal to your codebase and publishable to npm\n - Upgrading AngularJS applications\n - Single Component Application Modules (SCAMs)\n- NgRx helpers.\n- Utilities for automatic workspace refactoring.\n\n## Setting up the Angular plugin\n\nAdding the Angular plugin to an existing Nx workspace can be done with the following:\n\n```bash\nyarn add -D @nrwl/angular\n```\n\n```bash\nnpm install -D @nrwl/angular\n```\n\n## Using the Angular Plugin\n\n### Generating an application\n\nIt's straightforward to generate an Angular application:\n\n```bash\nnx g @nrwl/angular:app appName\n```\n\nBy default, the application will be generated with:\n\n- ESLint as the linter.\n- Jest as the unit test runner.\n- Cypress as the E2E test runner.\n\nWe can then serve, build, test, lint, and run e2e tests on the application with the following commands:\n\n```bash\nnx serve appName\nnx build appName\nnx test appName\nnx lint appName\nnx e2e appName\n```\n\n### Generating a library\n\nGenerating an Angular library is very similar to generating an application:\n\n```bash\nnx g @nrwl/angular:lib libName\n```\n\nBy default, the library will be generated with:\n\n- ESLint as the linter.\n- Jest as the unit test runner.\n\nWe can then test and lint the library with the following commands:\n\n```bash\nnx test libName\nnx lint libName\n```\n\nRead more about:\n\n- [Creating Libraries](/structure/creating-libraries)\n- [Library Types](/structure/library-types)\n- [Buildable and Publishable Libraries](/structure/buildable-and-publishable-libraries)\n\n## More Documentation\n\n- [Angular Nx Tutorial](/angular-tutorial/01-create-application)\n- [Setup Module Federation with Angular and Nx](/module-federation/faster-builds)\n- [Using NgRx](/guides/misc-ngrx)\n- [Using DataPersistence](/guides/misc-data-persistence)\n- [Upgrading an AngularJS application to Angular](/migration/migration-angularjs)\n- [Using Tailwind CSS with Angular projects](/guides/using-tailwind-css-with-angular-projects)\n" + "content": "![Angular logo](/shared/angular-logo.png)\n\nThe Nx Plugin for Angular contains executors, generators, and utilities for managing Angular applications and libraries within an Nx workspace. It provides:\n\n- Integration with libraries such as Storybook, Jest, Cypress, Karma, and Protractor.\n- Generators to help scaffold code quickly, including:\n - Micro Frontends\n - Libraries, both internal to your codebase and publishable to npm\n - Upgrading AngularJS applications\n - Single Component Application Modules (SCAMs)\n- NgRx helpers.\n- Utilities for automatic workspace refactoring.\n\n## Setting up the Angular plugin\n\nAdding the Angular plugin to an existing Nx workspace can be done with the following:\n\n```bash\nyarn add -D @nrwl/angular\n```\n\n```bash\nnpm install -D @nrwl/angular\n```\n\n## Using the Angular Plugin\n\n### Generating an application\n\nIt's straightforward to generate an Angular application:\n\n```bash\nnx g @nrwl/angular:app appName\n```\n\nBy default, the application will be generated with:\n\n- ESLint as the linter.\n- Jest as the unit test runner.\n- Cypress as the E2E test runner.\n\nWe can then serve, build, test, lint, and run e2e tests on the application with the following commands:\n\n```bash\nnx serve appName\nnx build appName\nnx test appName\nnx lint appName\nnx e2e appName\n```\n\n### Generating a library\n\nGenerating an Angular library is very similar to generating an application:\n\n```bash\nnx g @nrwl/angular:lib libName\n```\n\nBy default, the library will be generated with:\n\n- ESLint as the linter.\n- Jest as the unit test runner.\n\nWe can then test and lint the library with the following commands:\n\n```bash\nnx test libName\nnx lint libName\n```\n\nRead more about:\n\n- [Creating Libraries](/structure/creating-libraries)\n- [Library Types](/structure/library-types)\n- [Buildable and Publishable Libraries](/structure/buildable-and-publishable-libraries)\n\n## More Documentation\n\n- [Angular Nx Tutorial](/angular-tutorial/01-create-application)\n- [Setup Module Federation with Angular and Nx](/module-federation/faster-builds)\n- [Using NgRx](/guides/misc-ngrx)\n- [Using Data Persistence operators](/guides/misc-data-persistence)\n- [Upgrading an AngularJS application to Angular](/migration/migration-angularjs)\n- [Using Tailwind CSS with Angular projects](/guides/using-tailwind-css-with-angular-projects)\n" } ], "generators": [ @@ -1472,7 +1472,8 @@ "useDataPersistence": { "type": "boolean", "default": false, - "description": "Generate NgRx Effects with the `DataPersistence` helper service. Set to false to use plain effects data persistence operators." + "description": "Generate NgRx Effects with the `DataPersistence` helper service. Set to false to use plain effects data persistence operators.", + "x-deprecated": "This option is deprecated and will be removed in v15. Using the individual operators is recommended." }, "barrels": { "type": "boolean", diff --git a/docs/map.json b/docs/map.json index 1da8b8a2eb..fdb132691a 100644 --- a/docs/map.json +++ b/docs/map.json @@ -626,7 +626,7 @@ "file": "shared/guides/misc-ngrx" }, { - "name": "Using DataPersistence", + "name": "Using Data Persistence operators", "id": "misc-data-persistence", "file": "shared/guides/misc-data-persistence" }, diff --git a/docs/shared/angular-plugin.md b/docs/shared/angular-plugin.md index aa6e6412b0..aad8e653c4 100644 --- a/docs/shared/angular-plugin.md +++ b/docs/shared/angular-plugin.md @@ -80,6 +80,6 @@ Read more about: - [Angular Nx Tutorial](/angular-tutorial/01-create-application) - [Setup Module Federation with Angular and Nx](/module-federation/faster-builds) - [Using NgRx](/guides/misc-ngrx) -- [Using DataPersistence](/guides/misc-data-persistence) +- [Using Data Persistence operators](/guides/misc-data-persistence) - [Upgrading an AngularJS application to Angular](/migration/migration-angularjs) - [Using Tailwind CSS with Angular projects](/guides/using-tailwind-css-with-angular-projects) diff --git a/docs/shared/guides/misc-data-persistence.md b/docs/shared/guides/misc-data-persistence.md index 58feccf41f..5198a4e9cc 100644 --- a/docs/shared/guides/misc-data-persistence.md +++ b/docs/shared/guides/misc-data-persistence.md @@ -1,4 +1,4 @@ -# Using DataPersistence +# Using Data Persistence operators Managing state is a hard problem. We need to coordinate multiple backends, web workers, and UI components, all of which update the state concurrently. diff --git a/packages/angular/src/generators/ngrx/schema.json b/packages/angular/src/generators/ngrx/schema.json index ef91972034..679c67ab68 100644 --- a/packages/angular/src/generators/ngrx/schema.json +++ b/packages/angular/src/generators/ngrx/schema.json @@ -66,7 +66,8 @@ "useDataPersistence": { "type": "boolean", "default": false, - "description": "Generate NgRx Effects with the `DataPersistence` helper service. Set to false to use plain effects data persistence operators." + "description": "Generate NgRx Effects with the `DataPersistence` helper service. Set to false to use plain effects data persistence operators.", + "x-deprecated": "This option is deprecated and will be removed in v15. Using the individual operators is recommended." }, "barrels": { "type": "boolean", diff --git a/packages/angular/src/runtime/nx/data-persistence.ts b/packages/angular/src/runtime/nx/data-persistence.ts index e4bdd7796c..94cb1509aa 100644 --- a/packages/angular/src/runtime/nx/data-persistence.ts +++ b/packages/angular/src/runtime/nx/data-persistence.ts @@ -15,33 +15,22 @@ import { withLatestFrom, } from 'rxjs/operators'; -/** - * See {@link DataPersistence.pessimisticUpdate} for more information. - */ export interface PessimisticUpdateOpts, A> { run(a: A, ...slices: [...T]): Observable | Action | void; onError(a: A, e: any): Observable | any; } -/** - * See {@link DataPersistence.pessimisticUpdate} for more information. - */ + export interface OptimisticUpdateOpts, A> { run(a: A, ...slices: [...T]): Observable | Action | void; undoAction(a: A, e: any): Observable | Action; } -/** - * See {@link DataPersistence.fetch} for more information. - */ export interface FetchOpts, A> { id?(a: A, ...slices: [...T]): any; run(a: A, ...slices: [...T]): Observable | Action | void; onError?(a: A, e: any): Observable | any; } -/** - * See {@link DataPersistence.navigation} for more information. - */ export interface HandleNavigationOpts> { run( a: ActivatedRouteSnapshot, @@ -61,6 +50,63 @@ export type ActionStateStream = Observable< ActionOrActionWithStates<[T], A> >; +/** + * + * @whatItDoes Handles pessimistic updates (updating the server first). + * + * Updating the server, when implemented naively, suffers from race conditions and poor error handling. + * + * `pessimisticUpdate` addresses these problems. It runs all fetches in order, which removes race conditions + * and forces the developer to handle errors. + * + * ## Example: + * + * ```typescript + * @Injectable() + * class TodoEffects { + * updateTodo$ = createEffect(() => + * this.actions$.pipe( + * ofType('UPDATE_TODO'), + * pessimisticUpdate({ + * // provides an action + * run: (action: UpdateTodo) => { + * // update the backend first, and then dispatch an action that will + * // update the client side + * return this.backend.updateTodo(action.todo.id, action.todo).pipe( + * map((updated) => ({ + * type: 'UPDATE_TODO_SUCCESS', + * todo: updated, + * })) + * ); + * }, + * onError: (action: UpdateTodo, error: any) => { + * // we don't need to undo the changes on the client side. + * // we can dispatch an error, or simply log the error here and return `null` + * return null; + * }, + * }) + * ) + * ); + * + * constructor(private actions$: Actions, private backend: Backend) {} + * } + * ``` + * + * Note that if you don't return a new action from the run callback, you must set the dispatch property + * of the effect to false, like this: + * + * ```typescript + * class TodoEffects { + * updateTodo$ = createEffect(() => + * this.actions$.pipe( + * //... + * ), { dispatch: false } + * ); + * } + * ``` + * + * @param opts + */ export function pessimisticUpdate, A extends Action>( opts: PessimisticUpdateOpts ) { @@ -72,6 +118,64 @@ export function pessimisticUpdate, A extends Action>( }; } +/** + * + * @whatItDoes Handles optimistic updates (updating the client first). + * + * It runs all fetches in order, which removes race conditions and forces the developer to handle errors. + * + * When using `optimisticUpdate`, in case of a failure, the developer has already updated the state locally, + * so the developer must provide an undo action. + * + * The error handling must be done in the callback, or by means of the undo action. + * + * ## Example: + * + * ```typescript + * @Injectable() + * class TodoEffects { + * updateTodo$ = createEffect(() => + * this.actions$.pipe( + * ofType('UPDATE_TODO'), + * optimisticUpdate({ + * // provides an action + * run: (action: UpdateTodo) => { + * return this.backend.updateTodo(action.todo.id, action.todo).pipe( + * mapTo({ + * type: 'UPDATE_TODO_SUCCESS', + * }) + * ); + * }, + * undoAction: (action: UpdateTodo, error: any) => { + * // dispatch an undo action to undo the changes in the client state + * return { + * type: 'UNDO_TODO_UPDATE', + * todo: action.todo, + * }; + * }, + * }) + * ) + * ); + * + * constructor(private actions$: Actions, private backend: Backend) {} + * } + * ``` + * + * Note that if you don't return a new action from the run callback, you must set the dispatch property + * of the effect to false, like this: + * + * ```typescript + * class TodoEffects { + * updateTodo$ = createEffect(() => + * this.actions$.pipe( + * //... + * ), { dispatch: false } + * ); + * } + * ``` + * + * @param opts + */ export function optimisticUpdate, A extends Action>( opts: OptimisticUpdateOpts ) { @@ -83,6 +187,84 @@ export function optimisticUpdate, A extends Action>( }; } +/** + * + * @whatItDoes Handles data fetching. + * + * Data fetching implemented naively suffers from race conditions and poor error handling. + * + * `fetch` addresses these problems. It runs all fetches in order, which removes race conditions + * and forces the developer to handle errors. + * + * ## Example: + * + * ```typescript + * @Injectable() + * class TodoEffects { + * loadTodos$ = createEffect(() => + * this.actions$.pipe( + * ofType('GET_TODOS'), + * fetch({ + * // provides an action + * run: (a: GetTodos) => { + * return this.backend.getAll().pipe( + * map((response) => ({ + * type: 'TODOS', + * todos: response.todos, + * })) + * ); + * }, + * onError: (action: GetTodos, error: any) => { + * // dispatch an undo action to undo the changes in the client state + * return null; + * }, + * }) + * ) + * ); + * + * constructor(private actions$: Actions, private backend: Backend) {} + * } + * ``` + * + * This is correct, but because it set the concurrency to 1, it may not be performant. + * + * To fix that, you can provide the `id` function, like this: + * + * ```typescript + * @Injectable() + * class TodoEffects { + * loadTodo$ = createEffect(() => + * this.actions$.pipe( + * ofType('GET_TODO'), + * fetch({ + * id: (todo: GetTodo) => { + * return todo.id; + * }, + * // provides an action + * run: (todo: GetTodo) => { + * return this.backend.getTodo(todo.id).map((response) => ({ + * type: 'LOAD_TODO_SUCCESS', + * todo: response.todo, + * })); + * }, + * onError: (action: GetTodo, error: any) => { + * // dispatch an undo action to undo the changes in the client state + * return null; + * }, + * }) + * ) + * ); + * + * constructor(private actions$: Actions, private backend: Backend) {} + * } + * ``` + * + * With this setup, the requests for Todo 1 will run concurrently with the requests for Todo 2. + * + * In addition, if there are multiple requests for Todo 1 scheduled, it will only run the last one. + * + * @param opts + */ export function fetch, A extends Action>( opts: FetchOpts ) { @@ -109,6 +291,55 @@ export function fetch, A extends Action>( }; } +/** + * @whatItDoes Handles data fetching as part of router navigation. + * + * Data fetching implemented naively suffers from race conditions and poor error handling. + * + * `navigation` addresses these problems. + * + * It checks if an activated router state contains the passed in component type, and, if it does, runs the `run` + * callback. It provides the activated snapshot associated with the component and the current state. And it only runs + * the last request. + * + * ## Example: + * + * ```typescript + * @Injectable() + * class TodoEffects { + * loadTodo$ = createEffect(() => + * this.actions$.pipe( + * // listens for the routerNavigation action from @ngrx/router-store + * navigation(TodoComponent, { + * run: (activatedRouteSnapshot: ActivatedRouteSnapshot) => { + * return this.backend + * .fetchTodo(activatedRouteSnapshot.params['id']) + * .pipe( + * map((todo) => ({ + * type: 'LOAD_TODO_SUCCESS', + * todo: todo, + * })) + * ); + * }, + * onError: ( + * activatedRouteSnapshot: ActivatedRouteSnapshot, + * error: any + * ) => { + * // we can log and error here and return null + * // we can also navigate back + * return null; + * }, + * }) + * ) + * ); + * + * constructor(private actions$: Actions, private backend: Backend) {} + * } + * ``` + * + * @param component + * @param opts + */ export function navigation, A extends Action>( component: Type, opts: HandleNavigationOpts @@ -189,56 +420,18 @@ function normalizeActionAndState, A>( /** * @whatItDoes Provides convenience methods for implementing common operations of persisting data. + * + * @deprecated Use the individual operators instead. Will be removed in v15. */ @Injectable() export class DataPersistence { constructor(public store: Store, public actions: Actions) {} /** + * See {@link pessimisticUpdate} operator for more information. * - * @whatItDoes Handles pessimistic updates (updating the server first). - * - * Update the server implemented naively suffers from race conditions and poor error handling. - * - * `pessimisticUpdate` addresses these problems--it runs all fetches in order, which removes race conditions - * and forces the developer to handle errors. - * - * ## Example: - * - * ```typescript - * @Injectable() - * class TodoEffects { - * @Effect() updateTodo = this.s.pessimisticUpdate('UPDATE_TODO', { - * // provides an action and the current state of the store - * run(a, state) { - * // update the backend first, and then dispatch an action that will - * // update the client side - * return this.backend(state.user, a.payload).map(updated => ({ - * type: 'TODO_UPDATED', - * payload: updated - * })); - * }, - * - * onError(a, e: any) { - * // we don't need to undo the changes on the client side. - * // we can dispatch an error, or simply log the error here and return `null` - * return null; - * } - * }); - * - * constructor(private s: DataPersistence, private backend: Backend) {} - * } - * ``` - * - * Note that if you don't return a new action from the run callback, you must set the dispatch property - * of the effect to false, like this: - * - * ``` - * class TodoEffects { - * @Effect({dispatch: false}) - * updateTodo; //... - * } - * ``` + * @deprecated Use the {@link pessimisticUpdate} operator instead. + * The {@link DataPersistence} class will be removed in v15. */ pessimisticUpdate( actionType: string | ActionCreator, @@ -252,50 +445,10 @@ export class DataPersistence { } /** + * See {@link optimisticUpdate} operator for more information. * - * @whatItDoes Handles optimistic updates (updating the client first). - * - * `optimisticUpdate` addresses these problems--it runs all fetches in order, which removes race conditions - * and forces the developer to handle errors. - * - * `optimisticUpdate` is different from `pessimisticUpdate`. In case of a failure, when using `optimisticUpdate`, - * the developer already updated the state locally, so the developer must provide an undo action. - * - * The error handling must be done in the callback, or by means of the undo action. - * - * ## Example: - * - * ```typescript - * @Injectable() - * class TodoEffects { - * @Effect() updateTodo = this.s.optimisticUpdate('UPDATE_TODO', { - * // provides an action and the current state of the store - * run: (a, state) => { - * return this.backend(state.user, a.payload); - * }, - * - * undoAction: (a, e: any) => { - * // dispatch an undo action to undo the changes in the client state - * return ({ - * type: 'UNDO_UPDATE_TODO', - * payload: a - * }); - * } - * }); - * - * constructor(private s: DataPersistence, private backend: Backend) {} - * } - * ``` - * - * Note that if you don't return a new action from the run callback, you must set the dispatch property - * of the effect to false, like this: - * - * ``` - * class TodoEffects { - * @Effect({dispatch: false}) - * updateTodo; //... - * } - * ``` + * @deprecated Use the {@link optimisticUpdate} operator instead. + * The {@link DataPersistence} class will be removed in v15. */ optimisticUpdate( actionType: string | ActionCreator, @@ -309,71 +462,10 @@ export class DataPersistence { } /** + * See {@link fetch} operator for more information. * - * @whatItDoes Handles data fetching. - * - * Data fetching implemented naively suffers from race conditions and poor error handling. - * - * `fetch` addresses these problems--it runs all fetches in order, which removes race conditions - * and forces the developer to handle errors. - * - * ## Example: - * - * ```typescript - * @Injectable() - * class TodoEffects { - * @Effect() loadTodos = this.s.fetch('GET_TODOS', { - * // provides an action and the current state of the store - * run: (a, state) => { - * return this.backend(state.user, a.payload).map(r => ({ - * type: 'TODOS', - * payload: r - * }); - * }, - * - * onError: (a, e: any) => { - * // dispatch an undo action to undo the changes in the client state - * } - * }); - * - * constructor(private s: DataPersistence, private backend: Backend) {} - * } - * ``` - * - * This is correct, but because it set the concurrency to 1, it may not be performant. - * - * To fix that, you can provide the `id` function, like this: - * - * ```typescript - * @Injectable() - * class TodoEffects { - * @Effect() loadTodo = this.s.fetch('GET_TODO', { - * id: (a, state) => { - * return a.payload.id; - * } - * - * // provides an action and the current state of the store - * run: (a, state) => { - * return this.backend(state.user, a.payload).map(r => ({ - * type: 'TODO', - * payload: r - * }); - * }, - * - * onError: (a, e: any) => { - * // dispatch an undo action to undo the changes in the client state - * return null; - * } - * }); - * - * constructor(private s: DataPersistence, private backend: Backend) {} - * } - * ``` - * - * With this setup, the requests for Todo 1 will run concurrently with the requests for Todo 2. - * - * In addition, if DataPersistence notices that there are multiple requests for Todo 1 scheduled, - * it will only run the last one. + * @deprecated Use the {@link fetch} operator instead. + * The {@link DataPersistence} class will be removed in v15. */ fetch( actionType: string | ActionCreator, @@ -387,37 +479,10 @@ export class DataPersistence { } /** - * @whatItDoes Handles data fetching as part of router navigation. + * See {@link navigation} operator for more information. * - * Data fetching implemented naively suffers from race conditions and poor error handling. - * - * `navigation` addresses these problems. - * - * It checks if an activated router state contains the passed in component type, and, if it does, runs the `run` - * callback. It provides the activated snapshot associated with the component and the current state. And it only runs - * the last request. - * - * ## Example: - * - * ```typescript - * @Injectable() - * class TodoEffects { - * @Effect() loadTodo = this.s.navigation(TodoComponent, { - * run: (a, state) => { - * return this.backend.fetchTodo(a.params['id']).map(todo => ({ - * type: 'TODO_LOADED', - * payload: todo - * })); - * }, - * onError: (a, e: any) => { - * // we can log and error here and return null - * // we can also navigate back - * return null; - * } - * }); - * constructor(private s: DataPersistence, private backend: Backend) {} - * } - * ``` + * @deprecated Use the {@link navigation} operator instead. + * The {@link DataPersistence} class will be removed in v15. */ navigation( component: Type, diff --git a/packages/angular/src/runtime/nx/nx.module.ts b/packages/angular/src/runtime/nx/nx.module.ts index 6354d202e8..f805fa5cdf 100644 --- a/packages/angular/src/runtime/nx/nx.module.ts +++ b/packages/angular/src/runtime/nx/nx.module.ts @@ -5,6 +5,8 @@ import { DataPersistence } from './data-persistence'; * @whatItDoes Provides services for enterprise Angular applications. * * See {@link DataPersistence} for more information. + * + * @deprecated Use the individual operators instead. Will be removed in v15. */ @NgModule({}) export class NxModule {