React hook to wait until the previous call is complete - reactjs

I am working on a react project which uses hooks. And I was assigned a task
"change the useInterval hook, or create a new one (usePoll?). This should operate the same as useInterval, but should wait until the ajax request is complete before starting the timer".
I am new to react hooks and was looking for a solution for this but could not find. Current useInterval function is as follows.
import React, { useEffect, useRef } from 'react';
export function useInterval(callback, delay, immediate = true) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
if (immediate) {
tick();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
and it use in the program as follows.
useInterval(() => {
get(`/api/v1/streams/1`).then(({ data: { data } }) => {
setStream(data);
});
}, 5000);
and I need to change the useInterval function to wait until the ajax request is complete before starting the timer. It would be great if anyone can help me on this. Thanks

Give this a shot.. it requires calling the next function inside of then but it should come close to what you're looking for.
function useInterval(handler, delay, immediate = true) {
React.useEffect(() => {
let interval
const start = () => {
clearInterval(interval)
interval = setInterval(() => handler(start), delay)
}
handler(start)
return () => clearInterval(interval)
}, [])
}
usage:
useInterval((next) => {
get('/api/v1/streams/1').then(data => {
// tell the timer to begin
next()
})
}, 5000)

You can use async\await to await for first call completes.
Modify internal useEffect like so
export function useInterval(callback, delay, immediate = true) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
// useEffect doesn't like async callbacks (https://github.com/facebook/react/issues/14326) so create nested async callback
(async () => {
// Make tick() async
async function tick() {
await savedCallback.current();
}
if (delay !== null) {
if (immediate) {
await tick(); // Here we should await for tick()
}
let id = setInterval(tick, delay); // Unfortunately setInterval is not async/await compatible. So it will not await for tick
return () => clearInterval(id);
}
})(); // Call nested async function
}, [delay]);
}
And you callback should return Promise so async\await to work properly
useInterval(() => {
// After .then promise will be resolved, so our useInterval will know about that
return get(`/api/v1/streams/1`).then(({ data: { data } }) => {
setStream(data);
});
}, 5000);

Related

Refactor a custom hook to be called inside a method

