Rollie
Rollie

Reputation: 4752

How can I poll for updates to an item in a list?

Given a list of tasks:

const [tasks, setTasks] = useState([])

I want to add a task on user input with setTasks(...tasks, aNewTask), and then update the results of that task asynchronously:

while (true) {
  taskStatus = await getTaskStatus()
  setTasks(tasks.map(t => t.id == taskStatus.id ? taskStatus : t))
}

which looks logically correct. But it doesn't work; the task is added to the list, then deleted subsequently. tasks isn't being updated, so querying it yields the original list, before I called setTasks the first time. The best workaround I see while still using the same pattern is to wrap useState in a custom hook, and resolve a promise as part of the set-value function, but even then I need tasks to be var so I can update it locally.

Is there a cleaner way, that still uses the async logic?

Upvotes: 2

Views: 660

Answers (2)

HMR
HMR

Reputation: 39320

When you have an effect that sets state after an asynchronous action you should check if the component is still mounted before setting that state.

Here is an example that checks before setting state and will exit the infinite loop when component is unmounted. The effect has no dependencies so will only be run after first render. The tasks is not a dependency to the effect because I pass a callback to setTasks.

const { useRef, useEffect, useState } = React;
//helper to check if component is mounted so you won't
//  try to set state of an unmounted component
//  comes from https://github.com/jmlweb/isMounted/blob/master/index.js
const useIsMounted = () => {
  const isMounted = useRef(false);
  useEffect(() => {
    isMounted.current = true;
    return () => (isMounted.current = false);
  }, []);
  return isMounted;
};
//returns current date after waiting for a second
function getTasksStatus() {
  return new Promise(r =>
    setTimeout(() => r(Date.now(), 1000))
  );
}
function App() {
  const [tasks, setTasks] = useState(
    Math.floor(Date.now() / 1000)
  );
  //from the helper to check if component is still mounted
  const isMounted = useIsMounted();
  useEffect(() => {
    //babel of Stack overflow is ancient and doesn't know what
    //  to do with async so I create a recursive instead
    function polling() {
      getTasksStatus().then(newTasks => {
        //only do something if component is still mounted
        if (isMounted.current) {
          //pass callback to setTasks so effect doesn't depend on tasks
          setTasks(currentTasks =>
            Math.floor(newTasks / 1000)
          );
          //call recursively
          polling();
        }
      });
    }
    //the async version with "infinite" loop looks like this
    // async function polling() {
    //   //exit loop if component is unmounted
    //   while (isMounted.current) {
    //     const newTasks = await getTasksStatus();
    //     isMounted.current && //make sure component is still mounted
    //       //pass callback to setTasks so effect doesn't depend on tasks
    //       setTasks(currentTasks =>
    //         Math.floor(newTasks / 1000)
    //       );
    //   }
    // }
    polling();
  }, [isMounted]);
  return <div>{tasks}</div>;
}

ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Upvotes: 1

I am L
I am L

Reputation: 4662

The problem with doing a setState inside a loop is that it will only trigger the thing once. To make it so it will "poll" you can wrap it with a timeout function:

let timeout = null; //outside class
...
while (true) {
  timeout = setTimeout(() => {
    taskStatus = await getTaskStatus()
    setTasks(tasks.map(t => t.id == taskStatus.id ? taskStatus : t))
  }, 1000);
}

the 1000 is the number of millisecond to run the set task/poll.

make sure you clear the timeout on unmount like so:

useEffect(() => () => clearTimeout(timeout)));

This is so it doesn't poll if the component is no longer active

Upvotes: 0

Related Questions