UNPKG

@zerothrow/react

Version:

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

240 lines (238 loc) 7.15 kB
import { useReducer, useRef, useCallback, useEffect, useContext, createContext, Component } from 'react'; import { ZT } from '@zerothrow/core'; export { ZT, ZeroThrow } from '@zerothrow/core'; // src/hooks/useResult.ts function reducer(state, action) { switch (action.type) { case "LOADING": return { ...state, loading: true }; case "SUCCESS": return { result: action.result, loading: false }; case "RESET": return { result: void 0, loading: false }; default: return state; } } function useResult(fn, options = {}) { const { immediate = true, deps = [] } = options; const [state, dispatch] = useReducer(reducer, { result: void 0, loading: immediate }); const isMountedRef = useRef(true); const abortControllerRef = useRef(); const execute = useCallback(async () => { abortControllerRef.current?.abort(); abortControllerRef.current = new AbortController(); dispatch({ type: "LOADING" }); try { const result = await fn(); if (isMountedRef.current && !abortControllerRef.current.signal.aborted) { dispatch({ type: "SUCCESS", result }); } } catch (error) { if (isMountedRef.current && !abortControllerRef.current.signal.aborted) { const errorResult = ZT.err( error instanceof Error ? error : new Error(String(error)) ); dispatch({ type: "SUCCESS", result: errorResult }); } } }, deps); const reset = useCallback(() => { abortControllerRef.current?.abort(); dispatch({ type: "RESET" }); }, []); useEffect(() => { if (immediate) { execute(); } return () => { isMountedRef.current = false; abortControllerRef.current?.abort(); }; }, [execute, immediate]); return { result: state.result, loading: state.loading, reload: execute, reset }; } function reducer2(state, action) { switch (action.type) { case "LOADING": return { ...state, loading: true }; case "SUCCESS": return { ...state, result: action.result, loading: false, nextRetryAt: void 0 }; case "RETRY_SCHEDULED": return { ...state, nextRetryAt: action.nextRetryAt, retryCount: action.retryCount }; case "CIRCUIT_STATE_CHANGED": return { ...state, circuitState: action.state }; case "RESET": return { result: void 0, loading: false, retryCount: 0, nextRetryAt: void 0, circuitState: void 0 }; default: return state; } } function useResilientResult(fn, policy, options = {}) { const { immediate = true, deps = [] } = options; const [state, dispatch] = useReducer(reducer2, { result: void 0, loading: immediate, retryCount: 0, nextRetryAt: void 0, circuitState: void 0 }); const isMountedRef = useRef(true); const abortControllerRef = useRef(); const retryTimeoutRef = useRef(); const execute = useCallback(async () => { abortControllerRef.current?.abort(); abortControllerRef.current = new AbortController(); if (retryTimeoutRef.current) { clearTimeout(retryTimeoutRef.current); retryTimeoutRef.current = void 0; } dispatch({ type: "LOADING" }); let wrappedPolicy = policy; if ("onRetry" in policy) { wrappedPolicy = policy.onRetry((attempt, _error, delay) => { if (isMountedRef.current && !abortControllerRef.current?.signal.aborted) { dispatch({ type: "RETRY_SCHEDULED", nextRetryAt: Date.now() + delay, retryCount: attempt }); } }); } if ("onCircuitStateChange" in wrappedPolicy) { wrappedPolicy.onCircuitStateChange((state2) => { if (isMountedRef.current && !abortControllerRef.current?.signal.aborted) { dispatch({ type: "CIRCUIT_STATE_CHANGED", state: state2 }); } }); } try { const result = await wrappedPolicy.execute(fn); if (isMountedRef.current && !abortControllerRef.current?.signal.aborted) { dispatch({ type: "SUCCESS", result }); } } catch (error) { if (isMountedRef.current && !abortControllerRef.current?.signal.aborted) { const errorResult = ZT.err( error instanceof Error ? error : new Error(String(error)) ); dispatch({ type: "SUCCESS", result: errorResult }); } } }, [fn, policy, ...deps]); const reset = useCallback(() => { abortControllerRef.current?.abort(); if (retryTimeoutRef.current) { clearTimeout(retryTimeoutRef.current); retryTimeoutRef.current = void 0; } dispatch({ type: "RESET" }); }, []); useEffect(() => { if (immediate) { execute(); } return () => { isMountedRef.current = false; abortControllerRef.current?.abort(); if (retryTimeoutRef.current) { clearTimeout(retryTimeoutRef.current); } }; }, [execute, immediate]); return { result: state.result, loading: state.loading, retryCount: state.retryCount, nextRetryAt: state.nextRetryAt, circuitState: state.circuitState, reload: execute, reset }; } var ContextError = class extends Error { code = "CONTEXT_NOT_FOUND"; contextName; constructor(contextName) { super(`useResultContext: Context "${contextName}" not found. Did you forget to wrap your component in a provider?`); this.name = "ContextError"; this.contextName = contextName; } }; function useResultContext(context, options) { const value = useContext(context); if (value === void 0) { const contextName = options?.contextName || context.displayName || "Unknown"; return ZT.err(new ContextError(contextName)); } return ZT.ok(value); } function useResultContextNullable(context, options) { const value = useContext(context); if (value === void 0 || value === null) { const contextName = options?.contextName || context.displayName || "Unknown"; return ZT.err(new ContextError(contextName)); } return ZT.ok(value); } function createResultContext(contextName) { const Context = createContext(void 0); Context.displayName = contextName; return { Provider: Context.Provider, useContext: () => useResultContext(Context, { contextName }), Context }; } var ResultBoundary = class extends Component { constructor(props) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error) { return { hasError: true, error }; } componentDidCatch(error, errorInfo) { this.props.onError?.(error, errorInfo); } reset = () => { this.setState({ hasError: false, error: null }); }; render() { if (this.state.hasError && this.state.error) { const result = ZT.err(this.state.error); return this.props.fallback(result, this.reset); } return this.props.children; } }; export { ResultBoundary, createResultContext, useResilientResult, useResult, useResultContext, useResultContextNullable }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map