UNPKG

@zerothrow/react

Version:

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

254 lines (251 loc) 7.56 kB
'use strict'; var react = require('react'); var core = require('@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] = react.useReducer(reducer, { result: void 0, loading: immediate }); const isMountedRef = react.useRef(true); const abortControllerRef = react.useRef(); const execute = react.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 = core.ZT.err( error instanceof Error ? error : new Error(String(error)) ); dispatch({ type: "SUCCESS", result: errorResult }); } } }, deps); const reset = react.useCallback(() => { abortControllerRef.current?.abort(); dispatch({ type: "RESET" }); }, []); react.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] = react.useReducer(reducer2, { result: void 0, loading: immediate, retryCount: 0, nextRetryAt: void 0, circuitState: void 0 }); const isMountedRef = react.useRef(true); const abortControllerRef = react.useRef(); const retryTimeoutRef = react.useRef(); const execute = react.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 = core.ZT.err( error instanceof Error ? error : new Error(String(error)) ); dispatch({ type: "SUCCESS", result: errorResult }); } } }, [fn, policy, ...deps]); const reset = react.useCallback(() => { abortControllerRef.current?.abort(); if (retryTimeoutRef.current) { clearTimeout(retryTimeoutRef.current); retryTimeoutRef.current = void 0; } dispatch({ type: "RESET" }); }, []); react.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 = react.useContext(context); if (value === void 0) { const contextName = options?.contextName || context.displayName || "Unknown"; return core.ZT.err(new ContextError(contextName)); } return core.ZT.ok(value); } function useResultContextNullable(context, options) { const value = react.useContext(context); if (value === void 0 || value === null) { const contextName = options?.contextName || context.displayName || "Unknown"; return core.ZT.err(new ContextError(contextName)); } return core.ZT.ok(value); } function createResultContext(contextName) { const Context = react.createContext(void 0); Context.displayName = contextName; return { Provider: Context.Provider, useContext: () => useResultContext(Context, { contextName }), Context }; } var ResultBoundary = class extends react.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 = core.ZT.err(this.state.error); return this.props.fallback(result, this.reset); } return this.props.children; } }; Object.defineProperty(exports, "ZT", { enumerable: true, get: function () { return core.ZT; } }); Object.defineProperty(exports, "ZeroThrow", { enumerable: true, get: function () { return core.ZeroThrow; } }); exports.ResultBoundary = ResultBoundary; exports.createResultContext = createResultContext; exports.useResilientResult = useResilientResult; exports.useResult = useResult; exports.useResultContext = useResultContext; exports.useResultContextNullable = useResultContextNullable; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map