如何防止反应挂钩中的竞争条件?

How do I prevent a race condition in a react hook?

我已经为 React 写了一个方便的钩子来跟踪一个 promise 是否 运行,是否有错误,以及结果是什么。它是这样使用的:

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

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

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

它只不过是一组随着 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>();

    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,
    };
};

我的问题是如果 promise 在前一个 promise 解决之前更新,那么前一个 promise 的结果或错误仍将返回到组件。例如,启动几个 AJAX 请求,第一个请求在一分钟后失败,而第二个请求在几秒钟内解决,导致最初成功,但一分钟后报告失败。

如果 promise 在此期间发生了变化,我该如何修改挂钩,使其不会因错误或成功而调用 setState?

代码笔:https://codepen.io/whiterook6/pen/gOPwJGq

为什么不跟踪当前的 promise,如果 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,
    };
};
正如 docs 指出的那样,

useRef 不仅仅用于 DOM 元素引用。

Essentially, useRef is like a “box” that can hold a mutable value in its .current property. [...] Mutating the .current property doesn’t cause a re-render.

我在这里使用 useRef 的原因是因为我们需要一个可变值来保存最新的 promise 参数而不会导致重新渲染。因为 promiseRef 永远不会改变(只有 .current 会改变),你可以在 ** 的行上分配最新的值并在异步函数中访问它,即使在时间过去并且组件有重新渲染。