undefined when using useRef and scroll event of window? - React - javascript

I have a navbar with position sticky and when I go up to top 0 I change the color, using useRef, I apply a class .ToolbarSticky when the getBoundingClientRect().top is 0, that is, it is up, although it works it gives me an error for undefined as seen in the console
this is my code
import { memo, useRef } from 'react';
import styles from '../styles/NotesToolbar.module.css';
import FilterSelect from './FilterSelect';
import NotesCounter from './NotesCounter';
const NotesToolbar = () => {
const toolbarRef = useRef();
window.addEventListener('scroll', () => {
if (toolbarRef.current.getBoundingClientRect().top <= 0) {
toolbarRef.current.classList.add(styles.ToolbarSticky);
} else {
toolbarRef.current.classList.remove(styles.ToolbarSticky);
}
});
return (
<div className={styles.Toolbar} ref={toolbarRef}>
<div className={styles.ToolbarLeft}>
<FilterSelect />
</div>
<div className={styles.ToolbarRight}>
<NotesCounter />
</div>
</div>
);
};
export default memo(NotesToolbar);

Few things about your code
First - you should have the event listener inside a useEffect and cleanup after the component unrenders or you will have tons of eventListeners
Nest - ref is defined (or assigned) to the dom element just before render. So ref will be undefined at first. A simple check if(ref.current) do_stuff will fix it.
useEffect(() => {
window.addEventListener('scroll', scrollHandler);
return(() => window.removeEventListener('scroll', scrollHandler);
}, [])
You can define the scrollHandler function inside or outside the effect (but if function is heavy, outside would be better)
const scrollHandler = () => {
if (toolbarRef.current) {
if (toolbarRef.current.getBoundingClientRect().top <= 0) {
toolbarRef.current.classList.add(styles.ToolbarSticky);
} else {
toolbarRef.current.classList.remove(styles.ToolbarSticky);
}
}

Related

React useRef scrollIntoView only fires once

I'm trying to scroll to an element when it comes into view. The problem is that it only works on a reload when it's already in view.
I've tried debugging it, but the ref seems to be correct and the conditionals pass. There is no return or error message so I don't know how to debug this further.
The hook works as it should so I'm really struggling to figure out what the cause is...
I need to put this in useEffect later on, but even this basic setup doesn't work. Any help is very much appreciated!
EDIT: I need to get this in the center of the screen so that I can overtake the scroll and animate the element on scroll. If I already start that functionality without it being centered, it'll stick to the bottom of the screen while it animates.
This is the component
const Component = () => {
const sectionRef = useRef<HTMLDivElement>(null);
const isOnScreen = useOnScreen(sectionRef);
if (isOnScreen && sectionRef?.current) {
sectionRef.current.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest'});
}
return (
<section ref={sectionRef}>
// ...child components
</section>
)
}
export default Component
This is the hook
import { useEffect, useState, useRef, RefObject } from 'react';
export default function useOnScreen(ref: RefObject<HTMLElement>) {
const observerRef = useRef<IntersectionObserver | null>(null);
const [isOnScreen, setIsOnScreen] = useState(false);
useEffect(() => {
observerRef.current = new IntersectionObserver(([entry]) =>
setIsOnScreen(entry.isIntersecting)
);
}, []);
useEffect(() => {
if (ref.current) {
observerRef.current?.observe(ref.current);
}
return () => {
observerRef.current?.disconnect();
};
}, [ref]);
return isOnScreen;
}

how to translate this javascript code into JSX React

so I have this javascript code that I want to use in React react, it's working so far, so this is what I got, first of all
THIS IS MY JAVASCRIPT CODE the one that I want to turn into GATSBY react JSX
function onLoad() {
var showDiv;
if(localStorage.getItem ("showDiv") == null) {
showDiv = true;
}
else {
showDiv = localStorage.getItem ("showDiv")
}
if (showDiv) {
document.getElementById ('myDiv') .style.display = 'block';
}
else {
document.getElementById ('myDiv') .remove();
}
}
function onClose() {
document.getElementById ('myDiv') .remove();
localStorage.setItem("showDiv", false);
}
AND THIS IS WHAT I GOT SO FAR, its working but I don't know how to code the onClose function the one right above from the javascript code
SO THIS IS MY COMPONENT
import React, { useEffect } from 'react';
import '../index.css'
export default function Com() {
useEffect(() => {
var showDiv;
if (localStorage.getItem ("showDiv") == null) {
showDiv = true;
}
else {
showDiv = localStorage.getItem ("showDiv")
}
if (showDiv) {
document.querySelector ('.contenedor').style.display = 'block';
}
else {
document.querySelector ('.contenedor').remove();
}
() => {
document.querySelector ('.contenedor').remove(); /* THIS IS THE ANONYMOUS FUNCTION */
localStorage.setItem("showDiv", false);
}
}, []);
return (
<div className="contenedor" style={{display: "none"}}>
<img className="portada" src="https://res.cloudinary.com/lenguaestudiocreativo/image/upload/v1626228418/hportada_jgljqd.svg" alt=""/>
</div>
);
}
And so this is my main index:
import React from 'react';
import './index.css';
import Com from './componentes/Comdos';
export default function index() {
return (
<Com />
)
};
I tried to use this anonymous function but it doesn't work, of course, if I remove this anonymous function it works right, but I also need this last part of the code, the onClose function, if the anonymous function doesn't work then how do I code this?
and so this is the error that I'm getting with the anonymous function
ERROR in
C:\Users\USUARIO\Downloads\VSCODE\JSXejercicios\landingpage\src\pages\componentes\Comdos.js
22:10 error Expected an assignment or function call and instead saw
an expression no-unused-expressions
✖ 1 problem (1 error, 0 warnings)
you can use react useState and useEffect hooks, try this one :
import React, { useState, useEffect } from "react";
export default function App() {
const [show, setShow] = useState(true);
useEffect(() => {
if (localStorage.getItem("showDiv") == null)
localStorage.setItem("showDiv", show);
}, []);
function togglePic() {
setShow(!show ? true : false);
localStorage.setItem("showDiv", !show);
}
return (
<>
<button onClick={togglePic}>Click to toggle the picture</button>
<div className="contenedor" style={{ display: show ? "block" : "none" }}>
<img
className="portada"
src="https://static.wikia.nocookie.net/spongebob/images/3/3e/SpongeBob_Star_show.jpeg"
alt=""
/>
</div>
</>
);
}
Reference:
Here are some references you can explore more
ReactJs Documentation - Conditional Rendering
ReactJs Documentation - Handling Events
ReactJs Documentation - Hooks State
ReactJs Documentation - Hooks Effect
Code Example:
try live code using CodeSanbox
Code Sanbox -
Demo

Adding events after the page render React

I have a situation when SVG loaded from the server after the page render and after that, I need to add certain events to its elements. Let's say after clicking on a rect it shows an alert.
import React, { useEffect, useState } from 'react';
import css from "./room-plan.module.css";
import { Button } from "../../components";
export default function RoomPlan({ svg }) {
useEffect(() => {
var elements = Array.from(document.querySelectorAll('svg rect'));
elements.forEach(function(el) {
el.addEventListener("click", alert("hello"));
})
}, [])
return (
<div>
<h2 className={css.labels}>Select desk</h2>
<div id={css.roomPlan} dangerouslySetInnerHTML={{__html: svg, }}></div>
<div className={css.bookingButtons}>
<Button>CANCEL</Button>
<Button>BOOK DESK</Button>
</div>
</div>
);
}
But it totally does not work. Just does not add any events on elements. Cuz when it looks for the required selector it does not exist yet.
The answer above is correct, but make sure to include a return function inside the useEffect for cleanup during component unmount
useEffect(() => {
var elements = Array.from(document.querySelectorAll('svg rect'));
const listener = () => alert("hello");
if(elements) {
elements.forEach(function(el) {
el.addEventListener("click", listener);
})
}
return function cleanup(){
if(elements) {
elements.forEach(function(el) {
el.removeEventListener("click", listener);
})
}
}
},[])

How to change the nav bar color when it reaches the next div

Is there a way to for the header to change color when it reaches the next div? I am trying to have it where the nav bar color changes when scrolling to the next div "container"
link
Just like #neaumusic has already answered, adding a scroll event listener can help.
Here is a working code I wrote: codesandbox
What I like to do is separating the event listener to a custom hook.
import { useEffect, useState, useRef, RefObject } from "react";
interface ITopBottom {
top: number;
bottom: number;
}
const useElementLocation = <T extends HTMLElement>(): [
RefObject<T>,
ITopBottom
] => {
// ref object to return
const ref = useRef<T>(null);
// you can customize this to include width, height, etc.
const [loc, setLoc] = useState<ITopBottom>({ top: 0, bottom: 0 });
useEffect(() => {
const listener = () => {
const rect = ref.current?.getBoundingClientRect()
if(rect){
setLoc({
top:rect.top,
bottom: rect.bottom,
})
}
};
// add the listener as the component mounts
window.addEventListener("scroll",listener)
// guarantee the listener is executed at least once
listener();
// clean up
return ()=>window.removeEventListener("scroll",listener)
}, []);
return [ref,loc]
};
export default useElementLocation;
This hook returns a ref object to be placed in the div, and the corresponding position you need.
Now you know the bounding top and bottom position, use logical statements to determine whether the header has reached a target div, and change the color depending on the result.
import React, {useState, useEffect} from 'react'
import useElementLocation from "./useElementLocation"
export default () => {
const [headerRef, headerLoc] = useElementLocation<HTMLDivElement>();
const [divRef, divLoc] = useElementLocation<HTMLDivElement>();
const [headerColor, setHeaderColor] = useState("white"); // default color
useEffect(()=>{
const {bottom: headerBottom} = headerLoc;
const {top,bottom} = divLoc;
if(top<headerBottom && headerBottom<bottom){
// header has reached the div
setHeaderColor("black");
} else {
// header has left the div, either to the higher or lower
setHeaderColor("white");
}
},[divLoc, headerLoc]) //dependencies
return <div className="app">
<div className="header" style={{backgroundColor: headerColor}} ref={headerRef}></header>
<div className="div-to-watch" ref={divRef}></div>
</div>
}
Something like this
container.addEventListener('scroll', e => {
if (container.scrollTop > someChildElement.offsetTop) {
changeColor(navbar);
}
});

ReactJS how to render a component only when scroll down and reach it on the page?

I have a react component Data which includes several charts components; BarChart LineChart ...etc.
When Data component starts rendering, it takes a while till receiving the data required for each chart from APIs, then it starts to respond and render all the charts components.
What I need is, to start rendering each chart only when I scroll down and reach it on the page.
Is there any way could help me achieving this??
You have at least three options how to do that:
Track if component is in viewport (visible to user). And then render it. You can use this HOC https://github.com/roderickhsiao/react-in-viewport
Track ‘y’ scroll position explicitly with https://react-fns.netlify.com/docs/en/api.html#scroll
Write your own HOC using Intersection Observer API https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
To render component you may need another HOC, which will return Chart component or ‘null’ based on props it receives.
I have tried many libraries but couldn't find something that best suited my needs so i wrote a custom hook for that, I hope it helps
import { useState, useEffect } from "react";
const OPTIONS = {
root: null,
rootMargin: "0px 0px 0px 0px",
threshold: 0,
};
const useIsVisible = (elementRef) => {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
if (elementRef.current) {
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.unobserve(elementRef.current);
}
});
}, OPTIONS);
observer.observe(elementRef.current);
}
}, [elementRef]);
return isVisible;
};
export default useIsVisible;
and then you can use the hook as follows :
import React, { useRef } from "react";
import useVisible from "../../hooks/useIsVisible";
function Deals() {
const elemRef = useRef();
const isVisible = useVisible(elemRef);
return (
<div ref={elemRef}>hello {isVisible && console.log("visible")}</div>
)}
I think the easiest way to do this in React is using react-intersection-observer.
Example:
import { useInView } from 'react-intersection-observer';
const Component = () => {
const { ref, inView, entry } = useInView({
/* Optional options */
threshold: 0,
});
useEffect(()=>{
//do something here when inView is true
}, [inView])
return (
<div ref={ref}>
<h2>{`Header inside viewport ${inView}.`}</h2>
</div>
);
};
I also reccommend using triggerOnce: true in the options object so the effect only happens the first time the user scrolls to it.
you can check window scroll position and if the scroll position is near your div - show it.
To do that you can use simple react render conditions.
import React, {Component} from 'react';
import PropTypes from 'prop-types';
class MyComponent extends Component {
constructor(props){
super(props);
this.state = {
elementToScroll1: false,
elementToScroll2: false,
}
this.firstElement = React.createRef();
this.secondElement = React.createRef();
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll(e){
//check if scroll position is near to your elements and set state {elementToScroll1: true}
//check if scroll position is under to your elements and set state {elementToScroll1: false}
}
render() {
return (
<div>
<div ref={this.firstElement} className={`elementToScroll1`}>
{this.state.elementToScroll1 && <div>First element</div>}
</div>
<div ref={this.secondElement} className={`elementToScroll2`}>
{this.state.elementToScroll2 && <div>Second element</div>}
</div>
</div>
);
}
}
MyComponent.propTypes = {};
export default MyComponent;
this may help you, it's just a quick solution. It will generate you some rerender actions, so be aware.

Categories