I created this hook that is responsible to do something after an amount of time:
const useTime = (callback, timeout = 1000) => {
useEffect(() => {
const timer = setTimeout(() => {
callback()
}, timeout);
return () => clearTimeout(timer);
}, []);
}
The hook is working, but i can not call it inside a method like:
{
clear: () => {
useTime(() => console.log('close'), 6000 )
},
... this is happen because of hooks rules. Question: How to refactor the hook to be able to call it inside a method or a function?
You probably need to do like this -
function useTime(cb, timeout = 100) {
const timer = setTimeout(() => {
cb();
}, timeout);
return () => clearTimeout(timer);
}
function anotherMethod() {
const cancel = useTime(runJob, 1000);
// if you wanna cancel the timer, just call the cancel function
cancel();
}
You can try something around this:
const useTime = () => {
const timer = useRef();
const fn = useCallback((callback, timeout = 1000) => {
timer.current = setTimeout(() => {
callback()
}, timeout);
}, []);
useEffect(() => {
return () => clearTimeout(timer.current);
}, []);
return fn;
}
const delayedFn = useTime();
clear: () => {
delayedFn(() => console.log('close'), 6000)
},

React Lifecycles managing intervals with changing values

In one of my components I have a useEffect setup where an interval is set to fetch a function.
It's basically set up as follows:
...
const token = useToken() // custom hook that updates whenever access_token updates
const fetchData = useCallback(() => {
callAPI(token)
}, [token])
useEffect(() => {
if (!token) return
fetchData()
const interval = setInterval(fetchData, 60000)
return () => {
clearInterval(interval)
}
}, [token]}
...
It is supposed fetchData every 60 seconds which it does.
What it also needs to do (which it doesn't) is whenever the token is updated, it should account for that.
What I've currently done to try solve that is clear the interval when the token changes and start the process over. But I think I've handled that incorrectly in my attempts above.
Any idea on how to accomplish this correctly?
the only thing missing is fetchData should be added to the dependency array to make sure that the useEffect uses the updated callback
useEffect(() => {
if (!token) return
fetchData()
const interval = setInterval(fetchData, 60000)
return () => {
clearInterval(interval)
}
}, [token, fetchData])
but you can also move the fetchData(this time fetchData doesn't have to be memorized with useCallback) function inside the useEffect, that way you can only have token as a dependency:
useEffect(() => {
const fetchData = () => {
if(!token) return;
callAPI(token)
};
fetchData();
const interval = setInterval(fetchData, 60000)
return () => {
clearInterval(interval)
}
}, [token])
Edit:
You can remove token form the useEffect this way:
const fetchData = useCallback(() => {
if(!token) return; // moved the token check here
callAPI(token)
}, [token])
useEffect(() => {
fetchData();
const interval = setInterval(fetchData, 60000)
return () => {
clearInterval(interval)
}
}, [fetchData])

Jest testing useInterval React Hook not working

Trying to test Custom useInterval Hook but jest.advanceTimersByTime(199); and jest.advanceTimersToNextTimer(1); don't seem to be working.
I log jest.getTimerCount() anywhere and it returns 0;
Custom Hook:
import { useRef, useEffect } from 'react';
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef<() => void | null>();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
});
// Set up the interval.
useEffect(() => {
function tick() {
console.log("here"); // This never gets logged !!!!
if (typeof savedCallback?.current !== 'undefined') {
console.log(delay, savedCallback);
}
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
export default useInterval;
Test:
import useInterval from "./useInterval";
import { renderHook } from '#testing-library/react-hooks';
describe("useInterval Hook:", () => {
let callback = jest.fn();
beforeAll(() => {
// we're using fake timers because we don't want to
// wait a full second for this test to run.
jest.useFakeTimers();
});
afterEach(() => {
callback.mockRestore();
jest.clearAllTimers();
});
afterAll(() => {
jest.useRealTimers();
});
test('should init hook with delay', () => {
const { result } = renderHook(() => useInterval(callback, 5000));
expect(result.current).toBeUndefined();
expect(setInterval).toHaveBeenCalledTimes(1);
expect(setInterval).toHaveBeenCalledWith(expect.any(Function), 5000);
});
test('should repeatedly calls provided callback with a fixed time delay between each call', () => {
const { result } = renderHook(() => useInterval(callback, 200));
expect(callback).not.toHaveBeenCalled();
// fast-forward time until 1s before it should be executed
jest.advanceTimersByTime(199);
expect(callback).not.toHaveBeenCalled(); // FAILS
// jest.getTimerCount() here returns 0
// fast-forward until 1st call should be executed
jest.advanceTimersToNextTimer(1);
expect(callback).toHaveBeenCalledTimes(1);
// fast-forward until next timer should be executed
jest.advanceTimersToNextTimer();
expect(callback).toHaveBeenCalledTimes(2);
// fast-forward until 3 more timers should be executed
jest.advanceTimersToNextTimer(3);
expect(callback).toHaveBeenCalledTimes(5);
});
});
I solved it by moving jest.useFakeTimers(); to the beforeEach block instead of beforeAll.
I struggled with this for hours until finally I found this article. https://overreacted.io/making-setinterval-declarative-with-react-hooks/
Just copy the useInterval function verbatim, and then use it with the syntax also provided. It just works correctly with no fuss.

Do an API call every few seconds using the Context API and React Hooks

Is it possible to set an auto-refresh interval every few seconds when using the Context API from React? The getData() function runs axios.get() on the API, but still when I try setInterval() and cleanup in the return function of the useEffect hook, it doesn't clean up the interval. getData() sets to the app level state the current and loading variables.
I simply want to refresh and re-do the API call every few seconds. I tried with the useRef() hook and I got it to working, but still the useEffect doesn't clear up the interval once it's finished.
I want to access the current property in the return function of the component and display some data every time an API call is ran.
Here's the code:
const { loading, current, getData } = appContext;
useEffect(() => {
const interval = setInterval(() => {
getData();
console.log('updated');
}, 1000);
return () => clearInterval(interval);
}, []); // eslint-disable-line // also tried without the []
getData() code:
const getData = async () => {
setLoading();
const res = await axios.get(process.env.REACT_APP_APIT);
dispatch({ type: GET_CURRENT, payload: res.data });
};
I had a similar problem and I used to solution described here: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
Here is a simple example:
import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const useInterval = (callback, delay) => {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
function App() {
const [intervalTime, setIntervalTime] = useState(2000);
useInterval(() => {
// Do some API call here
setTimeout(() => {
console.log('API call');
}, 500);
}, intervalTime);
return (
<div className="App">
<button onClick={() => setIntervalTime(2000)}>Set interval to 2 seconds</button>
<button onClick={() => setIntervalTime(null)}>Stop interval</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Using the state variable intervalTime you can control the interval time. By setting it to null the interval will stop running.

Request to server each N seconds after flag

I have a specific case. The first thing I do is request the Index.DB. After I got the taskId from it, I need to start asking the server every 5 seconds. And stop doing this on a specific flag. How can i do that properly with hooks?
I'tried to use useInterval hook like this:
https://github.com/donavon/use-interval;
But when i set it in useEffect causes consistent error:
Invalid hook call. Hooks can only be called inside of the body of a function component.
const Page = () => {
const [task, setTask] = useState({})
const isLoaded = (task.status === 'fatal');
const getTask = (uuid: string) => {
fetch(`${TASK_REQUEST_URL}${uuid}`)
.then(res => {
return res.json();
})
.then(json => {
setTask(json.status)
})
.catch(error => console.error(error));
};
useEffect(() => {
Storage.get('taskId')
.then(taskId => {
if (!taskId) {
Router.push('/');
}
useInterval(() => getTask(taskId), 5000, isTaskStatusEqualsSomthing)
})
}, []);
return (
<p>view</p>
);
};
I also tried to play around native setInterval like this
useEffect(() => {
Storage.get('taskId')
.then(taskId => {
if (!taskId) {
Router.push('/');
}
setInterval(() => getTask(taskId), 5000)
})
}, []);
But in this case i don't know how to clearInterval and also code looks dirty.
The solution is simple. You just need to configure your setInterval within .then callback like
useEffect(() => {
let timer;
Storage.get('taskId')
.then(taskId => {
if (!taskId) {
Router.push('/');
else {
timer = setInterval(() => getTask(taskId), 5000)
}
}
})
return () => {clearInterval(timer)}
}, []);
The reason, first approach doesn't work for you is because you cannot call a hook conditionally or in useEffect as you did for useInterval

Resources