feat: implement useMutation hook for managing asynchronous operations with loading and error states

This commit is contained in:
2026-06-26 14:19:10 -03:00
parent da01745732
commit 4e2dccd2e3
2 changed files with 73 additions and 0 deletions

View File

@@ -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();
});
});

View File

@@ -0,0 +1,42 @@
import { signal, WritableSignal } from '@angular/core';
import { Observable, firstValueFrom } from 'rxjs';
export interface UseMutationResult<TData, TResult> {
loading: WritableSignal<boolean>;
error: WritableSignal<unknown | null>;
data: WritableSignal<TResult | null>;
execute: (factory: (payload: TData) => Observable<TResult>, payload: TData) => Promise<TResult>;
}
export function useMutation<TData, TResult>(): UseMutationResult<TData, TResult> {
const loading = signal(false);
const error = signal<unknown | null>(null);
const data = signal<TResult | null>(null);
async function execute(
factory: (payload: TData) => Observable<TResult>,
payload: TData
): Promise<TResult> {
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
};
}