1

Situation:
Im trying to write a custom hook that allows me to fetch data and launches a setInterval which polls status updates every two seconds. Once all data is processed, the interval is cleared to stop polling updates.

Problem:
My problem is that the function passed to setInterval only has the initial state (empty array) although it has already been updated. Either it is resetting back to the initial state or the function has an old reference.

Code:
Link to Codesandbox: https://codesandbox.io/s/pollingstate-9wziw7?file=/src/Hooks.tsx

Hook:

export type Invoice = {id: number, status: string};

export async function executeRequest<T>(callback: () => T, afterRequest: (response: {error: string | null, data: T}) => void) {
  const response = callback();
  afterRequest({error: null, data: response});
}

export function useInvoiceProcessing(formData: Invoice[]): [result: Invoice[], executeRequest: () => Promise<void>] {

  const timer: MutableRefObject<NodeJS.Timer | undefined> = useRef();

  const [result, setResult] = useState<Invoice[]>(formData);

  // Mock what the API would return
  const firstRequestResult: Invoice[] = [
    {id: 1, status: "Success"},
    {id: 2, status: "Pending"}, // This one needs to be polled again
    {id: 3, status: "Failed"}
  ];
  const firstPoll: Invoice = {id: 2, status: "Pending"};
  const secondPoll: Invoice = {id: 2, status: "Success"};

  // The function that triggers when the user clicks on "Execute Request"
  async function handleFirstRequest() {
    await executeRequest(() => firstRequestResult, response => {
      if (!response.error) {
        setResult(response.data)
        if (response.data.some(invoice => invoice.status === "Pending")) {
          // Initialize the timer to poll every 2 seconds
          timer.current = setInterval(() => updateStatus(), 2000);
        }
      } else {
        // setError
      }
    })
  }

  let isFirstPoll = true; // Helper variable to simulate a first poll
  async function updateStatus() {
    // Result has the initial formData values (NotUploaded) but should already have the values from the first request
    console.log(result); 
    const newResult = [...result];
    let index = 0;
    for (const invoice of newResult) {
      if (invoice.status === "Pending") {
        await executeRequest(() => isFirstPoll ? firstPoll : secondPoll, response => {
          if (!response.error) {
            newResult[index] = response.data;
          } else {
            // Handle error
          }
        });
      }
      index++;
    }
  
    setResult(newResult);
    isFirstPoll = false;
    
    const areInvoicesPending = newResult.some(invoice => invoice.status === "Pending");
    if (!areInvoicesPending) {
      console.log("Manual clear")
      clearInterval(timer.current);
    }
  };

  useEffect(() => {
    return () => {
      console.log("Unmount clear")
      clearInterval(timer.current);
    }
  }, [])

  return [result, handleFirstRequest];

Usage:

const [result, executeRequest] = useInvoiceProcessing([
    { id: 1, status: "NotUploaded" },
    { id: 2, status: "NotUploaded" },
    { id: 3, status: "NotUploaded" }
  ]);

  async function handleRequest() {
    console.log("Start request");
    executeRequest();
  }

  return (
    <div className="App">
      <button onClick={handleRequest}>Execute Request</button>
      <p>
        {result.map((invoice) => (
          <Fragment key={invoice.id}>
            {invoice.status}
            <br />
          </Fragment>
        ))}
      </p>
    </div>
  );

EDIT1
I have one possible solution. This post helped me in the right direction: React hooks functions have old version of a state var

The closure that updateStatus uses is outdated. To solve that, I saved updateStatus in a useRef (updateStatus also needs useCallback). Although not necessary, I had to store result in a useRef as well but I'm not sure yet why.

const updateStatusRef = useRef(updateStatus);
  useEffect(()=>{
    updateStatusRef.current = updateStatus;   
  }, [updateStatus]);

Here's a working example: https://codesandbox.io/s/pollingstate-forked-njw4ct?file=/src/Hooks.tsx

Sean A.S. Mengis
  • 129
  • 2
  • 13

0 Answers0