You want to make an event occur when scrolling to that location.
In the current mode, it only works when reloaded at the location, and does not work when it comes to the location while scrolling down from above.
I think I need to use useState, but I tried in many ways but failed. Please help me.
useEffect(() => {
AOS.init();
var cnt = document.querySelectorAll(".count")[props.num];
var water = document.querySelectorAll(".water")[props.num];
const Skills = document.querySelector('#skills');
const percentscroll = window.scrollY + Skills.getBoundingClientRect().top;
if (window.scrollY >= percentscroll) {
var percent = cnt.innerText;
var interval;
interval = setInterval(function () {
percent++;
cnt.innerHTML = percent;
water.style.transform = 'translate(0' + ',' + (100 - percent) + '%)';
if (percent == props.percent) {
clearInterval(interval);
}
}, 60);
}
}, [])
You are not doing it the React way. In React, you use refs to keep tract of DOM nodes. For you case about scrolling to a certain location, you should use IntersectionObserver (mdn). There are many libraries that can help you but I suggest doing it the vanilla way to learn more. Here is an example from dev.to
import { useEffect, useRef, useState } from 'react';
const Header = () => {
const containerRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);
const callbackFunction = (entries) => {
const [entry] = entries;
setIsVisible(entry.isIntersecting);
};
const options = {
root: null,
rootMargin: '0px',
threshold: 1
};
useEffect(() => {
const observer = new IntersectionObserver(callbackFunction, options);
if (containerRef.current) observer.observe(containerRef.current);
return () => {
if (containerRef.current) observer.unobserve(containerRef.current);
};
}, [containerRef, options]);
return (
<div>
<div>{isVisible}</div>
<div>A lot of content ...</div>
<div ref={containerRef}>Observe me</div>
</div>
);
};
export default Header;
Related
I want to run a function on the parent component in the child component.
Eventually, I want to have the function run when the scroll goes to that position.
UseImperativeHandle was used, but props did not apply. Is there a way to apply props in the useImperativeHandle?
Also, is it correct to use IntersectionObserver this way?
child Components
function Percent(props, ref) {
useImperativeHandle(ref,() => ({
percentst: () => {
var cnt = document.querySelectorAll(".count")[props.num];
var water = document.querySelectorAll(".water")[props.num];
var percent = cnt.innerText;
var interval;
interval = setInterval(function () {
percent++;
cnt.innerHTML = percent;
water.style.transform = 'translate(0' + ',' + (100 - percent) + '%)';
if (percent == props.percent) {
clearInterval(interval);
}
}, 80);
}
}));
}
export default forwardRef(Percent);
parent component
function About(props) {
const containerRef = useRef();
const myRef = useRef();
const [isVisible, setIsVisible] = useState(false);
const callbackFunction = (entries) => {
const [entry] = entries;
setIsVisible(entry.isIntersecting);
};
const options = {
root: document.getElementById('skills'),
rootMargin: '0px',
threshold: 1
};
useEffect(() => {
const observer = new IntersectionObserver(callbackFunction, options);
console.log(containerRef.current)
if (containerRef.current) observer.observe(containerRef.current);
return () => {
myRef.current.percentst()
if (containerRef.current) observer.unobserve(containerRef.current);
};
}, [containerRef, options]);
return(
<div ref={containerRef}></div>
<Percent ref={myRef} />
)
}
export default About;
Two ways of doing this
Pass your method as a prop to the child (this may not be desirable)
Use custom event listeners/triggers
There are some packages out there that will help with events such as https://www.npmjs.com/package/react-custom-events
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...
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.
I am making a pdf viewer using react app. I have a pdf with almost 150 pages and I am using zoom-in and zoom-out icons, I am using scale prop in Page component to accomplish that. But whenever I zoom-in or zoom-out there is a slight delay in pdf file to re-render with zoomed pages. And that delay doesnt happen for small pdf.
Here is my code -
const [pages, setPages] = React.useState(0);
const [pagesArr, setPagesArr] = React.useState([]);
const [scrolledValue, setScrolledValue] = React.useState(0);
const [pageNumber, setPageNumber] = React.useState(1);
const [pdfScale, setPdfScale] = React.useState(1.0);
function onLoadSuccess(param) {
setPages(param.numPages);
const numArr = [];
for (let num = 1; num <= param.numPages; num++) {
numArr.push(num);
}
setPagesArr(numArr);
}
function handleZoominIcon() {
setPdfScale(prev => {
if (prev > 2.1) {
return 2.2;
} else {
return prev + 0.1;
}
});
}
function handleZoomoutIcon() {
setPdfScale(prev => {
console.log(prev);
if (prev <= 0.5) {
return 0.5;
} else {
return prev - 0.1;
}
});
}
return (
<section className = 'ppt-view-pdf' style = {scrolledStyle[1], {zoom: '100%'}} ref = {props.pdfViewRef}>
<Document file = {PPT} onLoadSuccess = {onLoadSuccess}>
{pagesArr.map(element => <Page scale = {pdfScale} key = {element} pageNumber = {element}></Page>)}
</Document>
</section>
)
Is there any solution for this?
I had the same issue. The performance was poor because changing the scale factor will re-render all pages. I solved it using react-window that virtualises the invisible components in the dom. I used VariableSizeList because it was more performant than FixedSizeList. Here is a minimal example:
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Document, Page } from "react-pdf/dist/esm/entry.webpack";
import { VariableSizeList } from "react-window";
const PDFDoc = () => {
const [scale, setScale] = useState(1);
const [numPages, setNumPages] = useState(0);
const docRef = useRef<HTMLDivElement>(null);
const [docWidth, setDocWidth] = useState(0);
const [docHeight, setDocHeight] = useState(0);
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
setNumPages(numPages);
const rect = docRef.current?.getBoundingClientRect();
if (rect) {
setDocWidth(rect.width);
setDocHeight(rect.height);
}
};
return (
<div ref={docRef}>
{/* TODO: add your zoom controls here */}
<Document file={"sample.pdf"} onLoadSuccess={onDocumentLoadSuccess}>
<VariableSizeList
width={docWidth}
height={docHeight}
itemCount={numPages}
estimatedItemSize={numPages}
itemSize={() => scale * docWidth}
>
{({ style, index }) => {
const currPage = index + 1;
return (
<div style={style}>
<Page scale={scale} pageNumber={currPage} />
</div>
);
}}
</VariableSizeList>
</Document>
</div>
);
};
You can install react-window here.
Update
Due to some UI issues with react-window, I decided to switch to react-pdf-viewer at the end. It's working pretty well for me so far.
I am trying to implement infinite scroller using intersection observer in react but the problem i am facing is that in the callback of intersection observer i am not able to read latest value of current 'page' and the 'list' so that i can fetch data for next page.
import ReactDOM from "react-dom";
import "./styles.css";
require("intersection-observer");
const pageSize = 30;
const threshold = 5;
const generateList = (page, size) => {
let arr = [];
for (let i = 1; i <= size; i++) {
arr.push(`${(page - 1) * size + i}`);
}
return arr;
};
const fetchList = page => {
return new Promise(resolve => {
setTimeout(() => {
return resolve(generateList(page, pageSize));
}, 1000);
});
};
let options = {
root: null,
threshold: 0
};
function App() {
const [page, setPage] = useState(1);
const [fetching, setFetching] = useState(false);
const [list, setlist] = useState(generateList(page, pageSize));
const callback = entries => {
if (entries[0].isIntersecting) {
observerRef.current.unobserve(
document.getElementById(`item_${list.length - threshold}`)
);
setFetching(true);
/* at this point neither the 'page' is latest nor the 'list'
*they both have the initial states.
*/
fetchList(page + 1).then(res => {
setFetching(false);
setPage(page + 1);
setlist([...list, ...res]);
});
}
};
const observerRef = useRef(new IntersectionObserver(callback, options));
useEffect(() => {
if (observerRef.current) {
observerRef.current.observe(
document.getElementById(`item_${list.length - threshold}`)
);
}
}, [list]);
return (
<div className="App">
{list.map(l => (
<p key={l} id={`item_${l}`}>
{l}
</p>
))}
{fetching && <p>loading...</p>}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
current behaviour: value of 'page' and 'list' is always equals to the initial state and not the latest value. Infinite scroll is not working after page 2
expected behaviour: In callback function it should read updated value of state 'page' and 'list'.
Here is the working sandbox of this demo https://codesandbox.io/s/sweet-sun-rbcml?fontsize=14&hidenavigation=1&theme=dark
There are primary two problems here, closures and querying the DOM directly.
To solve the closures problem, use functional useState and references:
const listLengthRef = useRef(list.length);
const pageRef = useRef(page);
const callback = useCallback(entries => {
if (entries[0].isIntersecting) {
observerRef.current.unobserve(
document.getElementById(`item_${listLengthRef.current - threshold}`)
);
setFetching(true);
fetchList(pageRef.current + 1).then(res => {
setFetching(false);
setPage(page => page + 1);
setlist(list => [...list, ...res]);
});
}
}, []);
const observerRef = useRef(new IntersectionObserver(callback, options));
useEffect(() => {
listLengthRef.current = list.length;
}, [list]);
useEffect(() => {
pageRef.current = page;
}, [page]);
Although this code works, you should replace document.getElementById with reference, in this case, it will be a reference to the last element of the page.
You can make use of the React setState callback method to guarantee that you will receive the previous value.
Update your callback function as the following and it should work.
const callback = entries => {
if (entries[0].isIntersecting) {
setFetching(true);
setPage(prevPage => {
fetchList(prevPage + 1).then(res => {
setFetching(false);
setlist(prevList => {
observerRef.current.unobserve(document.getElementById(`item_${prevList.length - threshold}`));
return ([...prevList, ...res]);
});
})
return prevPage + 1;
})
}
};
I think the problem is due to the ref keeps reference to the old observer. you need to refresh observer everytime your dependencies gets updated. it relates to closure in js. I would update your app to move the callback inside useEffect
function App() {
const [page, setPage] = useState(1);
const [fetching, setFetching] = useState(false);
const [list, setlist] = useState(generateList(page, pageSize));
const observerRef = useRef(null);
useEffect(() => {
const callback = entries => {
if (entries[0].isIntersecting) {
observerRef.current.unobserve(
document.getElementById(`item_${list.length - threshold}`)
);
setFetching(true);
/* at this point neither the 'page' is latest nor the 'list'
*they both have the initial states.
*/
console.log(page, list);
fetchList(page + 1).then(res => {
setFetching(false);
setPage(page + 1);
setlist([...list, ...res]);
});
}
};
observerRef.current = new IntersectionObserver(callback, options);
if (observerRef.current) {
observerRef.current.observe(
document.getElementById(`item_${list.length - threshold}`)
);
}
}, [list]);
return (
<div className="App">
{list.map(l => (
<p key={l} id={`item_${l}`}>
{l}
</p>
))}
{fetching && <p>loading...</p>}
</div>
);
}
I am trying to change the height of a container, when in mobile landscape mode only. I am playing around in the developer tools to swap the orientation of a mobile device but it only works on the first render. I am new to react hooks so not sure if I am implementing it right.
The idea is that I am testing that once in landscape, if it's on mobile the height should be less than 450px (which is the check I am doing for the if statement)
Could someone point me in the right direction, please?
Thanks!
const bikeImageHeight = () => {
const windowViewportHeight = window.innerHeight;
const isLandscape = window.orientation === 90 || window.orientation === -90;
let bikeImgHeight = 0;
if (windowViewportHeight <= 450 && isLandscape) {
bikeImgHeight = windowViewportHeight - 50;
}
return bikeImgHeight;
};
useEffect(() => {
bikeImageHeight();
window.addEventListener("resize", bikeImageHeight);
return () => {
window.removeEventListener("resize", bikeImageHeight);
};
}, []);
The useEffect hook is not expected to fire on orientation change. It defines a callback that will fire when the component re-renders. The question then is how to trigger a re-render when the screen orientation changes. A re-render occurs when there are changes to a components props or state.
Lets make use of another related stackoverflow answer to build a useWindowDimensions hook. This allows us to hook into the windows size as component state so any changes will cause a re-render.
useWindowDimensions.js
import { useState, useEffect } from 'react';
function getWindowDimensions() {
const { innerWidth: width, innerHeight: height } = window;
return {
width,
height
};
}
export default function useWindowDimensions() {
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
useEffect(() => {
function handleResize() {
setWindowDimensions(getWindowDimensions());
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowDimensions;
}
You can then use that hook in your component. Something like:
BikeImage.js
import React from 'react'
import useWindowDimensions from './useWindowDimensions'
export default () => {
const windowDimensions = useWindowDimensions();
// Define these helper functions as you like
const width = getImageWidth(windowDimensions.width)
const height = getImageHeight(windowDimensions.height)
// AppImage is a component defined elsewhere which takes
// at least width, height and src props
return <AppImage width={width} height={height} src="..." .../>
}
Here is a custom hook that fires on orientation change,
import React, {useState, useEffect} from 'react';
// Example usage
export default () => {
const orientation = useScreenOrientation();
return <p>{orientation}</p>;
}
function useScreenOrientation() {
const [orientation, setOrientation] = useState(window.screen.orientation.type);
useEffect(() => {
const handleOrientationChange= () => setOrientation(window.screen.orientation.type);
window.addEventListener('orientationchange', handleOrientationChange);
return () => window.removeEventListener('orientationchange', handleOrientationChange);
}, []);
return orientation;
}
Hope this takes you to the right direction.
You need to trigger a re-render, which can be done by setting state inside of your bikeImageHeight.
const [viewSize, setViewSize] = useState(0)
const bikeImageHeight = () => {
const windowViewportHeight = window.innerHeight;
const isLandscape = window.orientation === 90 || window.orientation === -90;
let bikeImgHeight = 0;
if (windowViewportHeight <= 450 && isLandscape) {
bikeImgHeight = windowViewportHeight - 50;
}
setViewSize(bikeImgHeight)
return bikeImgHeight;
};
And per the comments conversation, here's how you'd use debounce:
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
const YourComponent = () => {
const bikeImageHeight = () => {
const windowViewportHeight = window.innerHeight;
const isLandscape = window.orientation === 90 || window.orientation === -90;
let bikeImgHeight = 0;
if (windowViewportHeight <= 450 && isLandscape) {
bikeImgHeight = windowViewportHeight - 50;
}
setViewSize(bikeImgHeight)
return bikeImgHeight;
};
const debouncedBikeHeight = debounce(bikeImageHeight, 200)
useEffect(() => {
bikeImageHeight();
window.addEventListener("resize", debouncedBikeHeight);
return () => {
window.removeEventListener("resize", debouncedBikeHeight);
};
}, []);
return <div>some jsx</div>
}
Example debounce taken from here: https://davidwalsh.name/javascript-debounce-function