HOTFIX: useMutation.run returns MutationOutcome (no throw); sweep all 7 call sites + README convention

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 00:44:56 +02:00
parent 3476b71525
commit 27299e225c
8 changed files with 54 additions and 40 deletions

View File

@@ -67,11 +67,25 @@ export function useApi<T>(loader: () => Promise<T>, deps: unknown[]): AsyncState
return { data: resolved.data, loading, error: loading ? null : resolved.error, reload }
}
/** Wraps a mutation (create/update/delete) with pending + error state. */
/**
* Outcome of a mutation. Discriminated on `ok` so callers MUST branch before
* touching `value` — this makes error handling compile-time-visible and means
* `run()` NEVER throws (no more unhandled rejections from async event handlers).
* Works for void mutations too (delete): success is `{ ok: true, value: undefined }`.
*/
export type MutationOutcome<T> =
| { ok: true; value: T }
| { ok: false; error: string; cause: unknown }
/**
* Wraps a mutation (create/update/delete) with pending + error state.
* `run()` resolves to a MutationOutcome and never rejects; `error` also drives
* inline UI as before. Call sites: `const r = await m.run(x); if (r.ok) …`.
*/
export function useMutation<TArgs extends unknown[], TResult>(
mutator: (...args: TArgs) => Promise<TResult>,
): {
run: (...args: TArgs) => Promise<TResult>
run: (...args: TArgs) => Promise<MutationOutcome<TResult>>
pending: boolean
error: string | null
} {
@@ -79,14 +93,16 @@ export function useMutation<TArgs extends unknown[], TResult>(
const [error, setError] = useState<string | null>(null)
const run = useCallback(
async (...args: TArgs) => {
async (...args: TArgs): Promise<MutationOutcome<TResult>> => {
setPending(true)
setError(null)
try {
return await mutator(...args)
const value = await mutator(...args)
return { ok: true, value }
} catch (err) {
setError(messageFor(err))
throw err
const message = messageFor(err)
setError(message)
return { ok: false, error: message, cause: err }
} finally {
setPending(false)
}