React scroll event keeps on firing - javascript

I'm working on a react functional components project, wherein I've to increment scroll position programmatically when a user actually scrolls in a div.
for example, I've a div with id testDiv & a user scrolled down a bit, now the scroll position is 100, so I want to programmatically increment it by 1 and make it 101.
Problem statement: The scroll position keeps on incrementing via onScroll handler, so the scrollbar only stops at the end of the element even if we scroll only once.
Expected behaviour: Scroll position should be incremented only once by the onScroll handler if we scroll once on the UI.
What I tried: (dummy code for the reproduction purpose)
import React, { useCallback } from "react";
import ReactDOM from "react-dom";
const App = (props) => {
const onScroll = useCallback((event) => {
const section = document.querySelector('#testDiv');
// **The problem is here, scrollTop keeps on incrementing even if we scrolled only once**
if (section) section.scrollTop = event.scrollTop + 1;
}, []);
return (
<div id="testDiv" onScroll={onScroll} style={{ height: "500px", overflowY: "scroll" }}>
<div>test</div>
<div className="forceOverflow" style={{height: 500 * 25}}></div>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));

What you're looking for is throttling or debouncing the function. It keeps on incrementing because with every bit of scroll, onScroll is being called.
There are many ways to throttle functions in react but I like to use debounce from lodash. If I remember correctly it was about 1.5kb gzipped.
I made you a sandbox. And here is the code:
import React, { useCallback } from "react";
import _debounce from "lodash/debounce";
export const App = (props) => {
let debouncedOnScroll = useCallback(
_debounce(() => {
// YOUR CODE HERE
console.log("CHECK THE CONSOLE TO SEE HOW MANY TIMES IT'S GETTING CALLED");
}, 150), []); // Wait 150ms to see if I'm being called again, if I got called before 150ms, don't run me!
return (
<div
id="testDiv"
onScroll={debouncedOnScroll}
style={{ height: "500px", overflowY: "scroll" }}
>
<div>test</div>
<div className="forceOverflow" style={{ height: 500 * 25 }}></div>
</div>
);
};
By the way, use useRef API instead of document.querySelector. This query selector is getting called with every scroll and it's not the lightest weight on the client computer.

Related

Check for Windowsize React component

