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;
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
I have been trying to solve this problem for a couple days but can't seem to get it to work.
First, I have a custom hook to get if the client is viewing in mobile or desktop format.
that looks like this:
import { useState, useEffect } from "react";
function getWindowDimensions() {
if (typeof window !== "undefined") {
const { innerWidth: width, innerHeight: height } = window;
console.log("defined");
console.log(width < 768);
return width < 768 ? true : false;
}
console.log("undefined");
return true;
}
export default function useWindowDimensions() {
const [windowDimensions, setWindowDimensions] = useState(
getWindowDimensions()
);
function handleResize() {
setWindowDimensions(getWindowDimensions());
}
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return windowDimensions;
}
Now I am trying to use this in order to switch between a footer or a header style interaction on my website:
import useWindowDimensions from "hooks/useWindowDimensions";
import React, { useEffect } from "react";
import Footer from "./Footer";
import Header from "./Header";
export default function Shim(props) {
const isMobile = useWindowDimensions();
return (
<div>
{!isMobile && <Header {...props} />}
{isMobile && <Footer {...props} />}
</div>
);
}
I expected this to work perfectly, however it looks as if the the initial value is getting set when typeof window === "undefined", so, the mobile view is rendered in desktop size (if I resize the window after the site is loaded, it works as expected.) Would anyone be willing to lend me a hand as to how to get this to work appropriately?
Thank you!
I solve same problem with this context api hook.
import React, { createContext, useContext, useEffect, useState } from 'react';
export const SizeContext = createContext();
const SizeContextProvider = ({ children }) => {
const [width, setWidth] = useState(window.innerWidth);
const [isMobile, setIsMobile] = useState(true);
const [isDesktop, setIsDesktop] = useState(true);
function debounce(fn, ms) {
let timer;
return () => {
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
fn.apply(this, arguments);
}, ms);
};
}
useEffect(() => {
const debouncedHandleResize = debounce(() => {
setWidth(window.innerWidth);
}, 0);
window.addEventListener('resize', debouncedHandleResize);
return () => {
window.removeEventListener('resize', debouncedHandleResize);
};
});
useEffect(() => {
if (width <= 575) {
setIsMobile(true);
setIsDesktop(false);
} else if (width >= 576 && width < 767) {
setIsMobile(true);
setIsDesktop(false);
} else if (width >= 768 && width < 991) {
setIsMobile(false);
setIsDesktop(true);
} else if (width >= 992 && width < 1199) {
setIsMobile(false);
setIsDesktop(true);
} else {
setIsMobile(false);
setIsDesktop(true);
}
}, [width]);
return (
<SizeContext.Provider value={{ width, isDesktop, isMobile }}>
{children}
</SizeContext.Provider>
);
};
export const useSizeContext = () => useContext(SizeContext);
export default SizeContextProvider;
import context api use any component.
const App = () => {
return (
<SizeContextProvider>
<Device />
</SizeContextProvider>
)
}
Here Device Component use Context Api
const Device = () => {
const { isMobile, isDesktop } = useSizeContext();
return (
<div>
{isMobile && <h1> Small Device </h1>}
{isDesktop && <h1> Largest Device </h1>}
</div>
);
}
I'm trying to test this function in jest,
But I cannot override, resize the screen or create a virtual dom.
I'm running this testing on node.
ps: I tried to use jsdom but I failed.
functions.js
export const getScreenWidth = () => {
const screenWidth = window.screen.width;
if (screenWidth <= 425) return "mobile";
if (screenWidth <= 768) return "tablet";
if (screenWidth <= 1024) return "laptopSm";
if (screenWidth <= 1440) return "laptopLg";
if (screenWidth <= 2560) return "HD";
return screenWidth;
};
The screen can be mocked by overriding the global screen variable.
Example:
const mockScreen = (size) => {
const { screen } = window.screen;
delete window.screen;
window.screen = {
...screen,
width: size
};
};
test("getScreenWidth", () => {
mockScreen(300);
expect(getScreenWidth()).toBe("mobile");
mockScreen(1025);
expect(getScreenWidth()).toBe("laptopLg");
});
I tried to make this mock and it worked but I don't really know if this is the right way to do so.
const mockScreen = (size) => {
global.window = {};
global.window.screen = {};
global.window.screen.width = size;
};
so the final test will be
describe("getScreenWidth()", () => {
it.only("returns a string representing the width of the screen", () => {
const mockScreen = (size) => {
global.window = {};
global.window.screen = {};
global.window.screen.width = size;
};
mockScreen(425);
expect(jsf.getScreenWidth()).toBe("mobile");
mockScreen(2560);
expect(jsf.getScreenWidth()).toBe("HD");
});
});
If you work with react testing library simulate a resize like this
global.innerWidth = 1024;
global.dispatchEvent(new Event('resize'));
then expect your function to return the right size
I tested a custom hook like
function useWindowSize() {
const [width, setWidth] = useState(window.innerWidth);
const [height, setHeight] = useState(window.innerHeight);
useEffect(() => {
const handleResize = () => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
});
return { width, height };
}
this way
function TestUseWindowSize() {
const { height, width } = useWindowSize();
return (
<div>
<h1 data-testid="height">{height}</h1>
<h1 data-testid="width">{width}</h1>
</div>
);
}
describe('useWindowSize Custom Hook', () => {
it('should return height and width', () => {
const { getByTestId } = render(<TestUseWindowSize />);
// screen defaults by render function
expect(getByTestId('height')).toHaveTextContent(/768/);
expect(getByTestId('width')).toHaveTextContent(/1024/);
global.innerWidth = 1000;
global.dispatchEvent(new Event('resize'));
expect(getByTestId('width')).toHaveTextContent(/1000/);
});
});
I'm building a form - series of questions (radio buttons) the user needs to answer before he can move on to the next screen. For fields validation I'm using yup (npm package) and redux as state management.
For one particular scenario/combination a new screen (div) is revealed asking for a confirmation (checkbox) before the user can proceed. I want to apply the validation for this checkbox only if displayed.
How can I check if an element (div) is displayed in the DOM using React?
The way I thought of doing it was to set a varibale 'isScreenVisible' to false and if the conditions are met I would change the state to 'true'.
I'm doing that check and setting 'isScreenVisible' to true or false in _renderScreen() but for some reason it's going into an infinite loop.
My code:
class Component extends React.Component {
constructor(props) {
super(props);
this.state = {
formisValid: true,
errors: {},
isScreenVisible: false
}
this.FormValidator = new Validate();
this.FormValidator.setValidationSchema(this.getValidationSchema());
}
areThereErrors(errors) {
var key, er = false;
for(key in errors) {
if(errors[key]) {er = true}
}
return er;
}
getValidationSchema() {
return yup.object().shape({
TravelInsurance: yup.string().min(1).required("Please select an option"),
MobilePhoneInsurance: yup.string().min(1).required("Please select an option"),
Confirmation: yup.string().min(1).required("Please confirm"),
});
}
//values of form fields
getValidationObject() {
let openConfirmation = (this.props.store.Confirmation === true)? 'confirmed': ''
return {
TravelInsurance: this.props.store.TravelInsurance,
MobilePhoneInsurance: this.props.store.MobilePhoneInsurance,
Confirmation: openConfirmation,
}
}
setSubmitErrors(errors) {
this.setState({errors: errors});
}
submitForm() {
var isErrored, prom, scope = this, obj = this.getValidationObject();
prom = this.FormValidator.validateSubmit(obj);
prom.then((errors) => {
isErrored = this.FormValidator.isFormErrored();
scope.setState({errors: errors}, () => {
if (isErrored) {
} else {
this.context.router.push('/Confirm');
}
});
});
}
saveData(e) {
let data = {}
data[e.target.name] = e.target.value
this.props.addData(data)
this.props.addData({
Confirmation: e.target.checked
})
}
_renderScreen = () => {
const {
Confirmation
} = this.props.store
if(typeof(this.props.store.TravelInsurance) !== 'undefined' && typeof(this.props.store.MobilePhoneInsurance) !== 'undefined') &&
((this.props.store.TravelInsurance === 'Yes' && this.props.store.MobilePhoneInsurance === 'No') ||
(this.props.store.TravelInsurance === 'No' && this.props.store.MobilePhoneInsurance === 'Yes')){
this.setState({
isScreenVisible: true
})
return(
<div>
<p>Please confirm that you want to proceed</p>
<CheckboxField
id="Confirmation"
name="Confirmation"
value={Confirmation}
validationMessage={this.state.errors.Confirmation}
label="I confirm that I would like to continue"
defaultChecked={!!Confirmation}
onClick={(e)=> {this.saveData(e)} }
/>
</FormLabel>
</div>
)
}
else{
this.setState({
isScreenVisible: false
})
}
}
render(){
const {
TravelInsurance,
MobilePhoneInsurance
} = this.props.store
return (
<div>
<RadioButtonGroup
id="TravelInsurance"
name="TravelInsurance"
checked={TravelInsurance}
onClick={this.saveData.bind(this)}
options={{
'Yes': 'Yes',
'No': 'No'
}}
validationMessage={(this.state.errors.TravelInsurance) ? this.state.errors.TravelInsurance : null }
/>
<RadioButtonGroup
id="MobilePhoneInsurance"
name="MobilePhoneInsurance"
checked={MobilePhoneInsurance}
onClick={this.saveData.bind(this)}
options={{
'Yes': 'Yes',
'No': 'No'
}}
validationMessage={(this.state.errors.MobilePhoneInsurance) ? this.state.errors.MobilePhoneInsurance : null }
/>
this._renderScreen()
<ButtonRow
primaryProps={{
children: 'Continue',
onClick: e=>{
this.submitForm();
}
}}
</div>
)
}
}
const mapStateToProps = (state) => {
return {
store: state.Insurance,
}
}
const Insurance = connect(mapStateToProps,{addData})(Component)
export default Insurance
Here is a reusable hook that takes advantage of the IntersectionObserver API.
The hook
export default function useOnScreen(ref: RefObject<HTMLElement>) {
const [isIntersecting, setIntersecting] = useState(false)
const observer = useMemo(() => new IntersectionObserver(
([entry]) => setIntersecting(entry.isIntersecting)
), [ref])
useEffect(() => {
observer.observe(ref.current)
return () => observer.disconnect()
}, [])
return isIntersecting
}
Usage
const DummyComponent = () => {
const ref = useRef<HTMLDivElement>(null)
const isVisible = useOnScreen(ref)
return <div ref={ref}>{isVisible && `Yep, I'm on screen`}</div>
}
You can attach a ref to the element that you want to check if it is on the viewport and then have something like:
/**
* Check if an element is in viewport
*
* #param {number} [offset]
* #returns {boolean}
*/
isInViewport(offset = 0) {
if (!this.yourElement) return false;
const top = this.yourElement.getBoundingClientRect().top;
return (top + offset) >= 0 && (top - offset) <= window.innerHeight;
}
render(){
return(<div ref={(el) => this.yourElement = el}> ... </div>)
}
You can attach listeners like onScroll and check when the element will be on the viewport.
You can also use the Intersection Observer API with a polyfil or use a HoC component that does the job
Based on Avraam's answer I wrote a Typescript-compatible small hook to satisfy the actual React code convention.
import { useRef, useEffect, useState } from "react";
import throttle from "lodash.throttle";
/**
* Check if an element is in viewport
* #param {number} offset - Number of pixels up to the observable element from the top
* #param {number} throttleMilliseconds - Throttle observable listener, in ms
*/
export default function useVisibility<Element extends HTMLElement>(
offset = 0,
throttleMilliseconds = 100
): [Boolean, React.RefObject<Element>] {
const [isVisible, setIsVisible] = useState(false);
const currentElement = useRef<Element>();
const onScroll = throttle(() => {
if (!currentElement.current) {
setIsVisible(false);
return;
}
const top = currentElement.current.getBoundingClientRect().top;
setIsVisible(top + offset >= 0 && top - offset <= window.innerHeight);
}, throttleMilliseconds);
useEffect(() => {
document.addEventListener('scroll', onScroll, true);
return () => document.removeEventListener('scroll', onScroll, true);
});
return [isVisible, currentElement];
}
Usage example:
const Example: FC = () => {
const [ isVisible, currentElement ] = useVisibility<HTMLDivElement>(100);
return <Spinner ref={currentElement} isVisible={isVisible} />;
};
You can find the example on Codesandbox.
I hope you will find it helpful!
#Alex Gusev answer without lodash and using useRef
import { MutableRefObject, useEffect, useRef, useState } from 'react'
/**
* Check if an element is in viewport
* #param {number} offset - Number of pixels up to the observable element from the top
*/
export default function useVisibility<T>(
offset = 0,
): [boolean, MutableRefObject<T>] {
const [isVisible, setIsVisible] = useState(false)
const currentElement = useRef(null)
const onScroll = () => {
if (!currentElement.current) {
setIsVisible(false)
return
}
const top = currentElement.current.getBoundingClientRect().top
setIsVisible(top + offset >= 0 && top - offset <= window.innerHeight)
}
useEffect(() => {
document.addEventListener('scroll', onScroll, true)
return () => document.removeEventListener('scroll', onScroll, true)
})
return [isVisible, currentElement]
}
usage example:
const [beforeCheckoutSubmitShown, beforeCheckoutSubmitRef] = useVisibility<HTMLDivElement>()
return (
<div ref={beforeCheckoutSubmitRef} />
I have had the same problem, and, looks, I found the pretty good solution in pure react jsx, without installing any libraries.
import React, {Component} from "react";
class OurReactComponent extends Component {
//attach our function to document event listener on scrolling whole doc
componentDidMount() {
document.addEventListener("scroll", this.isInViewport);
}
//do not forget to remove it after destroyed
componentWillUnmount() {
document.removeEventListener("scroll", this.isInViewport);
}
//our function which is called anytime document is scrolling (on scrolling)
isInViewport = () => {
//get how much pixels left to scrolling our ReactElement
const top = this.viewElement.getBoundingClientRect().top;
//here we check if element top reference is on the top of viewport
/*
* If the value is positive then top of element is below the top of viewport
* If the value is zero then top of element is on the top of viewport
* If the value is negative then top of element is above the top of viewport
* */
if(top <= 0){
console.log("Element is in view or above the viewport");
}else{
console.log("Element is outside view");
}
};
render() {
// set reference to our scrolling element
let setRef = (el) => {
this.viewElement = el;
};
return (
// add setting function to ref attribute the element which we want to check
<section ref={setRef}>
{/*some code*/}
</section>
);
}
}
export default OurReactComponent;
I was trying to figure out how to animate elements if the are in viewport.
Here is work project on CodeSandbox.
This is based on the answer from Creaforge but more optimized for the case when you want to check if the component has become visible (and in TypeScript).
Hook
function useWasSeen() {
// to prevents runtime crash in IE, let's mark it true right away
const [wasSeen, setWasSeen] = React.useState(
typeof IntersectionObserver !== "function"
);
const ref = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (ref.current && !wasSeen) {
const observer = new IntersectionObserver(
([entry]) => entry.isIntersecting && setWasSeen(true)
);
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}
}, [wasSeen]);
return [wasSeen, ref] as const;
}
Usage
const ExampleComponent = () => {
const [wasSeen, ref] = useWasSeen();
return <div ref={ref}>{wasSeen && `Lazy loaded`}</div>
}
Keep in mind that if your component is not mounted at the same time as the hook is called you would have to make this code more complicated. Like turning dependency array into [wasSeen, ref.current]
After trying out the different proposed solutions with TypeScript, we have been facing errors due to the first render setting the default useRef to null.
Here you have our solution just in case it helps other people 😊
The hook
useInViewport.ts:
import React, { useCallback, useEffect, useState } from "react";
export function useInViewport(): { isInViewport: boolean; ref: React.RefCallback<HTMLElement> } {
const [isInViewport, setIsInViewport] = useState(false);
const [refElement, setRefElement] = useState<HTMLElement | null>(null);
const setRef = useCallback((node: HTMLElement | null) => {
if (node !== null) {
setRefElement(node);
}
}, []);
useEffect(() => {
if (refElement && !isInViewport) {
const observer = new IntersectionObserver(
([entry]) => entry.isIntersecting && setIsInViewport(true)
);
observer.observe(refElement);
return () => {
observer.disconnect();
};
}
}, [isInViewport, refElement]);
return { isInViewport, ref: setRef };
}
Usage
SomeReactComponent.tsx:
import { useInViewport } from "../layout/useInViewport";
export function SomeReactComponent() {
const { isInViewport, ref } = useInViewport();
return (
<>
<h3>A component which only renders content if it is in the current user viewport</h3>
<section ref={ref}>{isInViewport && (<ComponentContentOnlyLoadedIfItIsInViewport />)}</section>
</>
);
}
Solution thanks to #isma-navarro 😊
TypeScript based approach to #Creaforge's Intersection Observer approach, that fixes the issue with ref.current being potentially undefined if the hook was called before the element is mounted:
export default function useOnScreen<Element extends HTMLElement>(): [
boolean,
React.RefCallback<Element>,
] {
const [intersecting, setIntersecting] = useState(false);
const observer = useMemo(
() => new IntersectionObserver(([entry]) => setIntersecting(entry.isIntersecting)),
[setIntersecting],
);
const currentElement = useCallback(
(ele: Element | null) => {
if (ele) {
observer.observe(ele);
} else {
observer.disconnect();
setIntersecting(false);
}
},
[observer, setIntersecting],
);
return [intersecting, currentElement];
}
Usage:
const [endOfList, endOfListRef] = useOnScreen();
...
return <div ref={endOfListRef} />
Answer based on the post from #Alex Gusev
React hook to check whether the element is visible with a few fixes and based on the rxjs library.
import React, { useEffect, createRef, useState } from 'react';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, throttleTime } from 'rxjs/operators';
/**
* Check if an element is in viewport
* #param {number} offset - Number of pixels up to the observable element from the top
* #param {number} throttleMilliseconds - Throttle observable listener, in ms
* #param {boolean} triggerOnce - Trigger renderer only once when element become visible
*/
export default function useVisibleOnScreen<Element extends HTMLElement>(
offset = 0,
throttleMilliseconds = 1000,
triggerOnce = false,
scrollElementId = ''
): [boolean, React.RefObject<Element>] {
const [isVisible, setIsVisible] = useState(false);
const currentElement = createRef<Element>();
useEffect(() => {
let subscription: Subscription | null = null;
let onScrollHandler: (() => void) | null = null;
const scrollElement = scrollElementId
? document.getElementById(scrollElementId)
: window;
const ref = currentElement.current;
if (ref && scrollElement) {
const subject = new Subject();
subscription = subject
.pipe(throttleTime(throttleMilliseconds))
.subscribe(() => {
if (!ref) {
if (!triggerOnce) {
setIsVisible(false);
}
return;
}
const top = ref.getBoundingClientRect().top;
const visible =
top + offset >= 0 && top - offset <= window.innerHeight;
if (triggerOnce) {
if (visible) {
setIsVisible(visible);
}
} else {
setIsVisible(visible);
}
});
onScrollHandler = () => {
subject.next();
};
if (scrollElement) {
scrollElement.addEventListener('scroll', onScrollHandler, false);
}
// Check when just loaded:
onScrollHandler();
} else {
console.log('Ref or scroll element cannot be found.');
}
return () => {
if (onScrollHandler && scrollElement) {
scrollElement.removeEventListener('scroll', onScrollHandler, false);
}
if (subscription) {
subscription.unsubscribe();
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [offset, throttleMilliseconds, triggerOnce, scrollElementId]);
return [isVisible, currentElement];
}