From 4e2dccd2e337a191f355740c79bb448f8d670159 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Fri, 26 Jun 2026 14:19:10 -0300 Subject: [PATCH] feat: implement useMutation hook for managing asynchronous operations with loading and error states --- src/app/core/utils/use-mutation.spec.ts | 31 ++++++++++++++++++ src/app/core/utils/use-mutation.ts | 42 +++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/app/core/utils/use-mutation.spec.ts create mode 100644 src/app/core/utils/use-mutation.ts diff --git a/src/app/core/utils/use-mutation.spec.ts b/src/app/core/utils/use-mutation.spec.ts new file mode 100644 index 0000000..82a0590 --- /dev/null +++ b/src/app/core/utils/use-mutation.spec.ts @@ -0,0 +1,31 @@ +import { of, throwError } from 'rxjs'; +import { describe, expect, it, vi } from 'vitest'; + +import { useMutation } from './use-mutation'; + +describe('useMutation', () => { + it('stores the resolved value and clears loading after a successful execution', async () => { + const mutation = useMutation<{ id: number }, { ok: boolean; id: number }>(); + const factory = vi.fn((payload: { id: number }) => of({ ok: true, id: payload.id })); + + await expect(mutation.execute(factory, { id: 7 })).resolves.toEqual({ ok: true, id: 7 }); + + expect(factory).toHaveBeenCalledWith({ id: 7 }); + expect(mutation.loading()).toBe(false); + expect(mutation.error()).toBeNull(); + expect(mutation.data()).toEqual({ ok: true, id: 7 }); + }); + + it('stores the thrown error and clears loading after a failed execution', async () => { + const mutation = useMutation<{ id: number }, { ok: boolean }>(); + const failure = new Error('request failed'); + const factory = vi.fn(() => throwError(() => failure)); + + await expect(mutation.execute(factory, { id: 7 })).rejects.toThrow('request failed'); + + expect(factory).toHaveBeenCalledWith({ id: 7 }); + expect(mutation.loading()).toBe(false); + expect(mutation.error()).toBe(failure); + expect(mutation.data()).toBeNull(); + }); +}); diff --git a/src/app/core/utils/use-mutation.ts b/src/app/core/utils/use-mutation.ts new file mode 100644 index 0000000..60ef895 --- /dev/null +++ b/src/app/core/utils/use-mutation.ts @@ -0,0 +1,42 @@ +import { signal, WritableSignal } from '@angular/core'; +import { Observable, firstValueFrom } from 'rxjs'; + +export interface UseMutationResult { + loading: WritableSignal; + error: WritableSignal; + data: WritableSignal; + execute: (factory: (payload: TData) => Observable, payload: TData) => Promise; +} + +export function useMutation(): UseMutationResult { + const loading = signal(false); + const error = signal(null); + const data = signal(null); + + async function execute( + factory: (payload: TData) => Observable, + payload: TData + ): Promise { + loading.set(true); + error.set(null); + + try { + const result = await firstValueFrom(factory(payload)); + data.set(result); + + return result; + } catch (err) { + error.set(err); + throw err; + } finally { + loading.set(false); + } + } + + return { + loading, + error, + data, + execute + }; +}