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