使用React定时轮询API

85

我需要每一到两秒监控屏幕上的一些数据更新信息。我找到的方法是使用以下实现:

    componentDidMount() {
        this.timer = setInterval(()=> this.getItems(), 1000);
      }
    
      componentWillUnmount() {
        this.timer = null;
      }
    
      getItems() {
        fetch(this.getEndpoint('api url endpoint'))
            .then(result => result.json())
            .then(result => this.setState({ items: result }));
      }

这是正确的方法吗?


4
这是一种方法,但效率不高,当你扩展应用程序时会使服务器超载。如果使用套接字连接,您可以在消息到达时收到通知,这将更加高效。 - Mikkel
我只是需要使用 REST API ... 我该如何正确地进行“池化(Pooling)”呢? - Eduardo Spaki
这取决于您的服务器上有哪些技术。您应该阅读一些关于Web套接字工作原理的文章,这里有一篇文章http://blog.teamtreehouse.com/an-introduction-to-websockets,但是还有很多资源可供使用。 - Mikkel
7个回答

75

嗯,既然你只有一个API并且无法控制它以更改为使用套接字,那么你唯一的方法就是轮询。

就你的轮询而言,你采取了合适的方法。但是你上面的代码存在一个陷阱。

componentDidMount() {
  this.timer = setInterval(()=> this.getItems(), 1000);
}

componentWillUnmount() {
  this.timer = null; // here...
}

getItems() {
  fetch(this.getEndpoint('api url endpoint'))
    .then(result => result.json())
    .then(result => this.setState({ items: result }));
}

问题在于一旦组件卸载,尽管您在this.timer中存储的interval引用被设置为null,但它仍未停止。即使在组件已被卸载后,该interval将继续调用处理程序,并尝试在不再存在的组件中setState
为了正确处理它,请先使用clearInterval(this.timer),然后再设置this.timer = null
另外,fetch调用是异步的,这可能会导致相同的问题。请将其设置为cancelable,并在任何fetch未完成时取消。
希望对您有所帮助。

20
如果您能演示如何实施您建议的更改,那就太好了。 - Pro Q
5
注意使用 setInterval() 进行异步调用时要小心,因为它即使在等待 API 响应时也会调用。更安全的方法是使用递归的 setTimeout()。 - Gustavo Garcia
2
@GustavoGarcia的评论的一个工作示例已添加在此处-https://dev59.com/alYO5IYBdhLWcg3wPvJv#63134447 - Vasanth Gopal

60

虽然这是一个老问题,但当我搜索React轮询时,它是顶部结果,并且没有与Hooks一起使用的有效答案。

// utils.js

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

export const useInterval = (callback, delay) => {

  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);


  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      const id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

源代码: https://overreacted.io/making-setinterval-declarative-with-react-hooks/

然后您只需导入并使用即可。

// MyPage.js

import useInterval from '../utils';

const MyPage = () => {

  useInterval(() => {
    // put your interval code here.
  }, 1000 * 10);

  return <div>my page content</div>;
}

9
我们如何取消 useInterval?也就是在任何条件下停止轮询。 - AmitJS94
1
将时间设置为0会取消计时器。 - vexingCoder
1
如何在TypeScript中实现这个? - uber
1
Typescript NPM包针对此钩子:https://www.npmjs.com/package/@use-it/interval - secretshardul
如何在轮询后获得所需响应后取消计时器?尝试在延迟中添加“0”。这会导致对API进行连续调用。 - coderpc
传入 null。在我的代码中,我也将 0 视为 null,以避免意外地对我的 API 进行 DDOS 攻击。 - Sam Sussman

11
您可以结合使用setTimeoutclearTimeout的方法。如果使用setInterval,则无论前一个API调用成功或失败,它都会每隔'x'秒触发一次API调用。这可能会占用您的浏览器内存,并随着时间的推移而降低性能。此外,如果服务器已关闭,setInterval将继续向服务器发送请求,而不知道其状态。
相比之下,您可以使用setTimeout进行递归。仅在前一个API调用成功时才触发后续的API调用。如果上一个调用失败,则清除超时并不再触发任何调用。如果需要,在失败时提示用户。让用户刷新页面以重新启动此过程。
以下是示例代码:
let apiTimeout = setTimeout(fetchAPIData, 1000);

function fetchAPIData(){
    fetch('API_END_POINT')
    .then(res => {
            if(res.statusCode == 200){
                // Process the response and update the view.
                // Recreate a setTimeout API call which will be fired after 1 second.
                apiTimeout = setTimeout(fetchAPIData, 1000);
            }else{
                clearTimeout(apiTimeout);
                // Failure case. If required, alert the user.
            }
    })
    .fail(function(){
         clearTimeout(apiTimeout);
         // Failure case. If required, alert the user.
    });
}