What I am basically trying to create is a navbar that has two completely different html hierarchy based on the window size. I want it to be different for mobile than for a desktop version. eg a nav bar that is on the right on desktop and one that is on the top for mobile.
A simply state of what was doing. I created a const that would use a state of the screen size. I had used the useState() to get a default for now but I know that if I was first loading on desktop and it it was defaulted to mobile. I would have to resize first to get the desktop version instead.
const [sizeState, setSizeState] = useState("mobile");
const changeNavbar = () => {
if (window.innerWidth <= 900) {
setSizeState("mobile");
} else {
setSizeState("desktop");
}
};
window.addEventListener('resize', changeNavbar);
the sizeState would then call an if function determin what state it curently is set to.
if (sizeState === "mobile") {
return ( //some code for mobile) }
else {
// return some code for desktop
}
for now it always returns the mobile version even if loading upon a innderwidth that is above 900 abd only on resize it would do something.
I have been trying to use a onload stuff and an eventlistener that would listen to load. but i cant manage to call the changeNavbar function on the first load of the page.
I saw people recomending usein useMediaQuerry but i dont know how to get it to work based on my if (mediaquery is set to md) { return( mobile navbar) }
if someone could help me use the useMediaQuerry in this instead of my previous attempt, so that i can have two seperated returns i would also be soooooo thankful for the help!
You can simply implement it using styled-components and styled-breakpoints packages and its hooks API.
Here is an example: https://codesandbox.io/s/styled-breakpoints-n8csvf
import { down } from "styled-breakpoints";
import { useBreakpoint } from "styled-breakpoints/react-styled";
export default function App() {
const isMobile = useBreakpoint(down("sm"));
return (
<div className="App">
{isMobile ? "Mobile View" : "Desktop View"}
</div>
);
}
Or you can create custom hooks like this: https://github.com/jasonjin220/use-window-size-v2
import useWindowSize from "use-window-size-v2";
export default function App() {
const { width, height } = useWindowSize();
return (
<div className="box">
<h1>useWindowSize Hook</h1>
<p>
height: {height}
<br />
width: {width}
</p>
</div>
);
}

Having trouble paginating search results using useState, Material-UI and custom hook

I am having an issue with pagination on a page in my React application. On the page, search results are rendered when one types into the search bar (naturally). I think my issue arises from how pagination is set up on this page.
Pagination works fine as long as the user clicks back to the first page before searching for anything else. For example, if the user is on page 3 and then types something new into the search bar, the new search will not display without the user clicking 'page 1' again on the pagination bar. However if they returned to page 1 of their initial search before doing the new search, page 1 of the new search displays properly. Hopefully this makes sense. Here is the page where the issue occurs:
import React, { useState } from "react";
import Pagination from "#material-ui/core/Pagination";
import usePagination from "./usePagination.js";
export default function Main({reviews, web3}) {
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const updateSearch = (event) => {
setSearch(event.target.value.substr(0, 20));
}
let filteredReviews = reviews.filter(
(review) => {
return review.restaurantName.indexOf(web3.utils.toHex(search)) !== -1;
});
let paginatedReviews = usePagination(filteredReviews, 2);
const handleChange = (e, p) => {
setPage(p);
paginatedReviews.jumpPage(p);
}
return (
<div className="container-fluid mt-5" style={{ minHeight: "100vh" }}>
<div className="row">
<main role="main" className="col-lg-12 ml-auto mr-auto" style={{ maxWidth: '500px' }}>
<div className="content mr-auto ml-auto">
<input type="text" className="form-control" value={search} onChange={updateSearch} />
{filteredReviews.length > 0 ? paginatedReviews.pageData().map((review, key) => {
return (
<>
<div key={key}>
// search result item
</div>
</>
)
})
{filteredReviews.length > 1
? <Pagination
count={paginatedReviews.maxPage}
page={page}
onChange={handleChange}
/>
: null
)
</div>
</main>
</div>
</div>
);
}
and here is usePagination:
import { useState } from "react";
export default function usePagination(allReviews, perPage) {
const [currentPage, setCurrentPage] = useState(1);
const maxPage = Math.ceil(allReviews.length / perPage);
function pageData() {
const start = (currentPage - 1) * perPage;
const end = start + perPage
return allReviews.slice(start, end);
}
function jumpPage(page) {
const pageNumber = Math.max(1, page);
setCurrentPage((currentPage) => Math.min(pageNumber, maxPage));
}
return { jumpPage, pageData, currentPage, maxPage }
}
I thought I could resolve the issue I'm having by adding setPage(1) to updateSearch in order to have the page automatically move to page 1 for each new search, but that didn't work, as you still had to click page 1 on the actual pagination bar for the results to show up.
Edit: I tried renaming currentPage and setCurrentPage in the hook so that they shared the same names as on my page, but that also did not work.
Thanks in advance for any help you can offer. If you need me to elaborate on anything I will happily do so.
How about updating the page in a useEffect? That way you'll make sure all hooks have run and their return values are up-to-date (useEffect runs after render). If you reset the page too early, at the same time as the search query, jumpPage might rely on stale data: your search results and the internal usePagination values like maxPage will not have had a chance to recalculate yet.
Here is a working example based off your codesandbox: https://codesandbox.io/s/restless-dust-28351
Note that to make sure useEffect runs on search change only, you need to wrap jumpPage in a useCallback so that the jumpPage function reference remains the same. In general, I'd recommend you do that to methods returned from custom hooks. This way those methods are safer to consume anywhere, including as dependencies to useEffect, useCallback etc.
Also I'd recommend destructuring the custom hook return values, so that each of them can be used on its own as a hook dependency, like jumpPage in my example above.
I've also removed the page state from App, as it's already tracked in usePagination and returned from there. Having usePagination as a single source of truth that encapsulates all your pagination stuff makes things simpler. Simplicity is a great ideal to strive for:)
Lastly, a small side note: it's best not use <br /> purely as a spacer. It clutters up the markup without contributing any useful semantics, and it's better to leave the spacing concern to CSS.
And good luck with your React endeavors, you're doing great!

Material UI React Slider Component Not Working on mobile

I have been trying to add Slider component to a react project. functionality wise its working fine but i am having two issues which i am not able to get rid of
changing value of the slider is not smooth. Dragging doesn't work properly, its just drags to the nearest value and then stops.
On a mobile device its even worse, no dragging at all, i have to tap on the exact spot for the slider to move.
I did find the issue, i was using onChange, so when i removed it it worked exactly like the example. But i need to update state of parent component, so added line 18, but then again the same issue appeared. I fi remove line 18 then all this gets fixed but i need line 18 to call a function of parent component, to update its state variable.
Here is the gist link of my code
https://gist.github.com/kapiljhajhria/0e9beda641d561ef4448abf9195dbcca
import React from "react";
import Slider from "#material-ui/core/Slider";
export default function SliderWithLabel(props) {
const {
labelText, range = {
min: 0,
max: 10
}, step = 1,
// defaultValue = Math.ceil((range.min + range.max) / 2),
handleSliderChange,
name,
value: sliderValue
} = props;
function sliderValuetext(value) {
// handleChange({target: {value: value}});
if(value!==sliderValue)handleSliderChange(value,name)
return `${value}`;
}
return (
<div className="sliderField" style={{display: "flex", flexDirection: "column"}}>
<div>
{labelText}
</div>
<Slider
style={{width: "90%", justifyContent: "center", display: "flex", margin: "auto"}}
defaultValue={sliderValue}
getAriaValueText={sliderValuetext}
aria-labelledby="discrete-slider"
valueLabelDisplay="auto"
// onChange={sliderChange}
step={step}
// name={name}
// onChange={handleChange}
marks
min={range.min}
max={range.max}
/>
</div>
)
}
after spending 2 days on the issue, creating a sample project , trying to recreate the issue , it turned out to be a simple fix.
Parent component has a FORM, key which i was using for the form was
Date().getTime()
This was what was causing the issue with the slider. My guess would be that it was rebuilding the whole form with each slider value change. Which made slider UI behave in such a way. using appropraite key fixed the issue. I am now switching between two key value.

What does the .current property of the querySelector method refer to?

I came across this line of code via a snippet on https://usehooks.com,
document.querySelector('body').current
I haven't been able to find .current in the specification at all.
I was hoping someone could clarify its purpose in this context.
It's being used within the IntersectionObserver API in the full example (below) - perhaps the API is exposing the property?
Any help is much appreciated. Thanks in advance.
Following is the full source code:
import { useState, useEffect, useRef } from 'react';
// Usage
function App() {
// Ref for the element that we want to detect whether on screen
const ref = useRef();
// Call the hook passing in ref and root margin
// In this case it would only be considered onScreen if more ...
// ... than 300px of element is visible.
const onScreen = useOnScreen(ref, '-300px');
return (
<div>
<div style={{ height: '100vh' }}>
<h1>Scroll down to next section 👇</h1>
</div>
<div
ref={ref}
style={{
height: '100vh',
backgroundColor: onScreen ? '#23cebd' : '#efefef'
}}
>
{onScreen ? (
<div>
<h1>Hey I'm on the screen</h1>
<img src="https://i.giphy.com/media/ASd0Ukj0y3qMM/giphy.gif" />
</div>
) : (
<h1>Scroll down 300px from the top of this section 👇</h1>
)}
</div>
</div>
);
}
// Hook
function useOnScreen(ref, margin = '0px') {
// State and setter for storing whether element is visible
const [isIntersecting, setIntersecting] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
// Update our state when observer callback fires
setIntersecting(entry.isIntersecting);
},
{
rootMargin: margin,
root: document.querySelector('body').current
}
);
if (ref.current) {
observer.observe(ref.current);
}
return () => {
observer.unobserve(ref.current);
};
}, []); // Empty array ensures that effect is only run on mount and unmount
return isIntersecting;
}
document.querySelector('body').current is just a property of the body element, which has nothing to do with document.querySelector. It may have been set somewhere else as it is not an existing property of the body element.
var body = document.querySelector("body");
console.log("body.current:", "body.current");
body.current = "SOMEVALUE";
console.log("After setting body.current");
console.log("body.current:", "body.current");
Sorry to disappoint, but it doesn't do anything. It's just a way to supply undefined to the IntersectionObserver API. If you replace document.querySelector('body').current with undefined or remove the entire root field altogether, you still get the same result.
I removed that field to test it to verify the same behavior. Try it yourself in the Codesandbox link here.
As seen by this comment on the example, it can be removed entirely:
You can remove root entirely, since it defaults to the viewport anyway (also document.querySelector('body').current is always undefined, could be document.body but isn't needed anyway)

How to detect hidden component in React

In brief,
I have a infinite scroll list who render for each Item 5 PureComponent.
My idea is to somehow, only render the 5 PureComponent if the Item is visible.
The question is,
How to detect if the Item component is visible for the user or not?
Easiest solution:
add scrollPosition and containerSize to this.state
create ref to container in render()
<div ref={cont => { this.scrollContainer = cont; }} />
in componentDidMount() subscribe to scroll event
this.scrollContainer.addEventListener('scroll', this.handleScroll)
in componentWillUnmount() unsubscribe
this.scrollContainer.removeEventListener('scroll', this.handleScroll)
your handleScroll should look sth like
handleScroll (e) {
const { target: { scrollTop, clientHeight } } = e;
this.setState(state => ({...state, scrollPosition: scrollTop, containerSize: clientHeight}))
}
and then in your render function just check which element should be displayed and render correct ones numOfElementsToRender = state.containerSize / elementSize and firstElementIndex = state.scrollPosition / elementSize - 1
when you have all this just render your list of elements and apply filter base on element's index or however you want to sort them
Ofc you need to handle all edge cases and add bufor for smooth scrolling (20% of height should be fine)
You can use the IntersectionObserver API with a polyfill (it's chrome 61+) . It's a more performant way (in new browsers) to look for intersections, and in other cases, it falls back to piro's answer. They also let you specify a threshold at which the intersection becomes true. Check this out:
https://github.com/researchgate/react-intersection-observer
import React from 'react';
import 'intersection-observer'; // optional polyfill
import Observer from '#researchgate/react-intersection-observer';
class ExampleComponent extends React.Component {
handleIntersection(event) {
console.log(event.isIntersecting); // true if it gets cut off
}
render() {
const options = {
onChange: this.handleIntersection,
root: "#scrolling-container",
rootMargin: "0% 0% -25%"
};
return (
<div id="scrolling-container" style={{ overflow: 'scroll', height: 100 }}>
<Observer {...options}>
<div>
I am the target element
</div>
</Observer>
</div>
);
}
}

Categories