UNPKG

@zerothrow/react

Version:

React hooks for type-safe error handling with Result types. Stop throwing, start returning.

276 lines (270 loc) 7.86 kB
import { Result } from '@zerothrow/core'; export { Result, ZT, ZeroThrow } from '@zerothrow/core'; import { Policy } from '@zerothrow/resilience'; import * as react from 'react'; import { Context, Component, ReactNode, ErrorInfo } from 'react'; interface UseResultOptions { /** * Whether to execute the function immediately on mount * @default true */ immediate?: boolean; /** * Dependencies array for re-execution */ deps?: React.DependencyList; } interface UseResultReturn<T, E extends Error> { /** * The current Result value (undefined while loading) */ result: Result<T, E> | undefined; /** * Whether the async operation is in progress */ loading: boolean; /** * Manually trigger execution */ reload: () => void; /** * Reset to initial state */ reset: () => void; } /** * React hook for handling async operations that return Results. * * @example * ```tsx * const { result, loading, reload } = useResult( * async () => { * const response = await fetch('/api/user') * if (!response.ok) return ZT.err(new Error('Failed to fetch')) * const data = await response.json() * return ZT.ok(data) * }, * { deps: [userId] } * ) * * if (loading) return <Spinner /> * * return result?.match({ * ok: user => <UserProfile {...user} />, * err: error => <ErrorMessage error={error} /> * }) ?? null * ``` */ declare function useResult<T, E extends Error = Error>(fn: () => Promise<Result<T, E>> | Result<T, E>, options?: UseResultOptions): UseResultReturn<T, E>; type CircuitState = 'closed' | 'open' | 'half-open'; interface UseResilientResultOptions { /** * Whether to execute the function immediately on mount * @default true */ immediate?: boolean; /** * Dependencies array for re-execution */ deps?: React.DependencyList; } interface UseResilientResultReturn<T, E extends Error> { /** * The current Result value (undefined while loading) */ result: Result<T, E> | undefined; /** * Whether the async operation is in progress */ loading: boolean; /** * Number of retry attempts made */ retryCount: number; /** * Timestamp when the next retry will occur (if applicable) */ nextRetryAt: number | undefined; /** * Current state of the circuit breaker (if using CircuitBreakerPolicy) */ circuitState: CircuitState | undefined; /** * Manually trigger execution */ reload: () => void; /** * Reset to initial state */ reset: () => void; } /** * React hook for handling async operations with resilience policies. * * @example * ```tsx * import { RetryPolicy, CircuitBreakerPolicy } from '@zerothrow/resilience' * * const policy = RetryPolicy.exponential({ maxRetries: 3 }) * .chain(CircuitBreakerPolicy.create({ * failureThreshold: 5, * resetTimeout: 30000 * })) * * const { result, loading, retryCount, nextRetryAt } = useResilientResult( * async () => { * const response = await fetch('/api/flaky-endpoint') * if (!response.ok) throw new Error('Request failed') * return response.json() * }, * policy, * { deps: [userId] } * ) * * if (loading) { * return nextRetryAt * ? <div>Retrying in {timeUntil(nextRetryAt)}...</div> * : <Spinner /> * } * * return result?.match({ * ok: data => <DataView {...data} />, * err: error => <ErrorView error={error} retries={retryCount} /> * }) ?? null * ``` */ declare function useResilientResult<T, E extends Error = Error>(fn: () => Promise<T>, policy: Policy, options?: UseResilientResultOptions): UseResilientResultReturn<T, E>; declare class ContextError extends Error { readonly code: "CONTEXT_NOT_FOUND"; readonly contextName: string; constructor(contextName: string); } /** * A Result-based version of React's useContext hook. * * Instead of throwing when context is not available, this hook returns * a Result that can be pattern matched for safe error handling. * * @example * ```tsx * const ThemeContext = createContext<Theme | undefined>(undefined) * * function MyComponent() { * const themeResult = useResultContext(ThemeContext) * * return themeResult.match({ * ok: (theme) => <div style={{ color: theme.primary }}>Themed</div>, * err: (error) => <div>No theme provider found</div> * }) * } * ``` */ declare function useResultContext<T>(context: Context<T | undefined>, options?: { /** Custom context name for better error messages */ contextName?: string; }): Result<T, Error>; /** * A Result-based version of React's useContext hook that handles null values. * * This variant treats both undefined and null as missing context values. * * @example * ```tsx * const AuthContext = createContext<User | null>(null) * * function Profile() { * const userResult = useResultContextNullable(AuthContext) * * return userResult.match({ * ok: (user) => <div>Welcome {user.name}</div>, * err: () => <div>Please log in</div> * }) * } * ``` */ declare function useResultContextNullable<T>(context: Context<T | undefined | null>, options?: { /** Custom context name for better error messages */ contextName?: string; }): Result<T, Error>; /** * Creates a Result-based context with a companion hook. * * This helper creates both a Context and a custom hook that uses * useResultContext internally, providing a complete solution for * Result-based context patterns. * * @example * ```tsx * const { Provider, useContext } = createResultContext<UserSettings>('UserSettings') * * // In your app * <Provider value={settings}> * <App /> * </Provider> * * // In a component * function Profile() { * const settingsResult = useContext() * * return settingsResult.match({ * ok: (settings) => <div>{settings.name}</div>, * err: () => <div>No settings available</div> * }) * } * ``` */ declare function createResultContext<T>(contextName: string): { Provider: react.Provider<T | undefined>; useContext: () => Result<T, Error>; Context: Context<T | undefined>; }; interface ResultBoundaryProps { /** * Fallback component to render when an error is caught */ fallback: (result: Result<never, Error>, reset: () => void) => ReactNode; /** * Optional error handler for logging/telemetry */ onError?: (error: Error, errorInfo: ErrorInfo) => void; /** * Children to render when no error */ children: ReactNode; } interface ResultBoundaryState { hasError: boolean; error: Error | null; } /** * Error boundary that converts thrown errors to Result types. * * Unlike standard error boundaries, this provides the error as a Result * to the fallback component, enabling type-safe error handling. * * @example * ```tsx * <ResultBoundary * fallback={(result, reset) => ( * <ErrorFallback * error={result.error} * onRetry={reset} * /> * )} * onError={(error, info) => { * console.error('Boundary caught:', error) * sendToTelemetry(error, info) * }} * > * <App /> * </ResultBoundary> * ``` */ declare class ResultBoundary extends Component<ResultBoundaryProps, ResultBoundaryState> { constructor(props: ResultBoundaryProps); static getDerivedStateFromError(error: Error): ResultBoundaryState; componentDidCatch(error: Error, errorInfo: ErrorInfo): void; reset: () => void; render(): ReactNode; } export { ContextError, ResultBoundary, type ResultBoundaryProps, type UseResilientResultOptions, type UseResilientResultReturn, type UseResultOptions, type UseResultReturn, createResultContext, useResilientResult, useResult, useResultContext, useResultContextNullable };