@zerothrow/react
Version:
React hooks for type-safe error handling with Result types. Stop throwing, start returning.
276 lines (270 loc) • 7.86 kB
text/typescript
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 };