发布于 2026-01-06 1 阅读
0

注意 React.useEffect 竞态条件 🐛 BUGS 总结 DEV 全球展示挑战赛,由 Mux 呈现:展示你的项目!

注意 React.useEffect 竞态条件 🐛 错误

概括

由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!

ReactuseEffect引入竞态条件错误是很常见的。这种情况可能发生在任何时候,只要你在代码中包含异步代码React.useEffect

什么是竞态条件错误?

当两个异步进程同时更新同一个值时,就会发生竞态条件。在这种情况下,最终更新该值的是最后一个完成的进程。

这可能并非我们所愿。我们或许希望启动最后一个进程来更新该值。

例如,某个组件会获取数据,然后重新渲染并重新获取数据。

示例竞态条件组件

这是一个可能存在竞态条件缺陷的组件示例

import { useEffect, useState } from "react";
import { getPerson } from "./api";

export const Race = ({ id }) => {
    const [person, setPerson] = useState(null);

    useEffect(() => {
        setPerson(null);

        getPerson(id).then((person) => {
            setPerson(person);
        };
    }, [id]);

    return person ? `${id} = ${person.name}` : null;
}

乍一看,这段代码似乎没有任何问题,而这正是这个漏洞如此危险的原因。

useEffect每次id更改时都会触发并调用getPerson。如果getPerson已启动且发生id更改,则会启动第二次调用getPerson

如果第一次调用在第二次调用之前完成,那么第二次调用会person用第一次调用的数据覆盖第二次调用的数据,从而导致我们的应用程序出现错误。

中止控制器

使用时fetch,您可以使用AbortController手动中止第一个请求。

注意:稍后我们会找到更简单的方法来实现这个功能。这段代码仅用于教学目的。

import { useEffect, useRef, useState } from "react";
import { getPerson } from "./api";

export const Race = ({ id }) => {
    const [data, setData] = useState(null);
    const abortRef = useRef(null);

    useEffect(() => {
        setData(null);

        if (abortRef.current != null) {
            abortRef.current.abort();
        }

        abortRef.current = new AbortController();

        fetch(`/api/${id}`, { signal: abortRef.current.signal })
            .then((response) => {
                abortRef.current = null;
                return response;
            })
            .then((response) => response.json())
            .then(setData);
    }, [id]);

    return data;
}

取消之前的请求

对于我们来说,这AbortController并非总是可行的,因为有些异步代码无法与异步操作一起使用AbortController。所以我们仍然需要一种方法来取消之前的异步调用。

这可以通过cancelled在内部设置一个标志来实现useEffect。我们可以利用该功能在更改true时设置此标志idunmountuseEffect

注意:稍后我们会找到更简单的方法来实现这个功能。这段代码仅用于教学目的。

import { useEffect, useState } from "react";
import { getPerson } from "./api";

export const Race = ({ id }) => {
    const [person, setPerson] = useState(null);

    useEffect(() => {
        let cancelled = false;
        setPerson(null);

        getPerson(id).then((person) => {
            if (cancelled) return; // only proceed if NOT cancelled
            setPerson(person);
        };

        return () => {
            cancelled = true; // cancel if `id` changes
        };
    }, [id]);

    return person ? `${id} = ${person.name}` : null;
}

使用 React 查询

我不建议在每个组件中手动处理中止或取消操作。相反,你应该将该功能封装在一个 React Hook 中。幸运的是,已经有一个库为我们完成了这项工作。

我建议使用react-query库。这个库可以防止竞态条件错误,还提供了一些其他实用功能,例如缓存、重试等等。

我也很喜欢react-query简化代码的方式。

import { useQuery } from "react-query";
import { getPerson } from "./api";

export const Race = ({ id }) => {
    const { isLoading, error, data } = useQuery(
        ["person", id],
        (key, id) => getPerson(id)
    );

    if (isLoading) return "Loading...";
    if (error) return `ERROR: ${error.toString()}`;
    return `${id} = ${data.name}`;
}

react-query的第一个参数是缓存键,第二个参数是一个函数,当没有缓存、缓存过期或无效时,该函数将被调用。

概括

当内部存在异步调用React.useEffectReact.useEffect再次触发时,可能会出现竞态条件错误。

使用时fetch,您可以中止请求。APromise可以被取消。但我建议不要为每个组件手动编写该代码,而是使用像react-query这样的库。

请在joel.net上订阅我的新闻简报。

在 Twitter 上关注我@joelnet,或在 YouTube 上关注JoelCodes 。

干杯🍻

文章来源:https://dev.to/joelnet/beware-of-react-useeffect-race-condition-bugs-42hl