feat(angular): deprecate DataPersistence class in favor of data persistence operators (#11183)

This commit is contained in:
Leosvel Pérez Espinosa 2022-07-18 11:40:20 +01:00 committed by GitHub
parent bd9b33eaef
commit cc6c2f9c59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 267 additions and 198 deletions

View File

@ -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",

View File

@ -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"
},

View File

@ -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)

View File

@ -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.

View File

@ -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",

View File

@ -15,33 +15,22 @@ import {
withLatestFrom,
} from 'rxjs/operators';
/**
* See {@link DataPersistence.pessimisticUpdate} for more information.
*/
export interface PessimisticUpdateOpts<T extends Array<unknown>, A> {
run(a: A, ...slices: [...T]): Observable<Action> | Action | void;
onError(a: A, e: any): Observable<any> | any;
}
/**
* See {@link DataPersistence.pessimisticUpdate} for more information.
*/
export interface OptimisticUpdateOpts<T extends Array<unknown>, A> {
run(a: A, ...slices: [...T]): Observable<Action> | Action | void;
undoAction(a: A, e: any): Observable<Action> | Action;
}
/**
* See {@link DataPersistence.fetch} for more information.
*/
export interface FetchOpts<T extends Array<unknown>, A> {
id?(a: A, ...slices: [...T]): any;
run(a: A, ...slices: [...T]): Observable<Action> | Action | void;
onError?(a: A, e: any): Observable<any> | any;
}
/**
* See {@link DataPersistence.navigation} for more information.
*/
export interface HandleNavigationOpts<T extends Array<unknown>> {
run(
a: ActivatedRouteSnapshot,
@ -61,6 +50,63 @@ export type ActionStateStream<T, A> = 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<T extends Array<unknown>, A extends Action>(
opts: PessimisticUpdateOpts<T, A>
) {
@ -72,6 +118,64 @@ export function pessimisticUpdate<T extends Array<unknown>, 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<T extends Array<unknown>, A extends Action>(
opts: OptimisticUpdateOpts<T, A>
) {
@ -83,6 +187,84 @@ export function optimisticUpdate<T extends Array<unknown>, 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<T extends Array<unknown>, A extends Action>(
opts: FetchOpts<T, A>
) {
@ -109,6 +291,55 @@ export function fetch<T extends Array<unknown>, 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<T extends Array<unknown>, A extends Action>(
component: Type<any>,
opts: HandleNavigationOpts<T>
@ -189,56 +420,18 @@ function normalizeActionAndState<T extends Array<unknown>, 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<T> {
constructor(public store: Store<T>, 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<UpdateTodo>('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<TodosState>, 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<A extends Action = Action>(
actionType: string | ActionCreator,
@ -252,50 +445,10 @@ export class DataPersistence<T> {
}
/**
* 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<UpdateTodo>('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<TodosState>, 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<A extends Action = Action>(
actionType: string | ActionCreator,
@ -309,71 +462,10 @@ export class DataPersistence<T> {
}
/**
* 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<GetTodos>('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<TodosState>, 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<GetTodo>('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<TodosState>, 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<A extends Action = Action>(
actionType: string | ActionCreator,
@ -387,37 +479,10 @@ export class DataPersistence<T> {
}
/**
* @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<TodosState>, private backend: Backend) {}
* }
* ```
* @deprecated Use the {@link navigation} operator instead.
* The {@link DataPersistence} class will be removed in v15.
*/
navigation(
component: Type<any>,

View File

@ -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 {