3

@AmitJS94,这篇文章中有详细的部分介绍如何停止一个定时器,它补充了GavKilbride提到的方法(链接)

作者建议添加一个延迟变量的状态,并在想要暂停间隔时传入 "null" 作为该延迟时间:

const [delay, setDelay] = useState(1000);
const [isRunning, setIsRunning] = useState(true);
  useInterval(() => {
    setCount(count + 1);
  }, isRunning ? delay : null);

    useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);

一定要读这篇文章,以更好地理解细节 -- 这篇文章非常详尽和写得很好!


2

正如Vasanth所提到的,我更喜欢:

import { useEffect, useRef } from 'react';

export const useInterval = (
  callback: Function,
  fnCondition: Function,
  delay: number,
) => {
  const savedCallback = useRef<Function>();
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);
  useEffect(() => {
    let id: NodeJS.Timeout;
    const tick = async () => {
      try {
        const response =
          typeof savedCallback.current === 'function' &&
          (await savedCallback.current());
        if (fnCondition(response)) {
          id = setTimeout(tick, delay);
        } else {
          clearTimeout(id);
        }
      } catch (e) {
        console.error(e);
      }
    };
    tick();
    return () => id && clearTimeout(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [delay]);
};

作用:使用内部的 fnCondition,其中可以是基于上一个请求响应的条件。

//axios-hooks
const {
    data,
    isLoadingData,
    getData,
} = api.useGetData();

const fnCondition = (result: any) => {
    const randomContidion = Math.random();
    //return true to continue
    return randomContidion < 0.9;
  };
useInterval(() => getData(), fnCondition, 1000);
无法使用:像这样将延迟传递为null以停止useInterval对我来说不起作用,使用以下代码:https://www.aaron-powell.com/posts/2019-09-23-recursive-settimeout-with-react-hooks/(您可能会有这种印象,但在启动/停止几次后它会出现问题)。
  const [isRunning, setIsRunning] = useState(true);
  const handleOnclick = () => {
    setIsRunning(!isRunning);
  };

  useInterval(() => getData(), isRunning ? 1000 : null);

  <button onClick={handleOnclick}>{isRunning ? 'Stop' : 'Start'}</button>

总结:我可以通过传递fnCondition来停止useInterval的使用,但无法通过传递delay=null来停止使用。


你知道为什么要将回调函数保存在 ref 中吗? - Aaron_Actu

0

这是一个简单的示例,使用函数组件中的钩子,并且它将在一定时间间隔内刷新您的数据。

import React from 'react';

import { useEffect, useState } from 'react';

export default function App() {
  let [jokes, setJokes] = useState('Initial');

  async function fetchJokes() {
    let a = await fetch('https://api.chucknorris.io/jokes/random');
    let b = await a.json();
    setJokes(b.value);
  }

// Below function works like compomentWillUnmount and hence it clears the timeout
  useEffect(() => {
    let id = setTimeout(fetchJokes, 2000);
    return () => clearTimeout(id);
  });

  return <div>{jokes}</div>;
}

或者,您也可以使用axios来进行API调用。

function App() {
  const [state, setState] = useState("Loading.....");

  function fetchData() {
    axios.get(`https://api.chucknorris.io/jokes/random`).then((response) => {
      setState(response.data.value);
    });
  }

  useEffect(() => {
    console.log("Hi there!");
    let timerId = setTimeout(fetchData, 2000);
     return ()=> clearInterval(timerId); 
  });

  return (
    <>
      This component
      <h3>{state}</h3>
    </>
  );
}

0

这是一个简单而完整的解决方案,它包括:

  • 每 X 秒轮询

  • 有增加超时时间选项,以便每次逻辑运行时不会过载服务器

  • 当最终用户退出组件时清除超时

    //挂载数据
    componentDidMount() {
        //运行此函数以首次获取数据
        this.getYourData();
        //使用 setTimeout 进行连续轮询,但每次都增加计时器
        this.timer = setTimeout(this.timeoutIncreaser, this.timeoutCounter);
    }
    
    //卸载过程
    componentWillUnmount() {
        this.timer = null; //清除变量
        this.timeoutIncreaser = null; //清除重置计时器的函数
    }
    
    //每次运行时将超时时间增加一定量,并调用 fetchData() 重新加载屏幕
    timeoutIncreaser = () => {
        this.timeoutCounter += 1000 * 2; //每次增加 2 秒超时时间
        this.getYourData(); //这可以是您想要每 x 秒运行的任何函数
        setTimeout(this.timeoutIncreaser, this.timeoutCounter);
    }
    

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接