How to detect hidden component in React - javascript

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

Related

Modal component with fade-in overlay and dialog disabled until animation ends

I'm trying to rewrite one of my JS plugins to react, as a way of learning.
I have a panel that when hidden/shown needs to be updated with several classnames as well as some that need to wait for a css animation to complete (why the timer).
How should I do this in a react way? Using querySelector to change classnames seem very wrong..?
Detailed explanation
When showPanel is triggered the following need to happen
the body/html element need updated css (hence me adding classes)
an existing overlay fades in (adding a class to that)
the modal div is displayed (adding a class for that)
the modal div is told to be active AFTER the animation has been run (hence the timer and class "am-animation-done")
What I preferably would like to have/learn is best practice to do this in reactjs. I'm thinking a toggle state that when triggered sets the state to visible/hidden and if set to "visible" the class changes below happens. My biggest issue is the timer thing.
showPanel = () => {
document.querySelector('body').classList.add('am-modal-locked');
document.querySelector('html').classList.add('am-modal-locked');
document.querySelector('.am-overlay').classList.add('fadein');
document.querySelector('.am-modal').classList.add('am-show');
const timer = setTimeout(() => {
document.querySelector('.am-modal').classList.add('am-animation-done');
}, 500);
return () => clearTimeout(timer);
};
hidePanel = () => {
document.querySelector('.am-modal').classList.remove('am-show');
document.querySelector('.am-modal').classList.remove('am-animation-done');
document.querySelector('.am-overlay').classList.add('fadeout');
const timer = setTimeout(() => {
document.querySelector('.am-overlay').classList.remove('fadein');
document.querySelector('.am-overlay').classList.remove('fadeout');
document.querySelector('body').classList.remove('am-modal-locked');
document.querySelector('html').classList.remove('am-modal-locked');
}, 500);
return () => clearTimeout(timer);
};
Source code updated for clarifaction
This is a lot simpler in React, here's an example with hooks
function Panel() {
const [hidden, setHidden] = useState(false);
const toggleCallback = useCallback(() => setHidden(hidden => !hidden), []);
const cls = hidden ? 'hide' : 'show';
return (
<div className={cls}>
<button onClick={toggleCallback}>Toggle</>
</div>
)
}
You can use the state to dinamically change classnames inside your component
className={this.state.isPanelVisible}
And maybe instead of setting it as boolean you can set your variable to the class you need at the moment.
React working with virtual DOM so you should play with state and change class of that particular element like below example:
constructor(props) {
super(props);
this.state = {'active': false, 'class': 'album'};
}
handleClick(id) {
if(this.state.active){
this.setState({'active': false,'class': 'album'})
}else{
this.setState({'active': true,'class': 'active'})
}
}
<div className={this.state.class} onClick={this.handleClick.bind(this.data.id}>
<p>Data</p>
</div>
In very basic use cases you can write the logic inside of the class itself.
<div className={active ? "active" : "disabled"} />
In more advanced cases I would suggest to use something like classnames package.
https://www.npmjs.com/package/classnames
<div className={classNames({ foo: true, bar: true, boo: false })} />
Which would result in div having class foo and bar
This is mainly regarding one component, but if you really have to affect class of something so far away as body would be, than you are most likely gonna need useQuerySelector or put the state somewhere high and then base the logic on it.
Yes that's not a very good way to do it. Instead you should use state variables to toggle your classes as well. There is no need to manually manipulate DOM. The you can set up your timeout inside the callback of your first setState to change state again.
Maybe something like this:
class Todo extends Component {
constructor(props) {
super(props);
this.state = {
class1: 'on',
class2: 'off'
}
}
toggle = () => {
this.setState({class1: 'off'}, () => {
setTimeout(() => {
this.setState({class2: 'on'})
}, 2000)
})
}
render() {
const {class1, class2} = this.state;
return (
<div>
<h1 className={`${class1} ${class2}`} onClick={this.toggle}>Class toggle</h1>
</div>
)
}
}
use this approach for change styling on state change
<div className={`rest_of_classes ${isClassChange ? 'change_class_name': ''}`} />

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)

Get rendered element height before it shows up on screen (ReactJS)

I have an animated component that slides up/down depending on the prop (true or false). I'm using maxHeight: 0 to hide the component (transition is being done with CSS) and that's the default state that's being passed as prop. For the opened style I use a maxHeight much bigger than needed just to make sure the content will fit properly. After it's opened I'm able to get its height by ref and set the maxHeight accordingly.
export default class AnimatedInput extends React.Component {
constructor (props) {
super(props);
this.state = {
height: 600
}
}
componentDidUpdate(prevProps) {
var height = this.refs.inputNode ? this.refs.inputNode.clientHeight : height;
console.log(height);
if (this.props.open === false && prevProps.open === true) {
this.setState({height: height});
}
}
render () {
var {height} = this.state;
let test = this.props.open ? 'boxVisible' : 'boxHidden';
var styles = {
boxHidden: {
...
maxHeight: 0,
},
boxVisible: {
....
maxHeight: height,
}
}
return (
<div style={styles[test]} ref="inputNode">
{this.props.children}
</div>
)
}
}
There are 2 problems with this approach:
The first time it's opened and closed is not smooth due to maxHeight being larger than it should (maybe render the opened one off-screen and get its height first?)
If it's closed before fully opened the height will be lower than it should (I suppose it's an easy fix - just need to stop updating the height value).
Am I on the right track? How would you fix these? Should I stick to CSS or maybe make the transition entirely in JS. Thanks for your suggestions!
You're looking for ReactCSSTransitionGroup. I used this for the exact same thing you are.

