Как предотвратить состояние гонки в перехвате реакции? - PullRequest
0 голосов
/ 16 июня 2020

Я написал удобную ловушку для React, которая отслеживает, выполняется ли обещание, есть ли ошибка и каковы результаты. Он используется так:

const MyComponent = (props: IProps) => {
  const [promise, setPromise} = useState<Promise | undefined>();
  const {
    hasRun,
    isWaiting,
    results,
    error
  } = useService(promise);

  const doSomething = () => {
    setPromise(runSomeAsyncCode());
  };

  return (
    <div>...</div>
  );
};

Это не более чем группа состояний, которые обновляются при запуске, выполнении, успешном или неудачном выполнении обещания:

export const useService = <T>(promise?: Promise<T>) => {
    const [hasRun, setHasRun] = useState<boolean>(false);
    const [isWaiting, setIsWaiting] = useState<boolean>(false);
    const [error, setError] = useState<Error | undefined>(undefined);
    const [results, setResults] = useState<T | undefined>();

    useEffect(() => {
        if (!promise) {
            return;
        }

        (async () => {
            if (!hasRun) {
                setHasRun(true);
            }
            setError(undefined);

            setIsWaiting(true);
            try {
                const r = await promise;
                setResults(r);
            } catch (err) {
                setResults(undefined);
                setError(err);
            } finally {
                setIsWaiting(false);
            }
        })();
    }, [promise]);

    return {
        error,
        hasRun,
        isWaiting,
        results,
    };
};

Моя проблема в том, что если обещание обновляется до разрешения предыдущего обещания, то результаты или ошибка предыдущего обещания все равно будут возвращены компоненту. Например, запуск пары AJAX запросов, где первый не выполняется через минуту, а второй разрешается за пару секунд, приводит к первоначальному успеху, но затем к сообщению об ошибке через минуту.

Как я могу изменить ловушку, чтобы она не вызывала setState для ошибки или успеха, если обещание за это время изменилось?

Codepen: https://codepen.io/whiterook6/pen/gOPwJGq

1 Ответ

1 голос
/ 16 июня 2020

Почему бы не отслеживать текущий promise и не вносить изменения, если обещание изменилось?

export const useService = <T>(promise?: Promise<T>) => {
    const [hasRun, setHasRun] = useState<boolean>(false);
    const [isWaiting, setIsWaiting] = useState<boolean>(false);
    const [error, setError] = useState<Error | undefined>(undefined);
    const [results, setResults] = useState<T | undefined>();

    const promiseRef = useRef(promise);
    promiseRef.current = promise; // ** keep ref always up to date

    useEffect(() => {
        if (!promise) {
            return;
        }

        (async () => {
            setHasRun(true);
            setError(undefined);

            setIsWaiting(true);
            try {
                const r = await promise;
                if (promiseRef.current !== promise) {
                    return;
                }
                setResults(r);
                setIsWaiting(false);
            } catch (err) {
                if (promiseRef.current !== promise) {
                    return;
                }
                setResults(undefined);
                setError(err);
                setIsWaiting(false);
            }
        })();

        // you may want to reset states when the promise changes
        return () => {
            setHasRun(false);
            setIsWaiting(false);
            setError(undefined);
            setResults(undefined);
        }
    }, [promise]);

    return {
        error,
        hasRun,
        isWaiting,
        results,
    };
};

useRef не только для ссылок на элементы DOM, а docs указывают.

По сути, useRef подобен «ящику», который может содержать изменяемое значение в своем свойстве .current. [...] Изменение свойства .current не вызывает повторного рендеринга.

Причина, по которой я использовал useRef здесь, в том, что нам нужно изменяемое значение, которое может содержать последние promise аргумент, не вызывая повторной визуализации. Поскольку promiseRef никогда не изменяется (изменяется только .current), вы можете назначить последнее значение в строке с помощью ** и получить к нему доступ в функции asyn c, даже после того, как прошло время и компонент был повторно отрисован.

...