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