【问题标题】:Used latest state in Fetch API `then()`在 Fetch API `then()` 中使用了最新状态
【发布时间】:2020-05-20 18:20:02
【问题描述】:

我正在开发一个使用 Fetch API fetch 获取数据的 REACT 钩子组件。虽然,我面临一个我不太确定如何解决的问题,或者更好地说,我不确定是否有任何“推荐”的方式来解决这个问题。

以这段代码为例:

const Fetcher = () => {
    [data, setData] = useState([]);

    const handleButtonClick = (e) => {
        fetch('http://www.myapi.com')
            .then(response => response.json())
            .then(json => {
                const newData = [...data];
                newData.push(json);

                setData(newData);
            });
    }

    return <button onClick={handleButtonClick}>Click to fetch</button>
}

这不是一个有效的例子,但很清楚发生了什么:我单击一个按钮并获取一些东西;某些东西被添加到当前状态。

现在,问题出在哪里?当我写const newData = [...data] 时,我考虑的是fetch 启动时可用的data 变量,而不是当前的data。这意味着,如果 fetch 需要 1 分钟才能执行,那么在那一分钟内 data 可能会以其他方式更新,因此,当调用 fetch.then().then() 时,我会覆盖实际当前的 data一些不正确的东西。

我可以给你这个方案,让你更好地理解:

  1. 我单击并执行第一个fetchdata 等于[ ]
  2. 我在第一个 fetch 结束前再次单击(同样,data 等于 [ ]
  3. 第二次提取结束并在data 中保存新值(例如['foo']);
  4. 第一次提取结束接收'bar'。由于它使用的是空数组data,因此将数组['bar']保存在data中;

如您所见,归根结底,我有一个数组['bar'],而它应该是['foo', 'bar']

对于这个问题,我想出了两个解决方案:

  1. ref 中保存一份状态副本,并在fetch.then().then() 中使用它。像这样的:

    const Fetcher = () => {
        [data, setData] = useState([]);
        const refData = useRef(data);
    
        const handleButtonClick = (e) => {
            fetch('http://www.myapi.com')
                .then(response => response.json())
                .then(json => {
                    const newData = [...refData.current];
                    newData.push(json);
    
                    setData(newData);
                });
        }
    
        useEffect(() => {
            refData.current = data;
        }, [data]);
    
        return <button onClick={handleButtonClick}>Click to fetch</button>
    }
    
  2. 使用临时变量和 useEffect 来处理最新的变量:

    const Fetcher = () => {
        [data, setData] = useState([]);
        [lastFetchedData, setLastFetchedData] = useState();
    
        const handleButtonClick = (e) => {
            fetch('http://www.myapi.com')
                .then(response => response.json())
                .then(json => {
                    setLastFetchedData(json);
                });
        }
    
        useEffect(() => {
            const newData = [...data];
            newData.push(lastFetchedData);
    
            setData(newData);
        }, [lastFetchedData]);
    
        return <button onClick={handleButtonClick}>Click to fetch</button>
    }
    

我很确定它们都可以正常工作而不会产生任何重大问题,但是:

  1. 第一种方法:我不知道,在我看来这违背了 REACT 的思维方式。我正在使用 ref 在某处保持状态最新。我的意思是,我不知道这对于 REACT“生活方式”是否可行;

  2. 第二种方法:在这种情况下,我只使用状态。我在这里不喜欢的是,每当获取一些数据时,我都会进行额外的渲染。没什么大不了的,但是,你知道,如果可以避免它会更好。如果lastFetchedData 不是null,我可以返回一个空页面,但用户可能会看到该空页面一毫秒。实际上,它不会看到空白页面,但可能由于两次渲染导致页面闪烁;

【问题讨论】:

    标签: javascript reactjs react-hooks


    【解决方案1】:

    通常的解决方法是使用setData的回调形式:

    const Fetcher = () => {
        [data, setData] = useState([]);
    
        const handleButtonClick = (e) => {
            fetch('http://www.myapi.com')
                .then(response => response.json())
                .then(json => {
                    setData(data => [...data, json]); // <============
                });
        }
    
        return <button onClick={handleButtonClick}>Click to fetch</button>
    }
    

    旁注:

    1. 您的代码正在成为我描述 herefetch API 中的猎物。在调用json之前,您需要检查response.ok

    2. 您没有处理来自fetch 的错误。

    3. 您忘记声明 datasetData(我建议使用严格模式,所以这是一个错误):

    所以:

    const Fetcher = () => {
        const [data, setData] = useState([]);
    //  ^^^^^−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− #3
    
        const handleButtonClick = (e) => {
            fetch('http://www.myapi.com')
                .then(response => {
                    if (!response.ok) {                                   // #1
                        throw new Error("HTTP error " + response.status); // #1
                    }                                                     // #1
                    return response.json();
                })
                .then(json => {
                    setData(data => [...data, json]);
                })
                .catch(error => {                                         // #2
                    // Handle/report error here                           // #2
                });                                                       // #2
        }
    
        return <button onClick={handleButtonClick}>Click to fetch</button>
    }
    

    【讨论】:

    • 好吧,我觉得自己蠢得要死,因为它太简单了,而且我没有想到最简单的事情:/只有一个问题:因为使用了setData,所以在fetch 时定义了一个第一次运行,回调中使用的data可能还是旧的吗?
    • @Jolly - 不,您从 useState 获得的 setter 函数有两个有用的方面:1. 它们是稳定的(您总是得到相同的 setData 副本,它不会每次都重新创建),以及 2. 当您使用回调表单时,它们始终使用最新版本的数据。不要觉得自己笨! 很多 人不会意识到data 过时的问题! :-)
    • 好的,谢谢。我做了一个快速的测试,而且..正如你所说的那样简单,非常感谢。对于useState之前的const,是的,抱歉,我是直接在这里写代码的,所以我错过了。相反,对于fetch response.ok 我知道:通常我为fetch 编写了一个“增强器”,并提供了一些可用的链方法。 catch 仅捕获网络错误这一事实一开始很容易误导。不过,感谢您的宝贵时间!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-09-15
    • 2020-08-10
    • 2017-01-13
    • 2021-07-27
    • 2019-07-31
    • 2016-02-15
    • 1970-01-01
    相关资源
    最近更新 更多