React useRef scrollIntoView only fires once - javascript

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;
}

Related

Is the any way to identify whether a query is in view or not in react query

Assume I have 3 queries x, y and z. I want to update the query x only when it is in view. Is there any way to identify it ?
I tried to maintain the view query key as a global state, but it seems not working fine. Is there any way to identify without maintaining viewing query key as global state.
Is there any possible way to get list of viewing queries ???
First of all, you need to hold the state of the component if it's in the viewport or not. The IntersectionObserver API allows you to detect when an element enters or leaves the viewport, which you can use to update the state of your component.
Then you can use that state as a key in your useQuery. Also you can use it in enabled option if you want to prevent refetch when item is not on the viewport.
import React, { useState, useEffect, useRef } from 'react';
import { useQuery } from 'react-query';
function Test() {
const [isInViewport, setIsInViewport] = useState(false);
const ref = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsInViewport(true);
} else {
setIsInViewport(false);
}
});
});
observer.observe(ref.current);
return () => {
observer.unobserve(ref.current);
};
}, []);
useQuery(
[isInViewport],
() => (
/// your query
),
{
enabled: isInViewport,
},
);
return (
<div ref={ref}>
<div>{isInViewport ? 'In viewport' : 'Not in viewport'}</div>
</div>
);
}
export default Test;

AppState listener continues to work even though I remove it

I added a listener to screen A as you can see below. But when I navigate to screen B, I see that this listener is still running. How can I solve this problem?
import React, { useEffect } from "react"
import { Button, AppState } from "react-native"
import { MainRoutes } from "../../navigation/routes"
const ScreenA = ({ navigation }) => {
useEffect(() => {
const appStateListener = AppState.addEventListener('change', state => console.log(state));
return () => appStateListener.remove();
}, [])
return (
<Button
title={"Next"}
onPress={() => {
navigation.reset({
index: 0,
routes: [{ name: MainRoutes.ScreenB }],
})
}} />
)
}
export default ScreenA
The return of the useEffect runs when the screen is unmounted.
Navigating to a new screen will not unmount the old screen. Only an "orphaned" screen will be unmounted - i.e., on navigating back to screen A, screen B will be unmounted.
This scenario is described here in the docs: https://reactnavigation.org/docs/navigation-lifecycle/
To unmount when navigating away, an easy trick is to use the useIsFocused hook. You could save the listener in a ref.
const isFocused = useIsFocused();
const listener = useRef(null);
useEffect(() => {
listener.current = AppState.addEventListener('change', state => console.log(state));
}, [])
useEffect(() => {
if (!isFocused && listener.current) {
listener.current.remove();
}
}, [isFocused, listener]);
Even though you are navigating to Screen B, Screen A is not unmounted. Therefore, the code appStateListener.remove(); will not get executed.
One suggestion would be to add appStateListener to the global level (maybe in the App.js) since the app state will be used on many screens as the app grows and handling the listener in a centralized place will be helpful when the app grows too.
Therefore, I don't see any downside of adding it as a global listener.
Let's discuss more on that if anybody has a better idea. Because I also wanna find the best practice.

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

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);
}
}

React Infinite Scroll - Intersection Observer API always jumping back to top

I´m developing a React application with infinite scrolling using the Intersection Observer API (not using any 3rd library for infinite scrolling). My backend handles pagination properly, and the infinite scrolling also fetches the next parts when reaching the threshold. The problem is, that each time I scroll down and reach the threshold, the window is scrolled back to the top, which is not what I expect. It seems that not just the new items are going to be rendered, but instead the entire list of items.
Here is the way I´ve implemented it in React with Redux so far:
import React, {useEffect, useRef, useCallback} from "react";
import Spinner from "../../UI/Spinner";
import {withRouter} from "react-router-dom";
import {connect} from "react-redux";
import * as actions from "../../store/actions/index";
const List = props => {
const {items, next, loading, loadMoreData} = props;
const loader = useRef(null);
const loadMore = useCallback((entries) => {
const target = entries[0];
if (target.isIntersecting && next) {
!loading && loadMoreData(next);
}
}, [loading, next, loadMoreData]);
useEffect(() => {
const options = {
threshold: 1.0
};
// Observer API
const observer = new IntersectionObserver(loadMore, options);
const currentLoader = loader.current;
if (loader && currentLoader) {
observer.observe(currentLoader);
}
return () => observer.unobserve(loader.current);
}, [loader, loadMore]);
const content = <Spinner/>;
if (!loading) {
content = items.map((item, index) => return (<div key={index}>{item}</div>))
}
return (
<div>
{content}
<div ref={loader}></div>
</div>
)
}
const mapStateToProps = state => {
return {
items: state.items.items,
next: state.items.next,
loading: state.items.loading,
error: state.items.error
};
};
const mapDispatchToProps = dispatch => {
return {
fetchMoreData: (next) => dispatch(actions.fetchDataStart(next)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(List));
So summing up the problem is the following:
After scrolling down the screen and after reaching the set threshold the data is fetched successfully and added to my redux store. But the entire list is rendered again on the top of the page and I have to scroll down again. Any ideas how to solve this?
This topic can be closed. It was a small and stupid mistake. The re-rendering of the whole list is caused by the if statement where I populate my content variable. Removing this solves the issue and infinite scroll works like a charm.

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