Update Redux prop/state following div onclick

I have a table - let's call it table 1. When clicking on a row in table 1 another table is being displayed, let's call this one table 2. Table 2 displays data relevant to the clicked row in table 1. Sometimes a vertical scroll needs to be displayed in table 2 and sometimes not -depends on the number of rows.Need to solve: there is an unwanted transition of the border when the scroll is not being displayed:
. The idea for the solution: "change margin-right" according to conditions which show whether the scroll exits or not.Save the result of this condition into Redux prop:
element.scrollHeight > element.clientHeight || element.scrollWidth >
element.clientWidth
The problem: Trying to update the display/non-display of the scroll into redux prop from different React events such as componentDidMount, componentWillReceiveProps,CopmponentDidUpdate (set state causes infinte loop here) and from the click event.Tried to use forceUpdate() after setting props into Redux as well.
When console.log into the console in chrome (F12), the only result which is correlated correctly to the display/non display of the scrollbar is coming from within the componentDidUpdate and it doesn't reflect in the redux prop (isoverflown function returns true, redux this.props.scrollStatus and this.state.scrollStatus are false). Also don't like the usage of document.getElementById for the div which contains the rows, because it breaks the manipulation of the dom from within the props and state,but didn't find a different solution for now.
The F12 console when display the scroll bar:
The F12 console when no scroll bar is displayed:
.
The rest of the code:
1) action:
export function setScrollStatus(scrollStatus) {
return {
type: 'SET_SCROLL_STATUS',
scrollStatus: scrollStatus
};
}
2) reducer:
export function scrollStatus(state = [], action) {
switch (action.type) {
case 'SET_SCROLL_STATUS':
return action.scrollStatus;
default:
return state;
}
}
3)Page.js (please click on the picture to see the code)
import {setScrollStatus} from '../actions/relevantfilename';
function isOverflown(element) {
return element.scrollHeight > element.clientHeight ||element.scrollWidth > element.clientWidth;
}
class SportPage extends React.Component {
constructor(props) {
super(props);
this.state = initialState(props);
this.state = {
scrolled:false,
scrollStatus:false};
componentDidUpdate() {
console.log( "1 isoverflown bfr redux-this.props.setScrollStatus inside componentDidUpdate",isOverflown(document.getElementById('content')));
//redux props
this.props.setScrollStatus( isOverflown(document.getElementById('content')));
console.log( "2 isoverflown aftr redux-this.props.setScrollStatus inside componentDidUpdate",isOverflown(document.getElementById('content')));
//redux props
this.props.scrollStatus ? console.log (" 3 this.props.scrollStatus true inside componentDidUpdate") : console.log("this.props.scrollStatus false inside componentDidUpdate");
console.log ("4 state scrollstatus inside componentDidUpdate" , this.state.scrollStatus)
}
componentDidMount(){
console.log( "3 isoverflown bfr set",isOverflown(document.getElementById('content')));
this.props.setScrollStatus("set inside didMount", isOverflown(document.getElementById('content')));
console.log( "4 isoverflown aftr set didMount",isOverflown(document.getElementById('content')));
this.props.scrollStatus ? console.log ("scrollStatus true") : console.log("scrollStatus false");
console.log ("state scrollstatus inside didMount" , this.state.scrollStatus)
}
render() {
<div style={{overflowY:'scroll',overflowX:'hidden',height:'50vh',border:'none'}}>
{
this.props.rowData.map((row,index )=>
<div style={{ display: 'flex',flexWrap: 'wrap', border:'1px solid black'}}
onClick={ e => { this.setState({ selected: index, detailsDivVisible: true,scrolled:isOverflown(document.getElementById('content')),
scrollStatus:isOverflown(document.getElementById('content')) },
this.props.setScrollStatus( isOverflown(document.getElementById('content'))),this.forceUpdate(),console.log ("onclick this.state.scrollStatus", this.state.scrollStatus),
console.log ("onclick pure funtion", isOverflown(document.getElementById('content'))));
const mapDispatchToProps = (dispatch) => {
return {
setScrollStatus: function (scrollStaus) {dispatch (setScrollStatus(scrollStaus))},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Page);
Thank you for your reply. However,solved it in different way which does not involve the life cycle/events:
1) Calculate the height of the scroll by- multiple the height of single row by number of items to be displayed (arr.length, the arr comes from JSON)
2) setting the max height of the scroll to a needed value
3) setting the max height of the content to be the calculated height:
The result is a scroll that displays all the time with the correct height. This solved the indentation problem.
<div style={{overflowY:'auto', marginRight: '18px',zIndex:'1000',borderBottom:'1px solid black',borderRight:'1px solid black', height: this.props.rowData[this.state.selected].rowItemsList.length * singleRowHeight + 'px', maxHeight:'100px' }}>
<div style={{ width:'inherit', maxHeight:this.props.this.props.rowData[this.state.selected]‌​.rowItemsList.length * singleRowHeight + 'px' }}>
Lets simplify this. All you need is to dispatch reducer each time some one clicks inside a div.Please find the code snippet useful please go through the comments.
//import store from "./store/directory" - update this to ur store
let DOMObject = document.getElementById("id1"); //dom reference i did it based on ID its up to u to refer how u like it
//call back happens each time onclick event is triggered
DOMObject.onclick = ()=> {
/* store.dispatch(
{
type:"reducer to invoke",
data:"the data to update on click"
}
);
*/
//uncomment above and update to your requirement
console.log("clicked - Please update the dispatch event to you requirement");
}
#id1 {
padding :100px 150px 100px 80px;
background-color: lightblue;
}
<div id="id1">
DIV AREA - clcik
</div>

React updating DOM in an unpredictable manner with setState and class names

I'm attempting to do an animation with React and CSS classes. I have created a live demo, if you visit it and click the Start button you will see the text fade in and up one by one. This is the desired animation that I am after.
However, there seems to be issues of consistency when you hit Start multiple times and I cannot pinpoint why.
The Issue: Below is a recording of the issue, you can see the number 1 is not behaving as expected.
live demo
The process: Clicking Start will cancel any previous requestAnimationFrame' and will reset the state to it's initial form. It then calls the showSegments() function with a clean state that has no classNames attached to it.
This function then maps through the state adding a isActive to each segment in the state. We then render out the dom with a map and apply the new state.
This should create a smooth segmented animation as each class gets dropped one by one. However when i test this in Chrome (Version 56.0.2924.87 (64-bit)) and also on iOS, it is very inconsistent, sometimes it works perfectly, other times the first DOM element won't animate, it will just stay in up and visible it's completed transitioned state with "isActive".
I tried to replicate this issue in safari but it worked perfectly fine, I'm quite new to react so i am not sure if this is the best way to go about things, hopefully someone can offer some insight as to why this is behaving quite erratic!
/* MotionText.js */
import React, { Component } from 'react';
import shortid from 'shortid';
class MotionText extends Component {
constructor(props) {
super(props);
this.showSegments = this.showSegments.bind(this);
this.handleClickStart = this.handleClickStart.bind(this);
this.handleClickStop = this.handleClickStop.bind(this);
this.initialState = () => { return {
curIndex: 0,
textSegments: [
...'123456789123456789123456789123456789'
].map(segment => ({
segment,
id: shortid.generate(),
className: null
}))
}};
this.state = this.initialState();
}
handleClickStop() {
cancelAnimationFrame(this.rafId);
}
handleClickStart(){
cancelAnimationFrame(this.rafId);
this.setState(this.initialState(), () => {
this.rafId = requestAnimationFrame(this.showSegments);
});
}
showSegments() {
this.rafId = requestAnimationFrame(this.showSegments);
const newState = Object.assign({}, this.state);
newState.textSegments[this.state.curIndex].className = 'isActive';
this.setState(
{
...newState,
curIndex: this.state.curIndex + 1
},
() => {
if (this.state.curIndex >= this.state.textSegments.length) {
cancelAnimationFrame(this.rafId);
}
}
);
}
render(){
const innerTree = this.state.textSegments.map((obj, key) => (
<span key={obj.id} className={obj.className}>{obj.segment}</span>
));
return (
<div>
<button onClick={this.handleClickStart}>Start</button>
<button onClick={this.handleClickStop}>Stop</button>
<hr />
<div className="MotionText">{innerTree}..</div>
</div>
)
}
}
export default MotionText;
Thank you for your time, If there any questions please ask
WebpackBin Demo
Changing the method to something like this works
render(){
let d = new Date();
const innerTree = this.state.textSegments.map((obj, key) => (
<span key={d.getMilliseconds() + obj.id} className={obj.className}>{obj.segment}</span>
));
return (
<div>
<button onClick={this.handleClickStart}>Start</button>
<button onClick={this.handleClickStop}>Stop</button>
<hr />
<div className="MotionText">{innerTree}..</div>
</div>
)
}
How this helps is that, the key becomes different than previously assigned key to first span being rendered. Any way by which you can make the key different than previous will help you have this animation. Otherwise React will not render it again and hence you will never see this in animation.

Categories