LINUX.ORG.RU

Помогите улучшить хук usePromise

 


0

1

Пытаюсь подружить реакт с промисами. 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 };
  }
}

★★★

Я не настоящий фронтендер, но я бы на твоём месте взял исходный код существующих реализаций хука usePromise и подсмотрел, как оно там сделано.

theNamelessOne ★★★★★
()