What's the equivalent of document.querySelectorAll('.classname')
in React? I understand I should use Refs, but how dow I observe multiple Refs onScroll
?
I usually use a function like the one below to check the viewport position of multiple elements in the page, and trigger different css animation when each element enters the viewport:
HTML
<ul>
<li data-position="below-viewport"></li>
<li data-position="below-viewport"></li>
<li data-position="below-viewport"></li>
<li data-position="below-viewport"></li>
</ul>
Javascript
getPosition: function (element) {
const rect = element.getBoundingClientRect();
if ((rect.top > -1) && (rect.top < (window.innerHeight * 0.75))) {
element.setAttribute('data-js-position','in-viewport');
} else if ((rect.top > 0) && (rect.top < window.innerHeight)) {
element.setAttribute('data-js-position','entering-viewport');
} else if (rect.top > window.innerHeight) {
element.setAttribute('data-js-position','below-viewport');
} else if (rect.bottom < 0) {
element.setAttribute('data-js-position','above-viewport');
}
}
window.addEventListener('scroll', function() {
[].forEach.call(document.querySelectorAll('[data-js-position]'), el => {
positionChecker.getPosition(el);
})
});
How would I implement something similar in React? Can you give me an example of a function that observes multiple divs in React?
Even better: how can I abstract this function in App.js
, so that I can use it also in child Components?
What's the equivalent of document.querySelectorAll('.classname')
in React? I understand I should use Refs, but how dow I observe multiple Refs onScroll
?
I usually use a function like the one below to check the viewport position of multiple elements in the page, and trigger different css animation when each element enters the viewport:
HTML
<ul>
<li data-position="below-viewport"></li>
<li data-position="below-viewport"></li>
<li data-position="below-viewport"></li>
<li data-position="below-viewport"></li>
</ul>
Javascript
getPosition: function (element) {
const rect = element.getBoundingClientRect();
if ((rect.top > -1) && (rect.top < (window.innerHeight * 0.75))) {
element.setAttribute('data-js-position','in-viewport');
} else if ((rect.top > 0) && (rect.top < window.innerHeight)) {
element.setAttribute('data-js-position','entering-viewport');
} else if (rect.top > window.innerHeight) {
element.setAttribute('data-js-position','below-viewport');
} else if (rect.bottom < 0) {
element.setAttribute('data-js-position','above-viewport');
}
}
window.addEventListener('scroll', function() {
[].forEach.call(document.querySelectorAll('[data-js-position]'), el => {
positionChecker.getPosition(el);
})
});
How would I implement something similar in React? Can you give me an example of a function that observes multiple divs in React?
Even better: how can I abstract this function in App.js
, so that I can use it also in child Components?
4 Answers
Reset to default 5Make each li
html element its own ponent and hold a ref
reference to it in its own state.
class LiElement extends React.Component {
ponentDidMount() {
this.ref.getPosition()
}
render() {
return (
<li ref={(ctx) => this.ref = ctx}>
</li>
)
}
}
Iterating through refs might get you something similar to what you're trying to achieve.
In this example I'm storing every node in a local Map so you can iterate through them and get their getBoundingClientRect.
Note, that there are several ways of doing this, you don't have to create a Map, you can just get each element's "ref", but you would have to assign a different "ref" value to each "li".
Also, it would be a good idea to throttle the handleScroll call..
class App extends React.Component {
constructor(props) {
super(props);
this._nodes = new Map();
}
ponentDidMount() {
window.addEventListener("scroll", this.handleScroll);
}
handleScroll = e => {
Array.from(this._nodes.values())
// make sure it exists
.filter(node => node != null)
.forEach(node => {
this.getPosition(node);
});
};
getPosition = node => {
const rect = node.getBoundingClientRect();
// do something cool here
console.log("rect:", rect);
};
ponentWillUnmount() {
window.removeEventListener("scroll", this.handleScroll);
}
render() {
return (
<div>
<ul>
<li key="1" ref={c => this._nodes.set(1, c)}>
Your thing 1
</li>
<li key="2" ref={c => this._nodes.set(2, c)}>
Your thing 2
</li>
<li key="3" ref={c => this._nodes.set(3, c)}>
Your thing 3
</li>
</ul>
</div>
);
}
}
Adding this other possible solution: creating a new react ponent VisibilitySensor
, so to use only one ref and only one function
import React, { Component } from 'react';
class VisibilitySensor extends Component {
ponentDidMount() {
window.addEventListener('scroll', this.getElementPosition = this.getElementPosition.bind(this));
this.getElementPosition();
}
ponentWillUnmount() {
window.removeEventListener('scroll', this.getElementPosition);
}
getElementPosition() {
const element = this.visibilitySensor;
var rect = element.getBoundingClientRect();
if ((rect.top > 0) && (rect.top < (window.innerHeight * 0.75))) {
element.setAttribute("data-position","in-viewport");
} else if (rect.top > window.innerHeight) {
element.setAttribute("data-position","below-viewport");
} else if (rect.bottom < 0) {
element.setAttribute("data-position","above-viewport");
}
}
render() {
return (
<div data-position="below-viewport" ref={(element) => { this.visibilitySensor = element; }}>
{this.props.children}
</div>
);
}
}
export default VisibilitySensor;
Then wrapping every li
(or div
) I need to watch with the above ponent. I personally ended up disliking the solution above, because the new added div
would mess up with my css styling, particularly width of child related to width of parent, etc..
<ul>
<VisibilitySensor>
<li></li>
</VisibilitySensor>
<VisibilitySensor>
<li></li>
</VisibilitySensor>
<VisibilitySensor>
<li></li>
</VisibilitySensor>
<VisibilitySensor>
<li></li>
</VisibilitySensor>
</ul>
I was able to work around this with the useRef hook by adding a ref on the parent element that holds all the children you wish to scroll to.
So in your case, you could do:
TOP OF YOUR REACT CODE
const parentScrollRef = React.useRef();
HTML
<ul ref={parentScrollRef}>
<li data-position="below-viewport"></li>
<li data-position="below-viewport"></li>
<li data-position="below-viewport"></li>
<li data-position="below-viewport"></li>
</ul>
Then you'd use call the onQuerySelectorAll on the ref. With react v16, you will be able to call the querySelectorAll on the ref.current object. So the code should look like this:
getPosition: function (element) {
const rect = element.getBoundingClientRect();
if ((rect.top > -1) && (rect.top < (window.innerHeight * 0.75))) {
element.setAttribute('data-js-position','in-viewport');
} else if ((rect.top > 0) && (rect.top < window.innerHeight)) {
element.setAttribute('data-js-position','entering-viewport');
} else if (rect.top > window.innerHeight) {
element.setAttribute('data-js-position','below-viewport');
} else if (rect.bottom < 0) {
element.setAttribute('data-js-position','above-viewport');
}
}
// You would then target the ref, no need to hit the whole window.
parentScrollRef.current.addEventListener('scroll', function() {
const dataPositions = parentScrollRef.current.querySelectorAll(".data-js-position");
dataPositions.forEach((position) => {
positionChecker.getPosition(el);
}
});
//Also remember your clean up xD
return (
parentScrollRef.current.removeEventListener('scroll', function() {
// cleanup function
});
)