Пытаюсь подружить реакт с промисами. React Query и прочую муть не предлагать.
Пока родился такой вариант. Думаю, из сигнатур очевидно, как оно должно работать. Не нравится то, что reducer получился «не чистый». Вызывает abortController.abort(). Хотя и идемпотентный но кажется это всё равно не по феншую.
Как сделать нормально - я не придумал. Хотя думал много и это далеко не первая итерация. Буду благодарен помощи от гуру, если у кого будет хорошая идея.
import { DependencyList, useEffect, useReducer } from "react";
type PromiseFunction<T> = (signal: AbortSignal) => Promise<T>;
type Result<T> =
| { status: "pending" }
| { status: "fulfilled"; value: T }
| { status: "rejected"; reason: unknown };
type State<T> =
| { status: "uninitialized" }
| { status: "pending"; id: number; abortController: AbortController }
| { status: "fulfilled"; value: T }
| { status: "rejected"; reason: unknown };
type Action<T> =
| { type: "init"; id: number; abortController: AbortController }
| { type: "resolve"; id: number; value: T }
| { type: "reject"; id: number; reason: unknown }
| { type: "clean" };
function reducer<T>(state: State<T>, action: Action<T>): State<T> {
switch (action.type) {
case "init":
switch (state.status) {
case "uninitialized":
return {
status: "pending",
id: action.id,
abortController: action.abortController,
};
}
break;
case "resolve":
switch (state.status) {
case "uninitialized":
return state;
case "pending":
if (action.id < state.id) {
return state;
}
if (action.id == state.id) {
return { status: "fulfilled", value: action.value };
}
break;
}
break;
case "reject":
switch (state.status) {
case "uninitialized":
return state;
case "pending":
if (action.id < state.id) {
return state;
}
if (action.id == state.id) {
return { status: "rejected", reason: action.reason };
}
break;
}
break;
case "clean":
switch (state.status) {
case "pending":
state.abortController.abort();
return { status: "uninitialized" };
case "fulfilled":
case "rejected":
return { status: "uninitialized" };
}
break;
}
console.error("Unexpected state", state, action);
return state;
}
function loggingReducer<S, A>(
reducer: (state: S, action: A) => S,
): (state: S, action: A) => S {
return (state, action) => {
try {
const nextState = reducer(state, action);
console.log(state, action, nextState);
return nextState;
} catch (e) {
console.log(state, action, e);
throw e;
}
};
}
let nextId = 1;
export default function usePromise<T>(
promiseFunction: PromiseFunction<T>,
deps: DependencyList,
): Result<T> {
const [state, dispatch] = useReducer(loggingReducer(reducer<T>), {
status: "uninitialized",
});
useEffect(() => {
const id = nextId++;
const abortController = new AbortController();
dispatch({ type: "init", id, abortController });
promiseFunction(abortController.signal).then(
(value) => dispatch({ type: "resolve", id, value }),
(reason) => dispatch({ type: "reject", id, reason }),
);
return () => dispatch({ type: "clean" });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
switch (state.status) {
case "uninitialized":
case "pending":
return { status: "pending" };
case "fulfilled":
return { status: "fulfilled", value: state.value };
case "rejected":
return { status: "rejected", reason: state.reason };
}
}