Intersection Observer API not working with hidden elements - javascript

I am trying to observe a hidden element. I have an element which has style set to display: none. I want when my element intersect it will perform my action i.e: Play the video. I am sharing my sample code below
var options = {threshold: 0.5 }
var circle = document.getElementById('content_video');
var observer = new IntersectionObserver(entries => {
var [{ isIntersecting }] = entries
if (isIntersecting) {
player.play();
player.ima.getAdsManager().resume();
} else {
player.ima.getAdsManager().pause();
}
}, options);
window.addEventListener('load', () => {
if ('IntersectionObserver' in window) observer.observe(circle);
}, false);

This is normal behaviour, because element with display:none cannot be reached and ignoring by browser
Try set other styles instead display:none. Example, use opacity or width and height 0 with overflow: hidden

Unfortunately if your goal is to use the intersection observer to lazy load the media, the answer will not fulfil the purpose since the media will load before it intersects with the viewport. The solution is to make sure it is the containing element of the media that is being observed, instead the element on which display none is being applied.
https://codesandbox.io/s/react-lazyload-forked-yz93f?file=/src/LazyLoad.js:0-958
import React, { Component, createRef } from "react";
import "./styles.css";
class LazyLoad extends Component {
observer = null;
rootRef = createRef();
state = {
intersected: false
};
componentDidMount() {
this.observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (entry.isIntersecting) {
this.setState({ intersected: true });
this.observer.disconnect();
}
},
{ root: null, threshold: 0.2 }
);
this.observer.observe(this.rootRef.current);
console.log(this.rootRef);
}
render() {
return (
<div className="outer" ref={this.rootRef}>
<span>{JSON.stringify(this.state.intersected)}</span>
<div
className={`container ${
this.state.intersected ? "d-block" : "d-none"
}`}
>
{this.props.children}
</div>
</div>
);
}
}
export default LazyLoad;

Related

How to set Intersection Observer for a specific Svelte component/element for whole App?

