I am trying to use IntersectionObserver in my react app. I want to achieve lazy loading with it. Therefore I observe several elements and if they appear on screen I load content inside them.
Here is a very simplified code:
class Table extends React.Component {
constructor() {
super()
this.state = {
entries: 0
}
}
componentWillUnmount() {
console.log('componentWillUnmount')
if (this.observer) this.observer.disconnect()
}
observe (c) {
if (!this.observer) {
this.observer = new IntersectionObserver(
entries => {
this.setState({entries: entries.length})
},
{ threshold: [0, 0.25, 0.5, 0.75, 1] }
)
}
if (!c) return
this.observer.observe(c)
}
render() {
const {entries} = this.state
return (
<div>
<h1>Number of observer entries: {entries}</h1>
<div
ref={this.observe.bind(this)}
style={{height: '1000px', display: 'block', width: '500px'}}
/>
<div
ref={this.observe.bind(this)}
style={{height: '1000px', display: 'block', width: '500px'}}
/>
</div>
)
}
}
ReactDOM.render(<Table />, document.querySelector("#app"))
When a component is mounted it shows two elements are observed but as soon as I scroll down it changes to only one element. I have not idea what I am missing.
JSON fiddle - https://jsfiddle.net/w1zn49q6/12/
The divs are stacked one after the other vertically. In the initial render, as they are laid out, the intersection observer gets triggered for both, as they enter the viewport together (the first div enters, the second div exits). However, once they are rendered, they will enter/exit one at a time on a normal course of vertical scrolling, hence the entries will only ever contain one div, which intersected the x-axis most recently.
The intersection entry only reports a transition to/from 0 (not in view) from/to 1 (fully in view). So when one div has fully exited/entered the view, it will no longer be present in an entry update.
You can still get 2 entries however :). If you manage to scroll really fast! Try it using an accelerated mouse wheel. So basically, between two intersection calculations, if both the divs moved too far, both will raise the intersection event, but if they move slowly, the intersection will be gradual because they are stacked one by one.
If you would stack them in the same row, you will continuously get two entries, as both will be intersecting at the same moment with the x-axis.
Related
I have a need to have some text scale in ways only SVG can as far as I could find. The text will change frequently so it also needs to adapt to that.
I'm making the app in react and would like to know how to calculate the bbox of an SVG (initially and every time it changes) before rendering it or at least without flickering / layout shift.
An example could be found here - the current issue is that it flicker. Everything else works fine more or less.
I've seen some other questions that are similar or nearly identical - however they do not have the requirement of changing text so it's possible to compute the bounding box in advance once or at least a one time flicker is not a big issue. Another question / thread also used a class component that supposedly updated the state at component mount but before render which as they claim does not cause a flicker but a lot has changed since then in react and in the example I tried the flicker is there.
The best compromise I've found so far is to just make the SVG's visibility hidden and measure the bbox of the text first and show it when done. Then every time the text changes measure again. Generally speaking this should not be too crazy in terms of jumping around or any other visual quirks but for best results you'd want to set a certain fixed size box for the SVG to fill up and not cause any layout shift.
import { useState, useEffect, useRef } from "react";
export type BBox = { x: number; y: number; width: number; height: number };
const makeViewBox = (bbox: BBox) => {
return `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`;
};
// Note that this component still sometimes jiggles around a bit
// and it is particularly noticable with nont monospaced fonts.
const ScalableSVGText = ({ text }: { text: string }) => {
// Reference to the SVG element, needed to take bbox measurements
// and adjust the SVG's viewBox
const ref = useRef<null | SVGSVGElement>(null);
// State is somewhat needed to force re-render - other methods
// can be used but whatever...
const [bbox, setbbox] = useState<null | BBox>(null);
// On initial mount & every time the text changes measure the
// bbox and update the state so the component re-renders
useEffect(() => {
if (ref.current) {
setbbox(ref.current.getBBox());
}
}, [text]);
return (
<svg
ref={ref}
width="100%"
height="100%"
viewBox={bbox ? makeViewBox(bbox) : ""}
style={{
// not strictly needed but makes it somwehat easier on the eyes
visibility: bbox ? "visible" : "hidden"
}}
>
<text>{text}</text>
</svg>
);
};
export default ScalableSVGText;
I have a board with rectangles and connecting lines rendered at the same time. Each line needs to know the dimensions of the two rectangles it connects, which are dynamic depending on the name - so height and width of each rectangle is different.
export interface IRectangle {
name: string;
x: number;
y: number;
}
export interface IConnectingLine {
startingNodeName: string;
targetNodeName: string;
}
const Board: FC<Props> = ({ rectangles, connectingLines }) => {
const findRectangleByName = (name: string): IRectangle =>
rectangles.find((r) => r.name === name)!
return (
<S.Container>
{rectangles.map((r) => (
<Rectangle key={r.name} {...r} />
))}
{connectingLines.map((cl) => (
<ConnectingLine
key={`${cl.startingNodeName}-${cl.targetNodeName}`}
startingNode={findRectangleByName(cl.startingNodeName)}
targetNode={findRectangleByName(cl.targetNodeName)}
/>
))}
</S.Container>
)
}
My question is what's the best way to accomplish that. One way to do it is pass the entire rectangles props to the connecting line, have it render the rectangle offscreen (visibility hidden absolute top: -99999 left: -99999) and get the ref. However, that will mean each connecting line will render two additional rectangles just to have their dimensions. Doesn't sound very performant.
Another way would be - each rectangle would call onSetWidth(dimensions) on mount, and that will update that board's state object that will hold an object with rectangles names as keys and dimensions as values.
The problem with this approach is that every rectangle will trigger a board rerender, which also might be problematic for performance, and I'm not even sure it won't enter a recursive loop since it will rerender the rectangles again.
Any ideas?
Is there a way maybe to have the board "prerender" the rectangles and pass the dimensions to the connecting lines and the "rendering results (the actual dom outcome)" to the rectangles?
Thanks in advance!
I am using react-google-maps and I want to accomplish something similar to Airbnb where when you hover over a search result and then a "marker" (aka OverlayView) is highlighted on the map. But the issue I am trying to solve is if an OverlayView is underneath another OverlayView, I want to bring that "active" OverlayView to the top (like by adjusting the z-index). The map on Airbnb does this very thing, which is when you hover over the search result and inspect the marker the z-index is altered to be 9001 to ensure it is brought to the front of all other markers.
There are a few issues related to this from years ago that have not received any movement:
tomchentw/react-google-maps#199
tomchentw/react-google-maps#93
In issue #199 it was mentioned you could hack around it with this.refs.childDiv.parentNode and setting z-index on that, but in React v16 this.refs is no longer a thing from what I can tell (it was deprecated). The original commenter attached a jsfiddle with the code sample which was:
class OverlayViewExample extends React.Component {
render () {
const pos = {lat: -34.397, lng: 150.644};
const mapPane = OverlayView.OVERLAY_MOUSE_TARGET;
const getOffset = this.getPixelPositionOffset.bind(this);
return (
<GoogleMap
defaultZoom={8}
defaultCenter={pos}
>
<OverlayView
position={pos}
mapPaneName={mapPane}
getPixelPositionOffset={getOffset}
>
<div style={{zIndex: 2}} className="overlay" ref="childDiv">This should be in front</div>
</OverlayView>
<OverlayView
position={pos}
mapPaneName={mapPane}
getPixelPositionOffset={getOffset}
>
<div style={{zIndex: 1}} className="overlay">Another overlay should be in front of me</div>
</OverlayView>
</GoogleMap>
);
}
getPixelPositionOffset (width, height) {
// this.refs is an empty object when I tried this :(
if (this.refs.childDiv) {
this.refs.childDiv.parentNode.style.zIndex = 2;
}
return { x: -(width / 2), y: -(height / 2) };
}
}
When I tried this workaround, this.refs was an empty object and therefore I could not alter the underlying zIndex of that node.
Is there another way to ensure when two OverlayViews are near each other that I can bring the one in the background to the foreground?
The answer for this was actually in my question (I should've tested the example code from Github)...
On this line: <div style={{zIndex: 2}} className="overlay" ref="childDiv">This should be in front</div> the zIndex was actually doing what I wanted (it would bring forward whichever element I wanted in the foreground).
So within each OverlayView, just apply a z-index to the first child element and that will effect the ordering of each OverlayView.
I'm using react and trying to build a facebook like chat - where the right column scrolls down and has a list of threads, and the middle column had the thread content and scrolls bottom up.
I have it all setup using flex, but am stuck on getting the middle column to scroll to the bottom by default (since I want to see the latest chat message). Here is the snippet for the actual container (I'm using React bootstrap):
<Row >
<Col ref={ (node) => { this.cont = node }}>
{this._buildThread()};
</Col>
</Row>
and here is the snippet for my ComponentDidMount:
componentDidMount() {
MessageStore.addChangeListener(this._onChange);
MessageService.getThread(this.props.id, 1000, 1, false);
this.cont.scrollTop = this.cont.scrollHeight;
}
Where cont is the ref for the container div that holds my messages. Yet nothing happens - it doesn't scroll, and if I look at node.scrollTop after setting it, it remains at 0 - seems immutable.
Any help would be much appreciated. Thanks!
Not sure about the details of your CSS, but I just had a similar-looking issue. The solution in my case was to make sure that the element itself would scroll and not its container:
.containerIWantToScroll {
overflow-y: auto;
}
If the element's container expands to fit the element, then that element will not scroll, thus its scrollTop value is always zero.
When are you triggering the code you included? To scroll to the bottom on render, I would write something like the following, calling triggerScroll in componentDidMount:
class App extends React.Component {
componentDidMount () {
this.triggerScroll();
}
triggerScroll () {
this.cont.scrollTop = this.cont.scrollHeight;
}
render () {
return (
<div>
<div className='containerIWantToScroll' ref={ (node) => { this.cont = node } }>
Content
</div>
</div>
)
}
}
I'm trying to swap two children of an element with a transition using React.
<div style={{position: 'relative'}}>
{this.state.items.map((item, index) => (
<div
key={item}
style={{position: 'absolute',
transform: `translateY(${index * 20}px)`,
transition: '1s linear transform'}}>
{item}
</div>
))}
</div>
state.items is an array of two items. When it is reordered, the two child divs should transition to their new positions.
What happens in reality is while the second element transitions as expected, the first one jumps instantly.
As far as I can tell, React thinks that it can reuse one of the child elements, but not the other, although the docs say that if we use the key attribute, it should always reuse elements: https://facebook.github.io/react/docs/reconciliation.html (at least, that's how I understand it).
What should I change in my code to make it work as expected? Or is it a bug in React?
Live example: http://codepen.io/pavelp/pen/jAkoAG
caveat: I'm making some assumptions in this answer, nevertheless it shines light some of your (and previously my) questions. Also my solution can almost certainly be simplified, but for the purposes of answering this question it should be adequate.
This is a great question. I was a bit surprised to open up the dev tools and see what's actually happening when swapping the items.
If you take a look, you can sort of see what React is up to. The second element is not changing its style prop at all and just swaps the inner text node, while the first element is dropped into the dom as a fresh element.
If I had to guess, this is because of the way swapping two items in a array works, where at least one item is copied to a temp variable and placed back into the array.
I thought that maybe if you make the translation random, both elements would get new style props and animate, but that only made it more clear this was not the intended behaviour.
On the way to finding a solution:
As an experiment, what if we created the nodes ahead of time, and pass the index prop in render via React.cloneElement. While we're at it, let's render a span if index === 0 and a div otherwise. No keys to worry about.
http://codepen.io/alex-wilmer/pen/pbaXzQ?editors=1010
Opening up the dev tools now illustrates exactly what React is intending. React is preserving the elements and only changing the relevant part, in this case the innerText node and the element type. Because the styles are swapped exactly 1 : 1, no style update is needed.
Solution:
You can generate your React elements ahead of time, keep those in an array, and as such there are no keys to shuffle around and figure out how to place back into the DOM. Then use a different array to keep track of the intended order. Possibly highly convoluted, but it works!
http://codepen.io/alex-wilmer/pen/kXZKoN?editors=1010
const Item = function (props) {
return (
<div
style={{position: 'absolute',
transform: `translateY(${props.index * 20}px)`,
transition: '0.5s linear transform'}}>
{props.children}
</div>
)
}
const App = React.createClass({
getInitialState () {
return {
items: [
{item: 'One', C: <Item>One</Item>},
{item: 'Two', C: <Item>Two</Item>}
],
order: ['One', 'Two']
};
},
swap () {
this.setState({
order: [this.state.order[1], this.state.order[0]]
});
},
render: function () {
return <div>
<button onClick={this.swap}>Swap</button>
<div style={{position: 'relative'}}>
{this.state.items.map(x =>
React.cloneElement(x.C, {
index: this.state.order.findIndex(z => z === x.item)
}))
}
</div>
</div>;
}
});