React请求竞态

2023/10/17 JSReact

🌙 what - 什么是请求竞态?

比如: 在react组件中,组件接收一个props id作为请求的参数,获取后端数据更新组件内容:

import React, { useEffect, useState } from 'react';

export default function DataDisplayer(props) {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`https://swapi.dev/api/people/${props.id}/`);
      const newData = await response.json();
      setData(newData);
    };

    fetchData();
  }, [props.id]);

  if (data) {
    return <div>{data.name}</div>;
  } else {
    return null;
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

如果组件接收的id频繁切换,由于网络波动问题,新的id已经展示,但是上一次的fetchData响应数据晚于当前的fetchData响应数据,就会导致当前的id与展示的内容不符。这个现象就是请求竞态问题

我们可以使用一个setTimeout函数模拟网络超时:

export default function DataDisplayer(props) {
  const [data, setData] = useState(null);
  const [fetchedId, setFetchedId] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setTimeout(async () => {
        const response = await fetch(
          `https://swapi.dev/api/people/${props.id}/`
        );
        const newData = await response.json();

        setFetchedId(props.id);
        setData(newData);
      }, Math.round(Math.random() * 12000));
    };

    fetchData();
  }, [props.id]);

  if (data) {
    return (
      <div>
        <p style={{ color: fetchedId === props.id ? 'green' : 'red' }}>
          Displaying Data for: {fetchedId}
        </p>
        <p>{data.name}</p>
      </div>
    );
  } else {
    return null;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

在多次切换id的时候,之前的响应就很有可能晚于后续的响应,这样就可以促发这个bug了。

codesandbox example (opens new window)

🌙 how - 解决方案

为了方便测试,下面的代码都增加了setTimeout延迟响应。

🌙 1.使用一个标志位清除状态:

useEffect(() => {
  let active = true;

  const fetchData = async () => {
    setTimeout(async () => {
      const response = await fetch(`https://swapi.dev/api/people/${props.id}/`);
      const newData = await response.json();
      if (active) {
        setFetchedId(props.id);
        setData(newData);
      }
    }, Math.round(Math.random() * 12000));
  };

  fetchData();
  return () => {
    active = false;
  };
}, [props.id]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

🌙 2.使用AbortController (opens new window)取消请求:

useEffect(() => {
  const abortController = new AbortController();

  const fetchData = async () => {
    setTimeout(async () => {
      try {
        const response = await fetch(`https://swapi.dev/api/people/${id}/`, {
          signal: abortController.signal,
        });
        const newData = await response.json();

        setFetchedId(id);
        setData(newData);
      } catch (error) {
        if (error.name === 'AbortError') {
          // Aborting a fetch throws an error
          // So we can't update state afterwards
        }
        // Handle other request errors here
      }
    }, Math.round(Math.random() * 12000));
  };

  fetchData();
  return () => {
    abortController.abort();
  };
}, [id]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

这样一来, 只要id变化了,就会把上一次请求中的状态清除,保证每次的id与响应都匹配。

Fixing Race Conditions in React with useEffect (opens new window)