@zerothrow/react
Version:
React hooks for type-safe error handling with Result types. Stop throwing, start returning.
240 lines (238 loc) • 7.15 kB
JavaScript
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