My goal is to make it so I know which video the user has seen in the viewport latest. This was working until I turned the videos into functional React components, which I can’t figure out how to check the ref until after the inital render of the React parent. This is currently the top part of the component:
function App() { const ref1 = useRef(null); const ref2 = useRef(null); const ref3 = useRef(null); function useIsInViewport(ref) { const [isIntersecting, setIsIntersecting] = useState(false); const observer = useMemo( () => new IntersectionObserver(([entry]) => setIsIntersecting(entry.isIntersecting) ), [] ); useEffect(() => { observer.observe(ref.current); return () => { observer.disconnect(); }; }, [ref, observer]); return isIntersecting; } var videoProxy = new Proxy(videoViewports, { set: function (target, key, value) { // console.log("value " + value) // console.log("key " + key) console.log(videoViewports); if (value) { setMostRecentVideo(key); //console.log("Most Rec: " + mostRecentVideo); } target[key] = value; return true; }, }); const [isGlobalMute, setIsGlobalMute] = useState(true); const [mostRecentVideo, setMostRecentVideo] = useState(""); videoProxy["Podcast 1"] = useIsInViewport(ref1); videoProxy["Podcast 2"] = useIsInViewport(ref2); videoProxy["Podcast 3"] = useIsInViewport(ref3);
And each component looks like this:
<VideoContainer ref={ref1} videoProxy={videoProxy} mostRecentVideo={mostRecentVideo} setMostRecentVideo={setMostRecentVideo} title="Podcast 1" isGlobalMute={isGlobalMute} setIsGlobalMute={setIsGlobalMute} videoSource={video1} podcastName={podcastName} networkName={networkName} episodeName={episodeName} episodeDescription={episodeDescription} logo={takeLogo} muteIcon={muteIcon} unmuteIcon={unmuteIcon} ></VideoContainer>
I had moved the logic for checking if the component was in the viewport into each component, but then it was impossible to check which component was the LATEST to move into viewport. I tried looking online and I don’t understand how I would forward a ref here, or how to get the useIsInViewport to only start working after the initial render since it can’t be wrapped in a useEffect(() => {}, []) hook. Maybe I’m doing this completely the wrong way with the wrong React Hooks, but I’ve been bashing my head against this for so long…
Advertisement
Answer
First of all: I’m not quite sure, if a Proxy.set
is the right way of accomplishing your goal (depends on your overall app architecture). Because setting data does not always mean, the user has really seen the video or is in the viewport.
I’ve created a simple solution that uses two components. First the a VideoList
that contains all videos and manages the viewport calculations so you don’t have thousands of event listeners on resize
, scroll
and so on (or Observers respectively).
The Video
component is a forwardRef
component, so we get the ref
of the rendered HTML video element (or in the case of this example, the encompassing div).
import { forwardRef, useCallback, useEffect, useState, createRef } from "react"; function inViewport(el) { if (!el) { return false; } const rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); } const Video = forwardRef((props, ref) => { return ( <div ref={ref}> <p>{props.source}</p> <video {...props} /> </div> ); }); const VideoList = ({ sources }) => { const sourcesLength = sources.length; const [refs, setRefs] = useState([]); useEffect(() => { // set refs setRefs((r) => Array(sources.length) .fill() .map((_, i) => refs[i] || createRef()) ); }, [sourcesLength]); const isInViewport = useCallback(() => { // this returns only the first but you can also apply a `filter` instead of the index const videoIndex = refs.findIndex((ref) => { return inViewport(ref.current); }); if (videoIndex < 0) { return; } console.log(`lastSeen video is ${sources[videoIndex]}`); }, [refs, sources]); useEffect(() => { // add more listeners like resize, or use observer document.addEventListener("scroll", isInViewport); document.addEventListener("resize", isInViewport); return () => { document.removeEventListener("scroll", isInViewport); document.removeEventListener("resize", isInViewport); }; }, [isInViewport]); return ( <div> {sources.map((source, i) => { return <Video ref={refs[i]} source={source} key={i} />; })} </div> ); }; export default function App() { const sources = ["/url/to/video1.mp4", "/url/to/video1.mp4"]; return ( <div className="App"> <VideoList sources={sources} /> </div> ); }
Working example that should lead you into the right directions: https://codesandbox.io/s/distracted-waterfall-go6g7w?file=/src/App.js:0-1918
Please go over to https://stackoverflow.com/a/54633947/1893976 to see, why I’m using a useState
for the ref
list.