Lets assume I have Icon.svelte component in /elements folder.
I have used that Icon component in various other components in whole application.
Is there any way to set intersection observer for that Icon component?
So when that component comes in Viewport it mount and on outside of Viewport it destroys!
Basically thinking this approach for performance boost up of application.
First of all, you should measure whether any action is even necessary.
If so, you could try simply creating an observer in each component, and only if that is too expensive extract the observer instance.
To do that you can create the observer at the top level of your application and set a context that passes the observer on to all descendants. Then in the icon component you can get the context and call observe in onMount and unobserve in onDestroy. You would want to observe some container element in the component.
You may have to add something like an EventTarget to the context so the icons have something to get events from. In the observer callback a new event can then be dispatched to that to notify the components. The arguments from the callbacks have to be passed on, so the icon can check whether it was among the intersected elements. (Event subscription and unsubscription should be done in onMount/onDestroy.)
Example:
// In root
const context = setContext('intersection-observer', writable(null));
onMount(() => {
const event = new EventTarget();
const observer = new IntersectionObserver(
entries => event.dispatchEvent(new CustomEvent('intersect', { detail: entries })),
{ root: null, rootMargin: '0px', threshold: 0 },
);
$context = {
observe: element => observer.observe(element),
unobserve: element => observer.unobserve(element),
onIntersect: event,
};
return () => observer.disconnect();
});
<!-- Component that uses the observer -->
<script>
import { getContext } from 'svelte';
import { onDestroy } from 'svelte';
const intersectionContext = getContext('intersection-observer');
const cleanup = [];
let root;
let visible = false;
onDestroy(() => cleanup.forEach(fn => fn()));
$: if ($intersectionContext && root) {
$intersectionContext.observe(root);
cleanup.push(() => $intersectionContext.unobserve(root));
$intersectionContext.onIntersect.addEventListener('intersect', onIntersect);
cleanup.push(() =>
$intersectionContext.onIntersect
.removeEventListener('intersect', onIntersect)
);
}
function onIntersect(e) {
const entries = e.detail;
const entry = entries.find(entry => entry.target === root);
if (entry)
visible = entry.isIntersecting;
}
</script>
<div bind:this={root}>
<!-- Render expensive content here using {#if visible} -->
{visible ? 'Visible' : 'Invisible'}
</div>
REPL
(This is a bit more complicated than what I described because this is designed to support SSR. To support SSR, APIs like IntersectionObserver cannot be used outside of onMount.)
If you do not mind a bit more convention-based magic, you can just tag elements in some way, e.g. using a data attribute and then send events straight to those elements.
// In root
onMount(() => {
const intersectionObserver = new IntersectionObserver(
entries => entries.forEach(entry =>
entry.target.dispatchEvent(
new CustomEvent('intersect', { detail: entry })
)
),
{ root: null, rootMargin: '0px', threshold: 0 },
);
const mutationObserver = new MutationObserver(mutations =>
mutations.forEach(m => {
m.addedNodes.forEach(node => {
if (node instanceof HTMLElement &&
node.dataset.intersect != null &&
node.dataset.intersectInitialized == null) {
intersectionObserver.observe(node);
node.dataset.intersectInitialized = 'true';
}
});
m.removedNodes.forEach(node => {
if (node instanceof HTMLElement) {
intersectionObserver.unobserve(node);
}
});
})
);
[...document.querySelectorAll('[data-intersect]')].forEach(node => {
intersectionObserver.observe(node);
node.dataset.intersectInitialized = 'true';
});
mutationObserver.observe(document.body, { childList: true, subtree: true });
return () => {
mutationObserver.disconnect();
intersectionObserver.disconnect();
};
});
<script>
let visible = false;
</script>
<div data-intersect
on:intersect={e => visible = e.detail.isIntersecting}>
<!-- Render expensive content here using {#if visible} -->
{visible ? 'Visible' : 'Invisible'}
</div>
REPL

How to prevent React from rendering the entire array when I append more items

I have an array of 30 unique options that I use to render 30 SVGs.
There is a button that adds 30 more unique options to the array each time it's clicked. When the 30 new options are added, React will re-render all 60 SVGs instead of just rendering the 30 new ones.
This is a problem because it's causing the frame rate to drop and makes the browser stutter. The problem gets worse and worse as there are more SVGs in the array.
I want to know how I can get React to only render the newly added 30 SVGs and not re-render everything in the array.
I even added a unique key to each SVG.
Here's my code:
import generateRandomSVG from "../randomGenerator";
import SVGElement from "./SVGElement";
import Button from "./button";
const getSVGs = (num) => {
let avatarArray = [];
for (let i = 0; i < num; i++) {
avatarArray.push(generateRandomSVG());
}
return avatarArray;
};
class Home extends React.Component {
state = {
svgs: []
};
componentDidMount() {
//render 30 SVGs when the component first loads
this.setState({
svgs: getSVGs(30)
});
console.log(this.state.avatars);
}
handleClick = () => {
//when user clicks on button, add 30 more SVGs
const moreSVGs = getSVGs(30);
this.setState({
svgs: this.state.svgs.concat(moreSVGs)
});
};
render() {
return (
<div>
<Button onClick={this.handleClick}>Add 30 more SVGs </Button>
{this.state.svgs.map((SVGObject) => {
const key = SVGObject.uniqueKey;
return (
<div key={key}>
<SVGElement {...SVGObject.options} />
</div>
);
})}
</div>
);
}
}
Instead of holding all your svgs in one array, make svgs array hold arrays where each array holds 30 svgs.
componentDidMount() {
this.setState({
svgs: svgs.push(getSVGs(30))
});
}
handleClick = () => {
const moreSVGs = getSVGs(30);
const updatedSVGs = this.state.svgs.push(moreSVGs)
this.setState({
svgs: updatedSVGs
});
};
render() {
return (
<div>
<Button onClick={this.handleClick}>Add 30 more SVGs </Button>
{this.state.svgs.forEach((svgBatch) => {
svgBatch.map((SVGObject) => {
const key = SVGObject.uniqueKey;
return (
<div key={key}>
<SVGElement {...SVGObject.options} />
</div>
);
})}
})
</div>
);
}
This way you iterate over a batch of 30 svgs and create a compoment for each batch.
After this you can create a check in the shouldComponentUpdate method in your SVGElement to check if the new props differ from the currentones. That's how you prevent the rerendering.
shouldComponentUpdate(nextProps, nextState) {
//I don't know what your SVGObject.options contain so you will have to change
//the check a liitle but I think you get the idea
if (JSON.stringify(this.props.options.svgArray) === JSON.stringify(nextProps.options.svgArray)) {
return false;
}
}

React-trancition-group. Transition does not work after migration from .v1 to .v2

I'm trying to migrate my app from old one to the new React-trancition-group API, but it's not so easy in case of using the manual <Transition> mode for transition creation of the particular React component.
My animation logic is:
we have an array of the components, each child of him comes one by one in the <TransitionGroup> API by onClick action. Where every new income component smoothly replace and hide previous one, which is already present in <Transition> API.
I almost finish unleash this tangle in react-trancition-group .v2, but one thing is still not solved - the component, that already been in <Transition> API does not disappear after the new one is overlayed him, which was automatically happen in react-trancition-group .v1 instead. So now they all just stack together...
So, maybe you can look on my code and luckly say where is my problem located...
I'll be grateful for any help. Thanks for your time
My code:
import React, { Component } from 'react'
import { findDOMNode } from 'react-dom'
import { TweenMax, Power1 } from 'gsap'
import { Transition } from 'react-transition-group'
class Slider extends Component {
_onEntering = () => {
const { id, getStateResult, isCurrentRow, removeAfterBorder, calcHeight } = this.props
const el = findDOMNode(this)
const height = calcHeight(el)
TweenMax.set(el.parentNode, {
height: `${height}px`
})
TweenMax.fromTo(
el,
0.5,
{
y: -120,
position: 'absolute',
width: `${100}%`,
zIndex: 0 + id
},
{
y: 0,
width: `${100}%`,
zIndex: 0 + id,
ease: Power1.easeIn
}
)
}
_onEntered = () => {
const { activeButton, removeAfterBorder, getCurrentOutcome } = this.props
findDOMNode(this)
}
_onExiting = () => {
const el = findDOMNode(this)
TweenMax.to(el, 2, {
onComplete: () => {
el.className = ''
}
})
}
_onExited = () => {
const { getStateResult } = this.props
getStateResult(true)
}
render() {
const { children } = this.props
return (
<Transition
in={true}
key={id}
timeout={2000}
onEntering={() => this._onEntering()}
onEntered={() => this._onEntered()}
onExiting={() => this._onExiting()}
onExited={() => this._onExited()}
unmountOnExit
>
{children}
</Transition> || null
)
}
}
export default Slider
```
So, you problem is very typically. As you written here: the component, that already been in <Transition> API does not disappear after the new one is overlayed him - it's happen because you does not changing the status flag in for Transition Component. You set always true for him, it's not a right.
By the way, you need to understand what is you trying to do in your code. There is a big difference between methods <Transition></Transition> and <TransitionGroup><CSSTransition></CSSTransition><TransitionGroup>. You need to use pure <Transition></Transition> API only for very rare cases, when you need explicitly manipulate animation scenes.
As I see in you code you trying to replace one component by the other and in this case you need to use the second method that I have provided above.
So, try this:
import { TransitionGroup, CSSTransition } from 'react-transition-group'
...some code
return (
<TransitionGroup>
<CSSTransition
key={id}
timeout={2000}
onEntering={() => this._onEntering()}
onEntered={() => this._onEntered()}
onExiting={() => this._onExiting()}
onExited={() => this._onExited()}
unmountOnExit
>
{children}
</CSSTransition> || null
</TransitionGroup>
)
It should help you and start to render Compenents by normal overlaying each other.

Add and remove css class only during scroll in React?

I am working on a mockup blog for my portfoilio which has a grid of posts which are 400x400 cards with a hover effect that increases scale and adds a drop shadow (this is rendered in the dashboard route).
I have noticed however that when scrolling my page, the pointer will get hung up on the cards and stop registering scroll when the animation is enabled. I have come across this article (https://www.thecssninja.com/javascript/pointer-events-60fps) discussing the performance benefits of disabling pointer events to the body on scroll however I cannot figure out how to do this in react.
How would you add a class to the document body in React ONLY while scrolling and remove that class as soon as the scroll event had ended? Maybe with a setTimeOut in some way?? I feel like that would be a janky solution...
I included the code I am looking to implement this in, I don't know if that will help. The scroll event I have set up already is to maintain the scroll position of the page while the navbar is extended.
export default class Layout extends Component {
constructor(props) {
super(props);
this.state = {
hasMenuOpen: false,
scroll: {x: 0, y: 0},
};
this.handleScrollY = _.debounce(this.handleScrollY, 250);
this.handleWidthChange = _.debounce(this.handleWidthChange, 250);
}
componentDidMount() {
window.addEventListener('scroll', this.handleScrollY);
window.addEventListener('resize', this.handleWidthChange);
console.log('mount');
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (prevState.hasMenuOpen) {
/* eslint-disable */
let {x, y} = prevState.scroll;
/* eslint-enable */
// correct scroll y position back to 0 for positions <= 100
window.scrollTo(x, (y <= 100 ? y = 0 : y));
}
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScrollY);
window.removEventListener('resize', this.handleWidthChange);
}
// if menu is open an y = 0 (i.e position: fixed was added),
// scroll = previous states scroll
// else if you scroll and the menu isn't open, scroll = windows new scroll pos
handleScrollY = () => {
const y = window.scrollY;
const x = window.scrollX;
this.setState((prevState) => {
if (this.state.hasMenuOpen && y === 0) {
return {scroll: Object.assign({}, prevState.scroll)};
}
return {scroll: Object.assign({}, prevState.scroll, {x}, {y})};
});
}
handleWidthChange = () => {
console.log(window.innerWidth);
if (this.state.hasMenuOpen) {
return this.handleBurgerClick();
}
return null;
}
handleBurgerClick = () => {
this.setState((prevState) => ({
hasMenuOpen: !prevState.hasMenuOpen
}));
}
handleLinkClick = () => {
this.setState((prevState) => ({
hasMenuOpen: false
}));
}
render() {
const scrollTop = {
top: `-${this.state.scroll.y}px`,
};
console.log(this.state.scroll);
return (
<React.Fragment>
<Navbar onClick={this.handleBurgerClick} hasMenuOpen={this.state.hasMenuOpen} onLinkClick={this.handleLinkClick} />
<div className={this.state.hasMenuOpen ? styles.scroll_lock : ''} style={this.state.hasMenuOpen ? scrollTop : {}}>
<main className={styles.page_margin} >
<div className={this.state.hasMenuOpen ? styles.margin_extended : styles.margin_top}>
<Route path='/' exact component={Dashboard} />
<Route path='/new-post' component={NewPost} />
</div>
</main>
</div>
</React.Fragment>
);
}
}
For example I have tried setting document.body.style.pointerEvents = 'auto' in componentDidMount() and disabling it in the handleScrollY() however this obviously doesn't work as pointer events are never restored once the scroll event occurs. I have also tried setting it in componentDidUpdate() but that doesn't seem to work either as no component is being updated when the scroll event isn't happening.
One way of toggling a css class is:
componentWillUnmount() {
document.removeEventListener('scroll');
}
componentDidMount() {
document.addEventListener('scroll', (event) => {
const that = this;
if (!that.state.isScrolling) {
that.setState({ isScrolling: true });
setTimeout(() => {
that.setState({ isScrolling: false });
}, 300);
}
});
}
And then use the state or props to toggle the className
in your JSX something like
return (
<div className={`class1 class2 class99 ${this.state.isScrolling ? 'scrolling' : 'not-scrolling'}`} >Content</div>
)

Using context.drawImage() with a MediaStream

I'm new to React and I am building an app that takes screen grabs from a MediaStream. Based on what I've seen, the best way to do that is to draw it onto a canvas element using the context.drawImage() method, passing in the HTMLVideoElement as an argument. Here's what my action creator looks like:
const RecordImage = function(video, canvas, encoder) {
if(!encoder) {
encoder = new GifReadWrite.Encoder()
encoder.setRepeat(0)
encoder.setDelay(100)
encoder.start()
}
const context = canvas.getContext('2d')
context.drawImage(video, 0, 0)
encoder.addFrame(context)
return {
type: RECORD_IMAGE,
payload: encoder
}
}
This worked in the past because the RecordImage action was being called from the same component that housed the <video /> and <canvas /> element, and I could pass them in like so:
takePic(event) {
event.preventDefault()
this.props.RecordImage(this.video, this.canvas, this.props.encoder)
}
...
render() {
return (
<div>
<video
ref = { video => this.video = video }
width = { this.props.constraints.video.width }
height = { this.props.constraints.video.height }
autoPlay = "true"
/>
<canvas
ref = { canvas => this.canvas = canvas }
width = { this.props.constraints.video.width }
height = { this.props.constraints.video.height }
/>
<button onClick = { this.takePic }>Take Picture</button>
</div>
)
}
However, I would like to house the "Take Picture" button in a different component. This is a problem because now I don't know how to access the <video /> and <canvas /> elements from a sibling component. Normally I would store the arguments I need as part of the state, but the drawImage() method needs to use the HTML elements themselves. I've heard it's a bad idea to store DOM elements in the state, so what would be the best way to go about this?
In React it is possible to directly call a function on one of your components.
You could add a 'getPicture' function in your video component which returns the video element?
Your video component:
getPicture() {
return this.video
}
...
render() {
return (
<div>
<video
ref = { video => this.video = video }
width = { this.props.constraints.video.width }
height = { this.props.constraints.video.height }
autoPlay = "true"
/>
</div>
)
}
The picture button component could look something like this:
takePic() {
const element = this.props.getPic
const encoder = new GifReadWrite.Encoder()
encoder.setRepeat(0)
encoder.setDelay(100)
encoder.start()
const canvas = document.createElement("canvas")
canvas.width = element.offsetWidth
canvas.height = element.offsetHeight
canvas.drawImage(video, 0, 0)
encoder.addFrame(context)
return {
type: RECORD_IMAGE,
payload: encoder
}
}
...
render() {
return (
<button onClick = { () => this.takePic() }>Take Picture</button>
)
}
And then a parent component to bind the button and the video component:
getPicture() {
return this.video.getPicture()
}
...
render() {
return (
<div>
<VideoElement ref="video => this.video = video"/>
<TakePictureButton getPic="() => this.getPicture()" />
</div>
)
}
Disclaimer: I'm not sure how the draw function works, but you get the idea
Many thanks to #stefan-van-de-vooren for setting me on the right path! My situation was complicated by the child components using Redux connect, so getting them to work required some additional setup.
First, set up the parent component to call one child from the other:
setMedia(pic) {
return this.control.getWrappedInstance().setMedia(pic)
}
getPicture() {
return this.display.getWrappedInstance().getPicture()
}
componentDidMount() {
this.setMedia( this.getPicture() )
}
render() {
return (
<div>
<DisplayContainer ref= { display => this.display = display } />
<ControlContainer ref= { control => this.control = control } />
</div>
)
}
Because Redux connect returns a higher order component, we need to use getWrappedInstance() to gain access to the child functions. Next, we need to enable getWrappedInstance() in our child components by telling connect to use refs. Also, we will set up our child functions.
DisplayContainer:
getPicture() {
return this.video
}
...
render() {
return ( <video ref= { video => this.video = video } /> )
}
export default connect(mapStateToProps, null, null, { withRef: true })(Display)
ControlContainer:
setMedia(pic) {
this.setState({ media: pic })
}
takePic(event) {
event.preventDefault()
this.props.RecordImage(this.state.media, this.canvas, this.props.encoder)
}
...
render() {
return(
<button onClick= { this.takePic }>Take Picture</button>
<canvas ref= { canvas => this.canvas = canvas } />
)
}
export default connect(mapStateToProps, mapDispatchToProps, null, { withRef: true })(Control)
The action creator remains the same:
const RecordImage = function(video, canvas, encoder) {
if(!encoder) {
encoder = new GifReadWrite.Encoder()
encoder.setRepeat(0)
encoder.setDelay(100)
encoder.start()
}
const context = canvas.getContext('2d')
context.drawImage(video, 0, 0)
encoder.addFrame(context)
return {
type: RECORD_IMAGE,
payload: encoder
}
}
I should note, we are using refs quite a bit here. It is necessary in this case because the context.drawImage() needs the actual <video /> element in order to work. However refs are sometimes considered an anti-pattern, and it is worth considering if there is a better approach. Read this article for more information: https://reactjs.org/docs/refs-and-the-dom.html#dont-overuse-refs
A better solution would be to grab the image directly from the MediaStream. There is an experimental grabFrame() method that does just that, but as of Feb 2018, there is limited browser support. https://prod.mdn.moz.works/en-US/docs/Web/API/ImageCapture/grabFrame

Categories