Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(operators): add mapResponse #4302

Merged
merged 9 commits into from
May 13, 2024
77 changes: 77 additions & 0 deletions modules/operators/spec/map-response.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { noop, Observable, of, throwError } from 'rxjs';
import { mapResponse } from '..';
import { concatMap, finalize } from 'rxjs/operators';

describe('mapResponse', () => {
it('should map the emitted value using the next callback', () => {
const results: number[] = [];

of(1, 2, 3)
.pipe(
mapResponse({
next: (value) => value + 1,
error: noop,
})
)
.subscribe((result) => {
results.push(result as number);
});

expect(results).toEqual([2, 3, 4]);
});

it('should map the thrown error using the error callback', (done) => {
throwError(() => 'error')
.pipe(
mapResponse({
next: noop,
error: (error) => `mapped ${error}`,
})
)
.subscribe((result) => {
expect(result).toBe('mapped error');
done();
});
});

it('should map the error thrown in next callback using error callback', (done) => {
function producesError() {
throw 'error';
}

of(1)
.pipe(
mapResponse({
next: producesError,
error: (error) => `mapped ${error}`,
})
)
.subscribe((result) => {
expect(result).toBe('mapped error');
done();
});
});

it('should not unsubscribe from outer observable on inner observable error', () => {
const innerCompleteCallback = jest.fn<void, []>();
const outerCompleteCallback = jest.fn<void, []>();

new Observable((subscriber) => subscriber.next(1))
.pipe(
concatMap(() =>
throwError(() => 'error').pipe(
mapResponse({
next: noop,
error: noop,
}),
finalize(innerCompleteCallback)
)
),
finalize(outerCompleteCallback)
)
.subscribe();

expect(innerCompleteCallback).toHaveBeenCalled();
expect(outerCompleteCallback).not.toHaveBeenCalled();
});
});
1 change: 1 addition & 0 deletions modules/operators/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { concatLatestFrom } from './concat_latest_from';
export { mapResponse } from './map-response';
export { tapResponse } from './tap-response';
17 changes: 17 additions & 0 deletions modules/operators/src/map-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';

type MapResponseObserver<T, E, R1, R2> = {
next: (value: T) => R1;
error: (error: E) => R2;
};

export function mapResponse<T, E, R1, R2>(
observer: MapResponseObserver<T, E, R1, R2>
): (source$: Observable<T>) => Observable<R1 | R2> {
return (source$) =>
source$.pipe(
map((value) => observer.next(value)),
catchError((error) => of(observer.error(error)))
);
}
26 changes: 26 additions & 0 deletions projects/ngrx.io/content/guide/operators/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,29 @@ There is also another signature of the `tapResponse` operator that accepts the o
);
});
</code-example>

## mapResponse

The `mapResponse` operator is particularly useful in scenarios where you need to transform data and handle potential errors with minimal boilerplate.

In the example below, we use `mapResponse` within an NgRx effect to handle loading movies from an API. It demonstrates how to map successful API responses to an action indicating success, and how to handle errors by dispatching an error action.

<code-example header="movies.effects.ts">
export const loadMovies = createEffect(
(actions$ = inject(Actions), moviesService = inject(MoviesService)) => {
return actions$.pipe(
ofType(MoviesPageActions.opened),
exhaustMap(() =>
moviesService.getAll().pipe(
mapResponse({
next: (movies) => MoviesApiActions.moviesLoadedSuccess({ movies }),
error: (error: { message: string }) =>
MoviesApiActions.moviesLoadedFailure({ errorMsg: error.message }),
})
)
)
);
},
{ functional: true }
);
</code-example>