React.useEffect 경쟁 조건을 조심하세요 🐛 버그

14836 단어 reactjavascript
React의 useEffect에서 Race Condition Bug를 소개하는 것은 매우 일반적입니다. 이것은 React.useEffect 내부에 비동기 코드가 있을 때마다 발생할 수 있습니다.

레이스 컨디션 버그란 무엇입니까?



경쟁 조건은 동일한 값을 업데이트하는 두 개의 비동기 프로세스가 있을 때 발생할 수 있습니다. 이 시나리오에서는 값 업데이트를 완료하는 마지막 프로세스입니다.

이것은 우리가 원하는 것이 아닐 수도 있습니다. 값을 업데이트하기 위해 마지막 프로세스가 시작되기를 원할 수 있습니다.

예를 들어 데이터를 가져온 다음 데이터를 다시 렌더링하고 다시 가져오는 구성 요소가 있습니다.

경쟁 조건 구성 요소의 예



이것은 Race Condition Bug가 있을 수 있는 구성 요소의 예입니다.

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

언뜻 보기에 이 코드에는 아무런 문제가 없어 보이지만 이것이 이 버그를 매우 위험하게 만들 수 있습니다.
useEffectid이 변경될 때마다 실행되고 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 플래그를 설정하여 가능합니다. trueid 기능을 사용하여 unmount 이 변경될 때 이를 useEffect 으로 설정할 수 있습니다.

참고: 나중에 이 작업을 수행하는 더 간단한 방법을 찾을 것입니다. 이 코드는 교육용입니다.

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 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.useEffect 내부에 비동기 호출이 있고 React.useEffect이 다시 실행될 때 발생할 수 있습니다.
fetch 을 사용할 때 요청을 중단할 수 있습니다. Promise 취소 가능합니다. 그러나 각 구성 요소에 대해 해당 코드를 수동으로 작성하지 않고 대신 react-query과 같은 라이브러리를 사용하는 것이 좋습니다.

joel.net에서 내 뉴스 레터를 구독하십시오.

트위터나 유튜브에서 나를 찾아줘

건배 🍻

좋은 웹페이지 즐겨찾기