Handling scroll Animation in React - javascript

What is the correct way of dealing with scroll position in React? I really like smooth scrolling because of better UX. Since manipulating the DOM in React is an anti-pattern I ran into problem: how to scroll smoothly to some position/element? I usually change scrollTop value of an element, but this is a manipulation on the DOM, which is not allowed.
JSBIN
code:
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
handleClick = e => {
for (let i = 1; i <= 100; i++) {
setTimeout(() => (this.node.scrollTop = i), i * 2);
}
};
render() {
const someArrayToMap = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
return (
<div ref={node => this.node = node} style={{overflow: 'auto', height: '100vh'}}>
<button onClick={this.handleClick}>CLICK TO SCROLL</button>
{
[...someArrayToMap,
...someArrayToMap,
...someArrayToMap,
...someArrayToMap,
...someArrayToMap,
...someArrayToMap,
...someArrayToMap].map((e, i) => <div key={i}>some text here</div>)
}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
How to achieve this in a React way?

You can just use refs and the scrollIntoView method (with behavior: 'smooth' for smooth scrolling). It's only a few lines of code and doesn't require a package.
Say this is what you want to scroll to
<p ref={this.myRef} className="scrollToHere">[1] ...</p>
And some sort of button
<button onClick={() => {this.scroll(this.myRef)}} className="footnote">[1]</button>
to call the scroll method
class App extends Component {
constructor() {
super()
this.myRef = React.createRef();
scroll(ref) {
ref.current.scrollIntoView({behavior: 'smooth'})
}
}
EDIT: Because this method is not yet supported by all browsers (overview of browser support), you might want to use a polyfill.

window.scroll({top: 0, left: 0, behavior: 'smooth' }) works for me.
You also need to check for browser's compability
Or use polyfill
Edit: For the completeness' sake, here is how to dynamically polyfill with webpack.
if (!('scrollBehavior' in document.documentElement.style)) {
//safari does not support smooth scroll
(async () => {
const {default: smoothScroll} = await import(
/* webpackChunkName: 'polyfill-modern' */
'smoothscroll-polyfill'
)
smoothScroll.polyfill()
})()
}
By this dynamic polyfill, the package is loaded via ajax unless the browser supports the smooth scroll.
polyfill-modern is an arbitrary chunk name, which hints webpack compiler to combine packages together, in order to reduce the number of requests to the server.

The Simplest way to do: -
window.scrollTo({top: 0, left: 0, behavior: 'smooth' });
This simple JavaScript code is working with all browser.

Here's a small, no-dependancy solution using hooks
const useSmoothScrollTo = id => {
const ref = useRef(null)
useEffect(() => {
const listener = e => {
if (ref.current && location.hash === id) {
ref.current.scrollIntoView({behavior: 'smooth'})
}
}
window.addEventListener('hashchange', listener, true)
return () => {
window.removeEventListener('hashchange', listener)
}
}, [])
return {
'data-anchor-id': id,
ref
}
}
You use it like this:
export const FeaturesSection = () => {
const bind = useSmoothScrollTo('#features')
return (
<section {...bind} className={classes.features}>
...
</section>
)
}
Then anywhere else in your app you only need to do
Go to Features
Obviously, same caveats as above apply to .scrollIntoView({behavior: 'smooth'})

A couple good packages out there already that can handle this for you:
https://github.com/fisshy/react-scroll - Demo
https://www.npmjs.com/package/react-scroll-to-component
Simple scroll to component
Hope this helps!

There are several libraries for scrolling to anchors in React. The one to choose depend on the features you're seeking and the existing setup of your page.
React Scrollable Anchor is a lightweight library that's specifically for scrolling to anchors that are mapped to URL hashes. It also updates the URL hash based on currently focused section. [Full disclosure: I'm the author of this library]
React Scroll, mentioned in another answer, is a more fully featured library for scrolling to anchors, without any reflection of location in the URL.
You can also hook up something like React Router Hash Link Scroll if you're already using React Router, which will then also tie into your URL hash.

I really enjoy the react-router website in the API section, they seem to use this scrollToDoc component that is a really sweet translation of a typical VanillaJS smooth-scroll function into React which depends on react-motion!

Simple hook:
function useScrollTo(): [string, () => void] {
const id = useId();
const handleScroll = useCallback(() => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}, [id]);
return [id, handleScroll];
}
And usage:
function App() {
const [section2, scrollToSection2] = useScrollTo();
return (
<>
<button onClick={scrollToSection2}>Scroll</button>
<div id={section2}>Section 2</div>
</>
)
}

Related

Having an issue with making react component work inside a custom iframe component

So, I've basically tried everything with this one. I ran out of solutions or options. Thing is, I have a button. When you click on it your camera will open and you will see some filters that you can apply to your face. I am new to React. Made it work without the iframe to test the API first, but it's not working anymore inside this iframe. The react component needs to be inside this iframe. The code can be found here with what I did so far/tried: https://codesandbox.io/s/cool-fog-3k5si5?file=/src/components/button/button.jsx
The problem is that when I click the button, the canvas disappears from the page and I get this error in the console:
The DeepAR API fails initialization because the canvas is no longer on the page and it crashes. I really don't know what to search for as I considered this to be a react render error and I tried different ways to write the react code (functional/class). If you have any ideas or suggestions, please help. Thank you in advance.
Your use of useEffect in your Modal and App Component is incorrect.
To remind you, useEffect accepts a function which runs after the render is committed to the screen.
If the function returns a function (which is your case), this function is the "clean up" function which is run before the component is removed from the UI.
So what is happening is that your useEffect code is run only when your components are being unmounted.
Since we are not concerned with any clean up at this stage, a quick solution for you is to move the clean up expressions to the main effect function as follows:
useEffect(() => {
fetch(
"https://cors-anywhere.herokuapp.com/https://staging1.farmec.ro/rest/V1/farmec/deeparProducts/"
)
.then((response) => response.json())
.then((productsJson) => setProducts(productsJson));
}, []);
The same goes for your Modal component :
useEffect(() => {
let initializedDeepAR = new DeepAR({
licenseKey:
"6fda241c565744899d3ea574dc08a18ce3860d219aeb6de4b2d23437d7b6dcfcd79941dffe0e57f0",
libPath: DeepAR,
deeparWasmPath: deeparWasm,
canvas: canvas.current,
segmentationConfig: {
modelPath: segmentationMode
},
callbacks: {
onInitialize: () => {
// let filterName = colors[0].filterData[0]['Filter Binary Path'].match(new RegExp("[^/]+(?=\\.[^/.]*$)"))[0];
setDeepAR(initializedDeepAR);
initializedDeepAR.startVideo(true);
// initializedDeepAR.switchEffect(0, 'slot', `https://staging1.farmec.ro/media/deepArFilters/${filterName}.bin`);
}
}
});
/*#TODO: replace paths with server local path*/
initializedDeepAR.downloadFaceTrackingModel(models);
}, []);
With one additional fix concerning your use of useRef.
To target the element behind the useRef, you must use the .current property.
Finally, your Frame component is using useState to manage the mounting of the iframe. I would suggest using the useRef hook with a useState for your mountNode as follows:
export const Frame = ({
children,
styleSelector,
title,
...props
}) => {
const contentRef = useRef(null)
const [mountNode, setMountNode] = useState()
useEffect(() => {
setMountNode(contentRef.current.contentWindow.document.body)
}, [])
useEffect(() => {
const win = contentRef.current.contentWindow
const linkEls = win.parent.document.querySelectorAll(
styleSelector
)
if (linkEls.length) {
linkEls.forEach((el) => {
win.document.head.appendChild(el)
})
}
}, [styleSelector])
return (
<iframe title={title} {...props} ref={contentRef}>
{mountNode && createPortal(children, mountNode)}
</iframe>
)
}

Get scrollbar position with NextJs

I'm using NextJs to take advantage of Server Side Rendering. And also I have a navbar in my application that should change styles with scroll position. How can I check whether the window has been scrolled more than 100px, on my NextJs application?
You can simply use a useEffect hook like this:
import { useEffect, useState } from "react";
const IndexPage = () => {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY);
};
// just trigger this so that the initial state
// is updated as soon as the component is mounted
// related: https://stackoverflow.com/a/63408216
handleScroll();
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div style={{ height: 4000 }}> {/* just added to make scrollbar available */}
<div style={{ position: "fixed", top: 0 }}>
{scrollY > 100
? "Scrolled more than 100px"
: "Still somewhere near the top!"}
</div>
</div>
);
};
export default IndexPage;
Sandbox: https://codesandbox.io/s/cocky-drake-1xe0g
This code can be further optimized by debouncing the scroll handler. And optionally, setting the state only if it is not changed (not sure if newer versions of React handle this itself).
Your question was directly related to this thread: https://stackoverflow.com/a/59403018/11613622
For debouncing/throttling you can refer this: How to use throttle or debounce with React Hook?
Also if you don't want to use the provided solutions in that thread simply wrap the handleScroll with _.debounce and then feed it to the event handler.

