feat: implement useMutation hook for managing asynchronous operations with loading and error states
This commit is contained in:
31
src/app/core/utils/use-mutation.spec.ts
Normal file
31
src/app/core/utils/use-mutation.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
42
src/app/core/utils/use-mutation.ts
Normal file
42
src/app/core/utils/use-mutation.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user