From 78e0431bc09233a85d516f53a38493ceefaf626f Mon Sep 17 00:00:00 2001 From: cxspxr Date: Sun, 23 Oct 2022 22:29:39 +0200 Subject: [PATCH] feat(hook): start, stop, autostart --- LICENSE | 2 +- README.md | 353 ++++++++++++++++++ package.json | 2 +- .../animation-frame-listeners-tree.test.tsx | 107 ++++++ src/__tests__/generate-id.test.ts | 1 + .../use-listen-on-animation-frame.test.tsx | 137 ++++++- src/types.ts | 22 +- src/use-listen-on-animation-frame.ts | 48 ++- 8 files changed, 658 insertions(+), 14 deletions(-) diff --git a/LICENSE b/LICENSE index 891b249..ff0865a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 artelydev +Copyright (c) 2022 Yaroslav Kasperovych Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e69de29..20494d7 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,353 @@ +

useListenOnAnimationFrame

+ +

+ Invoke & track your functions on every animation frame +
+ + ESM + · CommonJS + · 1 dependency + +

+ +

+ + Github Actions Build Status + + npm version + + code style: prettier + + license: MIT + + +

+ +## :book: Usage + +### Invoke your function on every animation frame + +`setInterval` but extremely frequent & performant. + +```typescript +import React, { useCallback, useState } from "react"; +import { useListenOnAnimationFrame } from "use-listen-on-animation-frame"; + +const AnimationFrameCounter: React.FC = () => { + const [animationFramesCounter, setAnimationFramesCounter] = useState(0); + + /* better memoized */ + const handleNewAnimationFrame = useCallback(() => { + setAnimationFramesCounter((prev) => prev + 1); + }, []); + + useListenOnAnimationFrame(handleNewAnimationFrame); + + return
{animationFramesCounter}
; +}; +``` + +### Track your function return on every animation frame + +If you need to track your function return on every animation frame and do something with it - go for it! + +```typescript +import React, { useCallback, useEffect, useState } from "react"; +import { useListenOnAnimationFrame } from "use-listen-on-animation-frame"; + +const EveningHoursIndicator: React.FC = () => { + const [reached8PM, setReached8PM] = useState(false); + const [reached9PM, setReached9PM] = useState(false); + + /** + * It's better, but not a must, when memoized inside + * with useCallback, or defined outside of a component + */ + const getHours = useCallback(() => { + /** + * by default, listeners are only + * invoked if return value of tracked + * function has changed between frames + */ + return new Date().getTime(); + }, []); + + const [addListener, removeListener] = useListenOnAnimationFrame(getHours); + + useEffect(() => { + const pm8Listener = addListener((nextHours) => { + if (nextHours > 20) { + console.log("its later than 8 PM"); + + setReached8PM(true); + } + }); + + const pm9Listener = addListener((nextHours) => { + if (nextHours > 21) { + console.log("its later than 9 PM"); + + setReached9PM(true); + } + }); + + return () => { + removeListener(pm8Listener); + removeListener(pm9Listener); + }; + }, [addListener, removeListener]); + + return ( + <> +

+ {reached8PM ? "its finally 8 PM" : "its not yet 8 PM"} +

+

+ {reached9PM ? 'its finally 9 PM : 'its not yet 9 PM'} +

+ + ); +}; +``` + +### Access previous animation frame function return + +If you for some reason need previous animation frame return of your function - it is easily possible. + +```typescript +import React, { useCallback, useEffect, useState } from "react"; +import { useListenOnAnimationFrame } from "use-listen-on-animation-frame"; + +const getMsElapsedFrom1970 = () => { + return new Date().getTime(); +}; + +const MilisecondsElapsedFrom1970: React.FC = () => { + const [ms, setMs] = useState(new Date().getTime()); + + const [addListener, removeListener] = + useListenOnAnimationFrame(getMsElapsedFrom1970); + + useEffect(() => { + const previousFrameListenerId = addListener( + (_, previousFrameTimeElapsed) => { + /** + * Can be undefined, on the first call, + * because there was no previous one + */ + if (previousFrameTimeElapsed) { + setMs(previousFrameTimeElapsed); + } + } + ); + + return () => { + removeListener(previousFrameListenerId); + }; + }, [addListener, removeListener]); + + return ( + <> +

ms elapsed from 1970 on previous browser frame: {ms}

+ + ); +}; +``` + +## :gear: Advanced usage + +### Start and stop tracking your function + +You can stop and start tracking again whenever you want. + +Btw, compare the following with `setInterval`. You couldn't achieve same smoothness. + +```typescript +// extremely-precise-clock.tsx +import React, { useCallback, useEffect, useState } from "react"; +import { useListenOnAnimationFrame } from "use-listen-on-animation-frame"; + +const formatDate = (date: Date) => { + return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}.${date.getMilliseconds()}`; +}; + +type ExtremelyPreciseClock = { + isTicking: boolean; +}; + +export const ExtremelyPreciseClock: React.FC = ({ isTicking }) => { + const [currentTime, setCurrentTime] = useState( + formatDate(new Date()) + ); + + const trackTime = useCallback(() => { + return new Date(); + }, []); + + const [addListener, removeListener, stop, start] = useListenOnAnimationFrame( + trackTime, + { + /** + * optionally indicate that the trackTime function and + * listeners should not be invoked until you `start()` + */ + autoStart: false, + } + ); + + useEffect(() => { + const listenerId = addListener((date) => { + setCurrentTime(formatDate(date)); + }); + + return () => { + removeListener(listenerId); + }; + }, [addListener, removeListener]); + + useEffect(() => { + if (isTicking) { + /* start tracking trackTime & listeners */ + start(); + } else { + /* stop tracking trackTime & listeners */ + stop(); + } + }, [isTicking, start, stop]); + + return
{currentTime}
; +}; +``` + +```typescript +// index.tsx + +import { ExtremelyPreciseClock } from "./extremely-precise-clock"; + +const Component: React.FC = () => { + const isClockTicking = determineIfClockIsticking(); + + return ( + <> + {/* ... */} + + + ); +}; +``` + +### Optimize/Unoptimize your listeners + +By default, if you don't provide `shouldInvokeListeners` option - listeners will be invoked only if tracked function return changes. It means that a supplied function will still be invoked on every animation frame, but listeners will not. + +```typescript +import React, { useCallback, useEffect, useState, useRef } from "react"; +import { useListenOnAnimationFrame } from "use-listen-on-animation-frame"; + +const conditionallyInvokeListeners = ( + nextValue: number, + _previousValue: number /* previous animation frame value */ +) => { + /* defaults to return nextValue !== previousValue */ + + /** + * invoke only if current animation + * frame current time is less than 1 + * second OR bigger than 2 seconds + */ + return nextValue < 1 || nextValue > 2; +}; + +const alwaysInvokeListeners = () => { + /** + * usually you shouldn't do this, as we try to cut + * performance costs, we don't want to invoke a bunch + * of functions even if tracked function return hasn't changed + */ + return true; +}; + +const VideoWithCurrentTime: React.FC = () => { + const [videoCurrentTime, setVideoCurrentTime] = useState(0); + const videoRef = useRef(); + + const animationFrames; + + /* better memoized */ + const trackVideoTime = useCallback(() => { + if (videoRef.current) { + return videoRef.current.currentTime(); + } + }, []); + + const [addOptimizedListener, removeOptimizedListener] = + useListenOnAnimationFrame(trackVideoTime, { + shouldInvokeListeners: conditionallyInvokeListeners, + }); + + const [addNotOptimizedListener, removeNotOptimizedListener] = + useListenOnAnimationFrame(trackVideoTime, { + shouldInvokeListeners: alwaysInvokeListeners, + }); + + useEffect(() => { + const notOptimizedListenerId = addNotOptimizedListener((currentTime) => { + setVideoCurrentTime(currentTime); + }); + + return () => { + removeNotOptimizedListener(notOptimizedListenerId); + }; + }, [addNotOptimizedListener, removeNotOptimizedListener]); + + useEffect(() => { + const optimizedListenerId = addOptimizedListener(() => { + /** + * do something heavy only when video current time + * is less than 1 second or bigger than 2 seconds + */ + }); + + return () => { + removeOptimizedListener(optimizedListenerId); + }; + }, [addOptimizedListener, removeOptimizedListener]); + + return ( + <> +