SwipeableViews How to pass the updateHeight() as a prop

So I am using SwipeableViews no swipe between pages. My problem is each page have a different dynamic length. I have animateHeight as true but it only animate the height when the tab changes. In the documentation it says that the function updateHeight() can solve this.
https://react-swipeable-views.com/api/api/
Due to my lack of knowledge I just could not get the updateHeight since all the exampels I saw on class based app. I built my app in function based app. I just could not figure it out to pass it as props to call it later.
I've found a way to use the updateHeight function. It's a bit hacky, but it works 😅. I've added an interval and timeout to compensate for data loading in/slow machines. The useEffect cleans the interval when the page is unmounted.
There's probably a better solution, but this is what I've found so far!
export function SwipeView() {
const [ref, setRef] = useState(null);
const onRefChange = useCallback((node: SwipeableViews & HTMLDivElement) => {
// hacky solution to update the height after the first render. Height is not set correctly on initial render
setRef(node); // e.g. change ref state to trigger re-render
if (node === null) {
return;
} else {
interval = setInterval(() => {
// #ts-ignore typings are not correct in this package
node.updateHeight();
}, 100);
setTimeout(() => {
clearInterval(interval);
}, 10000);
}
}, []);
useEffect(() => {
return () => clearInterval(interval);
}, [interval]);
return (
<SwipeableViews
animateHeight
ref={onRefChange}
>
{children}
</SwipeableViews>
);
}

