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.
Related
somehow I got an infinity loop, the weird situation that I did solve it but I got a warning and I wish to fix the warning.
this is code that work:
import { ArrowDropDown, ArrowRight } from "#material-ui/icons";
import React, { useState, useEffect, useCallback } from "react";
import "./tree.css";
const Tree = ({ explorer }) => {
const [expand, setExpand] = useState(true);
const [arrow, setArrow] = useState(false);
const stateHandler = () => {
setExpand(!expand);
setArrow(!arrow);
};
useEffect(() => {
//this function will display only the first Tree as init the page.
stateHandler();
}, []);
return (
<div>
<div className="treeInfo">
{arrow ? (
<ArrowDropDown className="treeIcon" />
) : (
<ArrowRight className="treeIcon" />
)}
<span className="treeTitle" onClick={stateHandler}>
{explorer.name}
</span>
</div>
<div
style={{
display: expand ? "block" : "none",
paddingLeft: 20,
cursor: "pointer",
}}
>
{explorer.items.map((explore) => {
return <Tree key={explore.id} explorer={explore} />;
})}
{/* {explorer.items.map((explore) => (
<Tree explorer={explore} />
))} */}
</div>
</div>
);
};
export default Tree;
and this is the warning:
src\components\Tree\Tree.js
Line 17:6: React Hook useEffect has a missing dependency: 'stateHandler'. Either include it or remove the dependency array react-hooks/exhaustive-deps
Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.
WARNING in src\components\Tree\Tree.js
Line 17:6: React Hook useEffect has a missing dependency: 'stateHandler'. Either include it or remove the dependency array react-hooks/exhaustive-deps
webpack compiled with 1 warning
**
The result : (it's good its what I want to display a tree view of
files)
**
well as I can read I got this warning because useEffect got not dependencies, when I add the stateHandler dependencies I got an infinity loop, so I add a callback function but it still doesn't solve the infinity loop.
this is the code with the useCallback (its the same code, just with useCallback and a bit of configure of the useEffect):
const initTree = useCallback(() => {
setExpand(!expand);
setArrow(!arrow);
}, [setExpand, setArrow, arrow, expand]);
useEffect(() => {
//this function will display only the first Tree as init the page.
initTree();
}, [initTree]);
You need to change this:
const initTree = useCallback(() => {
setExpand(!expand);
setArrow(!arrow);
}, [setExpand, setArrow, arrow, expand]);
to this:
const initTree = useCallback(() => {
setExpand(e => !e);
setArrow(a => !a);
}, []);
Otherwise what happens is this:
The component renders, initTree variable is initialized with a function, the Effect is run and setExpand and setArrow are called.
That triggers a new render
The useCallback hook checks if the deps of initTree have changed and yes, arrow and expand have changed indeed, hence initTree variable is updated with a new function
The effect checks if initTree has changed from the previous render, and yes, it has changed, hence the effect executes again calling initTree again.
There you are stuck in an infinite render loop.
Eslint shouldn't complain of missing deps if you don't put the setState in deps, since they do not change during renders, unless you are passing them through props.
If you are ok with having another state, you can simply set something like a loading state and make the useEffect depend on that
State would look like this:
const [expand, setExpand] = useState(true);
const [arrow, setArrow] = useState(false);
const [loading, setLoading] = useState(true);
And then based on this, useEffect would change as follows:
useEffect(() => {
//this function will display only the first Tree as init the page.
stateHandler();
setLoading(false);
}, [loading]);
Once loading is set to false, it's value is not going to change and the useEffect will not be triggered indefinitely
I am trying to apply a parallax effect to an .svg image by using useRef() to grab bubblesRef and translateY() onScroll.
The parallax works but when I navigate to the next page I receive error "TypeError: Cannot read property 'style' of null". I think it is because the addEventListener is still listening and trying to useRef() on bubblesRef while navigating to the next page. So I added the cleanup function in useEffect() but that doesn't seem to fix it.
Any help is appreciated. Thanks!
p.s. If anyone can share their approach to a simple parallax effect like this that would be great too. This is the only approach I've figured that won't rerender everything else on the page onScroll.
const HomePage = () => {
const [loadedPosts, setLoadedPosts] = useState([]);
const { sendRequest } = useHttpClient();
console.log("loadedPosts homePage", loadedPosts);
const bubblesRef = useRef();
useEffect(() => {
if (loadedPosts.length === 0) {
//api call
}
}, [sendRequest, loadedPosts]);
useEffect(() => {
const parallax = () => {
let scrolledValue = window.scrollY / 3.5;
bubblesRef.current.style.transform = `translateY(
-${scrolledValue + "px"}
)`;
console.log("scrolling...", scrolledValue);
};
window.addEventListener("scroll", parallax);
return () => window.removeEventListener("scroll", parallax);
}, []);
return (
<HomePageContainer>
<Header />
<SectionOne posts={loadedPosts} />
<SectionTwo />
<BubbleBlobs className="bubbleBlobs" ref={bubblesRef} />
<BlobTop className="backBlobBottom" preserveAspectRatio="none" />
</HomePageContainer>
);
};
export default HomePage;
You definitely need the cleanup function any time you add a listener to the window, or the handler (and thus the component instance itself) will live on forever. However, since React runs those cleanup hooks asynchronously, it might not happen until after other window events. The value of the ref is set to null when the component unmounts, so you need to check that it is still defined before using the value.
useEffect(() => {
const handler = () => {
if (ref.current) {
// perform update
}
}
window.addEventListener('scroll', handler)
return () => window.removeEventListener('scroll', handler)
}, [])
When you call useEffect, your reference has not been instantiated, so the error message appears, in your useEffect dependency array, insert your ref and before running the code in useEffect, make sure your current reference is defined.
I have a section with a fixed height. I don't know when the component mounts (first renders) whether the content coming in will fit or not. If it does NOT fit, then I need to render a 'Read More' button.
It looks like this:
I wrote this originally as a Class component using the lifecycle methods DidMount/DidUpdate:
Class Component
import React, { createRef } from "react"
import styled from "#emotion/styled"
import Section from "../Section"
import ButtonReadMore from "./ButtonReadMore"
import Paragraphs from "./Paragraphs"
const StyledHeightContainer = styled.div`
max-height: 150px;
overflow: hidden;
`
class ParagraphList extends React.Component {
state = {
overflowActive: false,
}
wrapper = createRef() // so we can get a ref to the height container
isOverflowing(el) {
if (el) return el.offsetHeight < el.scrollHeight
}
componentDidMount() {
this.setState({ overflowActive: this.isOverflowing(this.wrapper.current) })
}
componentDidUpdate() {
if (this.wrapper.current && !this.state.overflowActive) {
this.setState({
overflowActive: this.isOverflowing(this.wrapper.current),
})
}
}
handleClick() {
this.setState({ overflowActive: false })
}
render() {
const { moreButtonText, titleText, paragraphs, theme } = this.props
return (
<>
<Section overflowActive={this.state.overflowActive}>
{this.state.overflowActive || !this.wrapper.current ? (
<StyledHeightContainer ref={this.wrapper}>
<Paragraphs paragraphs={paragraphs} />
</StyledHeightContainer>
) : (
<Paragraphs paragraphs={paragraphs} />
)}
</Section>
{overflowActive ?
<ButtonReadMore
onClicked={handleClick.bind(this)}
moreButtonText={moreButtonText}
theme={theme}
/>
: null}
</>
)
}
}
export default ParagraphList
My best way to explain the flow:
When the component mounts, the flag is false and we have no reference to the div so the StyledHeightContainer will try to render and thus provide a ref to it
In componentDidMount -> try to set the overflow flag (which will be false because at this point we do not yet have rendering completed so the ref will be null). But by setting the flag anyway, we queue an additional render pass
1st INITIAL rendering completes -> we have a ref to the div now
The 2nd (queued) render occurs, firing the componentDidUpdate -> we can calculate the overflow and set the flag to true when the content overflows
When the user clicks the button -> set the flag to false, which will trigger a re-render and hence the StyledHeightContainer will be removed from the DOM.
Functional Component With Hooks
Sandbox of the code
When I re-wrote this as a functional component using Hooks, I ended up with this:
import React, { createRef, useEffect, useState } from "react"
import styled from "#emotion/styled"
import Section from "../Section"
import ButtonReadMore from "./ButtonReadMore"
import Paragraphs from "./Paragraphs"
const StyledHeightContainer = styled.div`
max-height: 150px;
overflow: hidden;
`
const ParagraphList = ({ moreButtonText, titleText, paragraphs, theme }) => {
const [overflowActive, setOverflowActive] = useState(false)
const [userClicked, setUserClicked] = useState(false)
const wrapper = createRef(false) // so we can get a ref to the height container
const isOverflowing = el => {
if (el) return el.offsetHeight < el.scrollHeight
}
useEffect(() => {
if (!userClicked && !overflowActive && wrapper.current) {
setOverflowActive(isOverflowing(wrapper.current))
}
}, [userClicked]) // note: we only care about state change if user clicks 'Read More' button
const handleClick = () => {
setOverflowActive(false)
setUserClicked(true)
}
return (
<>
<Section theme={theme} overflowActive={overflowActive}>
{!userClicked && (overflowActive || !wrapper.current) ? (
<StyledHeightContainer ref={wrapper}>
<Paragraphs paragraphs={paragraphs} />
</StyledHeightContainer>
) : (
<Paragraphs paragraphs={paragraphs} />
)}
</Section>
{overflowActive ?
<ButtonReadMore
onClicked={handleClick.bind(null)}
moreButtonText={moreButtonText}
theme={theme}
/>
: null}
</>
)
}
export default ParagraphList
I was surprised that I needed to add another state (userClicked), which is how I force the 2nd render to occur (ie. the equivalent to the componentDidUpdate in the class solution).
Is this correct or can someone see a more concise way to write the 2nd solution?
NOTE
One of the reasons I ask is because in the console I get this warning:
48:6 warning React Hook useEffect has missing dependencies:
'overflowActive' and 'wrapper'. Either include them or remove the
dependency array react-hooks/exhaustive-deps
and I don't THINK I want to add them to the dependency array, as I don't want to trigger rendering when they change...?
I really enjoyed while solving the query.
Here is the implementation: https://codesandbox.io/s/react-using-hooks-in-section-component-5gibi?file=/src/ParagraphList.js
First of all, I was thinking of
useEffect(() => {
setOverflowActive(isOverflowing(wrapper.current));
}, [wrapper]);
But if we do this, it will again call the useEffect as when we'll click on the Read more button. Because it was comparing the reference of the wrapper and not it's value.
So, to avoid the reference comparison we have to use the useCallback hook.
const isOverflowingNode = node => {
return node.offsetHeight < node.scrollHeight;
};
const wrapper = useCallback(node => {
if (node !== null) {
setOverflowActive(isOverflowingNode(node));
}
}, []);
I came across the beautiful discussion: https://github.com/facebook/react/issues/14387
For more information:
https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
Thanks for the question :)
You could add an extra useEffect(() => (...),[]) that acts like componentDidMount(). And another useEffect(() => (...)) that acts like componentDidUpdate(). Then you should be able to get rid of userClicked.
This is a good link on how the lifestyle methods work with hooks. https://dev.to/trentyang/replace-lifecycle-with-hooks-in-react-3d4n
useEffect(() => {
setOverflowActive(isOverflowing(wrapper.current));
}, []);
useEffect(() => {
if (!overflowActive && wrapper.current) {
setOverflowActive(isOverflowing(wrapper.current))
}
});
The second one might need to be useLayoutEffect if you are wanting the update to happen after the layout.
I am attempting to add and remove an event listener within a functional React component. The listener is added fine but is not removed when asked to be. I believe the issue is that the function I am referencing handlemousemove is recreated every component render and so when removeEventListener attempts to remove it, it's not the same function reference as when addEventListener added it.
I tried moving handlemousemove out of the component but it required access to the setState hooks generated in the component.
const handleMouseMove = e => {
setYOffset(e.clientY-280)
setXOffset(e.clientX-350)
}
const followMouse = () => {
if (isFollowingMouse){
setIsFollowingMouse(false)
document.removeEventListener("mousemove", handleMouseMove)
} else {
setIsFollowingMouse(true)
document.addEventListener("mousemove", handleMouseMove)
}
}
...
<button name="mouse" onClick={followMouse}>
Follow Mouse
</button>
All branches of execution are hit here but document.removeEventListener("mousemove", handleMouseMove) doesn't actually remove the event listener.
Is there a way to have a "static method" within a functional component? Is that even the issue here?
Here's a link to code sandbox with the whole code: https://codesandbox.io/s/pzrwh
The old way to do it was with render props, but now that hooks have arrived this is a better solution
const MyComponent = (props) => {
const [isFollowingMouse, setIsFollowingMouse] = React.useState(false);
const [xOffset, setXOffset] = React.useState(0);
const [yOffset, setYOffset] = React.useState(0);
const handleMouseMove = e => {
if (isFollowingMouse) {
setYOffset(e.clientY-28);
setXOffset(e.clientX-35);
}
};
const followMouse = () => {
setIsFollowingMouse(!isFollowingMouse);
}
const styles = {
'cat': {
'backgroundColor': 'red',
'height': '20px',
'position': 'absolute',
'left': xOffset,
'top': yOffset,
'width': '20px',
'display': isFollowingMouse ? 'block' : 'none'
}
};
return (
<div style={{ 'height': '100%' }} onMouseMove={handleMouseMove}>
<div style={ styles.cat }>C</div>
<button name="mouse" onClick={followMouse}>
Follow Mouse
</button>
</div>
)
}
ReactDOM.render(<MyComponent />, document.getElementById('root'));
html,
body,
#root {
height: 100%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I think your description of the issue is spot on. A quick fix is to define the variable handleMouseMove outside of your App function - essentially making the variable static and not recreated every render.
Then, within the body of the function, only assign the handleMouseMove variable if it's currently unassigned, and set it back to null when you set isFollowingMouse to false.
With React 16.7 you can use Hooks to do this:
import React, { useCallback, useEffect, useState } from 'react';
const DraggedComponent = React.memo(
props => {
const [isFollowingMouse, setIsFollowingMouse] = useState(false);
const [xOffset, setXOffset] = useState(0);
const [yOffset, setYOffset] = useState(0);
const handleMouseMove = useCallback(
e => {
if (isFollowingMouse) {
setYOffset(e.clientY-28);
setXOffset(e.clientX-35);
}
}, [isFollowingMouse, setYOffset, setXOffset]
);
useEffect(
() => {
document.addEventListener('mousemove', handleMouseMove);
return () => document.removeEventListener('mousemove', handleMouseMove);
},
[handleKeyDown]
);
const followMouse = () => setIsFollowingMouse(!isFollowingMouse);
return (
<div onMouseMove={handleMouseMove}>
<div>C</div>
<button name="mouse" onClick={followMouse}>
Follow Mouse
</button>
</div>
)
}
);
ReactDOM.render(<DraggedComponent />, document.getElementById('root'));
In this example React.memo() ensures that the component is only redrawn if state or properties change. Similar useCallback() will cache the event listener for the mousemove event, such that this will not be recreated only if isFollowingMouse, setYOffset or setXOffset change, instead of every rerender. useEffect will be called once the component is created, and once every time the handleMouseMove callback changes. Furthermore it returns a function, which is automatically called if the component is destroyed or the parameter handleKeyDown changes.
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>
</>
)
}