Library provides 2 hooks which are useAnimationFrame
, useListenOnAnimationFrame
.
Starts invoking supplied function on every animation frame.
First argument fn
- a function to be invoked on every animation frame.
It's better for this fn
to be memoized either with useCallback
or to be defined outside of your component if that still fits your needs
fn
might accept a single argument of previous invocation return like in the example:
Example
import { useAnimationFrame } from "use-listen-on-animation-frame";
const fn = useCallback((previousFnReturn) => {
console.log("previouslyReturned", previousFnReturn); // undefined if no previous call yet
const fnReturn = new Date();
console.log("now returning", fnReturn);
return fnReturn;
}, []);
useAnimationFrame(fn);
Boolean indicator whether to start invoking function immediately when the hook is used or not. Defaults to true
.
Example
const [_stop, start] = useAnimationFrame(fn, {
autoStart: false,
});
useEffect(() => {
if (somethingIsGood) {
start();
}
}, [start]);
Returns [stop, start]
functions.
Function to stop the function from being invoked on every animation frame, and if that was the last function in application still running on animation frame - will effectively cancelRequestAnimationFrame
.
Note, that there is no need to stop()
on component unmount, this will be done automatically. As well as if it was the last hook in your app - it will cancel a single animation frame loop. Use it only if you explicitly need to stop()
your listeners & tracking function for some matter.
You can be assured in it's static reference - no changes between renders, same function.
Example
const [stop] = useAnimationFrame(fn);
useEffect(() => {
if (somethingIsWrong) {
stop();
}
}, [stop]);
Function to start the function invocations on every animation frame, and if it's the first function in application to be run on animation frame - starts a single animation frame loop.
You can be assured in it's static reference - no changes between renders, same function
Example
const [stop, start] = useAnimationFrame(fn);
useEffect(() => {
if (somethingIsWrong) {
stop();
}
}, [stop]);
useEffect(() => {
if (somethingIsGoodAgain) {
start();
}
}, [start]);
useAnimationFrame
is a handy alias for.
const [_add, _remove, stop, start] = useListenOnAnimationFrame(fn, {
autostart,
});
return [stop, start];
Optional second argument options
See useAnimationFrame
autoStart
Function that accepts 2 arguments thisFrameReturn
and previousFrameReturn
and determines if listeners should be invoked on this frame or not.
previousFrameReturn
can be undefined
if there was no previous frame return yet.
It's better for this function to be memoized with useCallback
or defined outside of your component if it still fits your needs
Example
const shouldInvokeListeners = useCallback(
(thisFrameReturn, previousFrameReturn) => {
/** invoke listeners only if this frame return is bigger than 5 or previous frame return is bigger than 3 */
return thisFrameReturn > 5 || previousFrameReturn < 3;
},
[]
);
const [addFrameListener] = useListenOnAnimationFrame(fn, {
shouldInvokeListeners,
});
Returns [addListener, removeListener, stop, start]
;
Function that accepts your listener
function.
listener
in turn accepts 2 arguments thisFrameReturn
and previousFrameReturn
.
Returns listenerId: string
, unique uuid of your listener, if you need to remove it later.
You can be assured in it's static reference - no changes between renders, same function
Example
const fn = useCallback(() => {
return new Date().getTime();
}, []);
const [addListener] = useListenOnAnimationFrame(fn);
useEffect(() => {
addListener((thisFrameReturn, previousFrameReturn) => {
console.log("this frame returned", thisFrameReturn);
// possibly undefined if no previous call yet
if (previousFrameReturn !== undefined) {
console.log("previous frame returned", previousFrameReturn);
}
// do anything
});
}, [addListener]);
Function that accepts listenerId: string
unique uuid from [addListener]
and removes a listener.
You can be assured in it's static reference - no changes between renders, same function
NOTE! There is no need to removeListener()
as a cleanup for a component. Whole listener tree will be destroyed when component will be unmounted. Use it only when you explicitly need to remove your side effect for some matter, or if you add your listener conditionally, and want it to be conditionally removed.
Example
const [addListener, removeListener] = useListenOnAnimationFrame(fn);
const [listenerId, setListenerId] = useState<string>();
useEffect(() => {
if (somethingGoodHappened) {
setListenerId(
addListener(() => {
// do anything
})
);
}
}, [addListener, somethingGoodHappened]);
useEffect(() => {
if (somethingBadHappened) {
removeListener(listenerId);
}
}, [removeListener, listenerId, somethingBadHappened]);
You might have noticed that you could simply put your function inside useAnimationFrame
, do side effects inside it, start
and stop
it when you need.
It is true, however you should consider using useListenOnAnimationFrame
with listeners when you want multiple side effects (or callbacks) for your function on animation frames because of performance implications.
Comparing
import { useAnimationFrame } from "use-listen-on-animation-frame";
const biggerThan2S = () => {
if (video.currentTime > 2) {
console.log("video current time is bigger than 2s");
}
};
const lesserThan5S = () => {
if (video.currentTime < 5) {
console.log("video current time is lesser than 5s");
}
};
const seekToStartWhenReached10S = () => {
if (video.currentTime >= 10) {
video.currentTime = 0;
}
};
useAnimationFrame(biggerThan2S);
useAnimationFrame(lesserThan5S);
useAnimationFrame(seekToStartWhenReached10S);
This will effectively call video.currentTime
3 times on every animation frame and do some side effects. Instead, we could track video.currentTime
specifically once and add listeners to it.
This way, tracked function (video.currentTime
in this example, but could be something really heavy) will only be invoked once on each animation frame:
import { useListenOnAnimationFrame } from "use-listen-on-animation-frame";
const getCurrentTime = () => video.currentTime;
const biggerThan2S = (currentTime) => {
if (currentTime > 2) {
console.log("video current time is bigger than 2s");
}
};
const lesserThan5S = (currentTime) => {
if (currentTime < 5) {
console.log("video current time is lesser than 5s");
}
};
const seekToStartWhenReached10S = (currentTime) => {
if (currentTime >= 10) {
video.currentTime = 0;
}
};
const [addListener] = useListenOnAnimationFrame(getCurrentTime);
useEffect(() => {
addListener(biggerThan2S);
addListener(lesserThan5S);
addListener(seekToStartWhenReached10S);
}, [addListener]);
Which will effectively call video.currentTime
once on each animationFrame and 3 listeners to it.