Screen orientation in ReactJS

how to get orientation type and lock orientation in ReactJS?
The Screen Orientation API doesn't work with ReactJS or I just don't know how to install and use it. Please guide me to do the above. Thanks.
Your use case is not that clear from your post. However, plain JavaScript (Window.matchMedia() method) along with a bit of CSS (media query for orientation) and event listener for window resize, may let you grab window orientation and store it within app state.
Following code demonstrates that concept (no live-demo due to stacksnippets limitations, so you may try that out at stackblitz):
import React, { useState, useEffect } from 'react'
import { render } from 'react-dom'
const rootNode = document.getElementById('root')
const App = () => {
const isLandscape = () => window.matchMedia('(orientation:landscape)').matches,
[orientation, setOrientation] = useState(isLandscape() ? 'landscape' : 'portrait'),
onWindowResize = () => {
clearTimeout(window.resizeLag)
window.resizeLag = setTimeout(() => {
delete window.resizeLag
setOrientation(isLandscape() ? 'landscape' : 'portrait')
}, 200)
}
useEffect(() => (
onWindowResize(),
window.addEventListener('resize', onWindowResize),
() => window.removeEventListener('resize', onWindowResize)
),[])
return (
<div>{orientation}</div>
)
}
render (
<App />,
rootNode
)
p.s. while above is pretty viable, it may become even more comprehensive in a short while as window.screen.orientation (working draft currently) will make it to the standard, one more improvement may seem obvious (listen for orientationchange event, rather than resize), but former doesn't seem to me working so smoothly for desktop version of the app

How to mix useCallback with useRef in functional components

I'm newish to React and am working on an infinite scroll component. Multiple components will use infinite scroll and need to be synchronized together (i.e., scrolling one element programmatically scrolls other components as well).
So I've created ScrollProvider which maintains scroll state among components (even if they're rerendered), and a lower level hook useScrollSync. useScrollState returns a ref and a handleScroll callback which modify the state in the scroll provider. That all works just fine. However, I separately want to measure a component's size. The example provided in by the React team shows a callback since that for sure will be executed once the component is mounted, and the element would not be null. The problem is that the div already has a ref from the useScrollSync hook.
The core question
If I wanted to measure my div in addition to using scroll sync on it, how do I assign both a callback ref AND other ref to it? Is there a pattern around this, given that an element can only have one div?
Some (simplified) code:
ScrollProvider
const ScrollContext = React.createCreateContext();
const ScrollProvider = ({initialScrollTop, initialScrollLeft}) => {
const controlledElements = useRef(new Map());
const scrollPositions = useRef({
scrollTop: initialScrollTop,
scrollLeft: initialScrollLeft,
controllingElementKey: null
});
const register = (key, controlledElementRef) => {
controlledElements.current.set(key, controlledElementRef);
}
const handleScrollHOF = (key) => {
return () => {
scrollPositions.controllingElementKey = key;
//some scrolling logic
}
}
return {register, scrollPositions, handleScrollHOF};
}
useScrollSync
const useScrollSync = () => {
const scrollContext = useContext(ScrollContext);
const elementRef = useRef(null);
const keyRef = useRef({key: Symbol()}); // this probably could also be useState
useEffect(() => {
scrollContext.register(keyRef, elementRef);
}, []);
return {ref: elementRef, handleScroll: handleScrollHOF(keyRef.current)};
}
SomeComponent (Round 1)
const SomeComponent = () => {
// this would be within the provider tree
const {ref, handleScroll} = useScrollSync();
return (
<div onScroll={handleScroll} ref={ref}>some stuff</div>
)
}
Now the challenge is adding in a measurements hook...
useMeasurements
const useMeasurements = () => {
// something like this, per the React team's Hooks FAQ
const [measurements, setMeasurements] = useState(null);
const measurementRef = useCallback((element) => {
if(element !== null) {
setMeasurements(element.getBoundingClientRect());
}
});
return {measurementRef, measurements};
}
In order to add this to SomeComponent...
SomeComponent (Round 2)
const SomeComponent = () => {
// this would be within the provider tree
const {ref, handleScroll} = useScrollSync();
const {measurementRef, measurements} = useMeasurements();
// I cannot assign measurementRef to this same div,
// and changing useMeasurements to just measure an injected ref winds up
// with that ref being null and it never being recalculated
return (
<div onScroll={handleScroll} ref={ref}>some stuff</div>
)
}
I've sort of hit a wall here, or maybe I'm just overtired. Any thoughts on how to get beyond this?
The main issue I see is that you aren't referencing a viable ref in useMeasurement. In addition, useCallback executes synchronously as part of rendering, before the DOM is created. You'll need to reference your element in another useEffect hook.

Categories

Resources