How to get height of an element at each page update - reactjs

I have a content area on my page that displays data that gets to be pulled through from an API. I hooked it up so once data gets pulled from the API, it gets saved to the Redux store and page displays it. Now, I need to add a scroll to top and bottom buttons so if page is longer than given amount lets say 600px, they will appear. Odd thing is that, at first load of the page, API query does not kick in therefore page is empty so buttons should not appear.
Given all this, I don't see a way but to do this within componentDidUpdate()
componentDidUpdate() {
let ContentArea = document.getElementById('contentArea');
if(ContentArea != null & ContentArea.clientHeight > 0){
this.props.pageContentHeight(ContentArea.clientHeight)
}
}
and save the height value to Redux store so in the code I can display the scrolls like this:
{(contentHeight > 600) && <Scroll to="bottom" /> }
<Router page={pageURL} />
{(contentHeight > 600) && <Scroll to="top" /> }
contentHeight above is coming from redux store.
From what I read, it's not recommended to dispatch within React update lifecycle especially since dispatch causes update lifecycle to kick in too, it might cause a endless cycle. Is there any way I can accomplish that you guys think of adding scroll to top or bottom buttons onto page?
There's this package allows scrolling bottom but not top:
https://www.npmjs.com/package/react-scroll-to-bottom

It is perfectly okay to dispatching actions from life cycles. Just make sure to add guards to avoid infinite loops. That is, only dispatch an action from componentDidUpdate if the data needs to be updated.

Thank you for #pgsandstrom for his help on the topic.
For anyone trying to implement Scrollbar for content area, I've actually set up Redux to work using componentDidUpdate but then, found out even better approach. What if we can check body height and window inner height and compare then decide based on that? so below code exactly does that. Essentially returns true if scrollbar is visible on the page and false if not, then you can decide to display Scrollbar based on that.
document.body.scrollHeight > document.body.clientHeight
I did below:
componentDidUpdate() {
if(document.body.clientHeight > window.innerHeight) {
document.getElementById("topPage").style.display = "block";
document.getElementById("bottomPage").style.display = "block";
} else {
document.getElementById("topPage").style.display = "none";
document.getElementById("bottomPage").style.display = "none";
}
}
make sure to place it inside a component that can recognize the update.

Related

Performance issues if MapComponent state is updated

I am not sure if this is an issue of react-leaflet-markercluster, react-leaflet, leaflet, react, or my code.
I have a map with several thousand markers and I am using react-leaflet-markercluster for marker clustering. If I need to update a global state of MapComponent, there is 1-3 seconds delay when this change is reflected.
I created a codesandox with 5000 markers and you can see there 2 use cases with performance issues:
1.) MapComponent is inside react-reflex element, that allows resizing panel and propagates new dimensions (width, height) to MapComponent. If width and height are changed, mapRef.invalidateSize() is called to update map dimensions. Resizing is extremely slow.
2.) If user clicks on Marker, global state selected is updated. It is a list of clicked marker ids. Map calls fitBounds method to focus on clicked marker and also marker icon is changed. There is around 1 second delay.
In my project, if I need to change a MapComponent state, it takes 2-3 seconds in dev mode when changes are reflected and it is just a single rerender of MapComponent and its elements (markers).
I took a look at Chrome performance profile and it seems like most time is spent in internal React methods.
It is possible to fix this by preventing rerendering using memo, which is similar to shouldComponentUpdate, but it makes whole code base too complicated. preferCanvas option doesn't change anything. I am wondering what is a good way to fix these issues.
The main problem I identified in your code is that you re-render the whole set of marker components. If you memoize the generation of those, you achieve a good performance boost; instead of running the .map in JSX, you can store all the components in a const; this way, the .map won't run on every render.
from this
...
<MarkerClusterGroup>
{markers.map((marker, i) => {
...
to something like this
const markerComponents = React.useMemo(() => {
return markers.map((marker) => {
return (
<MarkerContainer .../>
);
});
}, [markers, onMarkerClick]);
return (
<>
<MarkerClusterGroup>{markerComponents}</MarkerClusterGroup>
</>
);
The second refactor I tried is changing the way you select a marker. Instead of determining the selected prop from the selected array for each marker, I put a selected field on every marker object and update it when selecting a marker. Also, I add the position to the onClickHandler args to avoid looking for that in the markers array.
There are some other tweaks I don't explain here so please check my codesandbox version.
https://codesandbox.io/s/dreamy-andras-tfl67?file=/src/App.js

Scroll to props.location.hash using useEffect after component mounts

I am creating a glossary page where each term has it's own card that is not expanded by default. Each card uses the term as it's ID because they will all be unique. I want to support direct links to a specific term via URL hash.
So, if the URL is localhost:3000/#/glossary#Actor, the initial load will scroll to the term and 'open' the card by simulating the click on the element (click handler on that element opens the card).
It is working, but too frequently. Every time I enter text into the search bar or cause a render in any way, it re-scrolls and does the card open animation again. I just want to initially scroll to the term if there is a hash, then ignore it unless it changes.
It only works if I don't include a dep array. If I include a dep array with props.location.hash, the el returns as null and nothing happens. I'm aware that the restarting of the effect is happening because of no dep array, but I don't know why it doesn't work when I add it.
useEffect(() => {
//looks for hash in URL; gets element associated with the hash
let el = document.getElementById(decodeURI(props.location.hash.slice(1)));
console.log(el)
//if element exists, get coords and offset for header before scrolling
if (el) {
const yCoordinate = el.getBoundingClientRect().top + window.pageYOffset;
const yOffset = -80;
window.scrollTo({
top: yCoordinate + yOffset,
behavior: 'smooth'
});
//opens the card only if not already open
if (!el.classList.contains('openTermCard')) {
el.click();
}
}
})
useEffect may not be the correct method. I might not even need a hook for this, idk. I just want this initial scroll event to happen on page load and not again.
It's a bit hard to figure out a solution without a working sandbox.
From your description, it seems to me your component is rendering more than once on the first load. If you put [] as dependency array, the effect only runs once, when the element is not defined yet (hence why you get null) and that's why it doesn't work.
One way to go about it is to think what else you can do in terms of designing your app. You mention that every time your component updates it focuses the card again, but that's kind of what you expect when you're using useEffect. Is it possible to re-structure the component so that the parent (the one with this side effect) doesn't re-render as often?
If you can provide a code sandbox I'm sure I (and probably others) will be able to help you further.

How to control a non-React component (BokehJS) in React?

Backstory
I want to include a BokehJS plot in my React component. The process for this is to render <div id="my_plot_id" className="bk-root"/> and call window.Bokeh.embed.embed_item(plotData, 'my_plot_id') which injects needed HTML into the DOM.
Because I want to control the BokehJS plot using the React component's state (i.e replace the plot with new generated plot data), I don't want to just call embed_item() in componentDidMount(). I've instead placed embed_item() in render() and added some code to remove child nodes of the container div prior to this call.
Problem
My React component renders 3 times on page load and although by the final render I have only one plot displayed, there is a brief moment (I think between the 2nd and 3rd/final render) where I see two plots.
Code
render()
{
let plotNode = document.getElementById('my_plot_id');
console.log(plotNode && plotNode.childElementCount);
while (plotNode && plotNode.firstChild) {
//remove any children
plotNode.removeChild(plotNode.firstChild);
}
const { plotData } = this.state;
window.Bokeh.embed.embed_item(plotData, 'my_plot_id');
return(
<div id="my_plot_id" className="bk-root"/>
)
}
In console I see:
null
0
2
Question
So it seems embed_item executes twice before the my_plot_id children are correctly detected.
Why is this happening and how can I resolve it? While the triple render may not be performance optimized I believe my component should be able to re-render as often as it needs to (within reason) without visual glitching like this, so I haven't focused my thought on ways to prevent re-rendering.
Interaction with DOM elements should never happen inside the render method. You should initiate the library on the element using the lifecycle method componentDidMount, update it based on props using componentDidUpdate and destroy it using componentWillUnmount.
The official React documentation has an example using jQuery which shows you the gist of how to handle other dom libraries.
At start plotNode is unable to reach 'my_plot_id'.
You can render null at start, when plotData is unavailable.
You can use componentDidUpdate().
In this case I would try shouldComponentUpdate() - update DOM node and return false to avoid rerendering.

How can I define state based on another state property?

I come from the Ember world so apologies if this question is very basic (I'm sure it is). I have a component which sets the state "scrollPosition" whenever the window is scrolled. I would like to define a new state property, "isScrolledToTop", which is equal to true when "scrollPosition" is 0.
In Ember I would have simple defined a new property and checked the condition when scrollPosition changed. Not quite sure how to do this in React. I was thinking of using "componentDidUpdate", but pretty sure this is not the right approach. Thanks for the help in advance!
If you're asking "how can I fire some event once the user scrolls to the top of the page" then the answer is to do that in the onscroll event callback.
If you want to render something differently (e.g. a full-size header) when the scroll position is at the top of the page, then the logic would go in the render() method of your component. Something like:
render() {
if (this.state.scrollPosition === 0) {
// do something specific to this scenario
}
return ...
}
This part of the docs suggests not putting 'computed data' in state, which is what isScrolledToTop is.
As an FYI, if you're tracking the scroll position, the window object keeps this in its 'state' for you, available at window.scrollY - you don't need to duplicate this into your component's state.

infinity scroll in reactjs fixed data table

I need to implement infinity scroll features for fixed data table.
I checked API doc, there is no events dispatched when user scroll to the end of the table. The only seemly helpful event is onScrollEnd, but that event gives me back scrollX and scrollY.
ScrollY is pretty huge, I don't know how to use this number to detect user is scrolling near the end.
Can anyone tell me how I can implement infinity scroll feature using fixed data table?
Thanks
It is a shame that such component does not have proper API for infinite scroll.
The solution is this (but it is ugly):
The onScrollEnd() returns you a scrollY value. You need to persist this value in component state, e.g. this.state.scroll. On each onScrollEnd, you have to check for equality:
this.state.scroll === nextState.scroll && nextState.scroll !== 0 // you dont want to load dada on scrollTop.
If this is true, then you can load additional data, update your store or what you have, and re-render the component.
BIG PROBLEM: the onScrollEnd() function is very very slow :( Maybe do debouncing/throttling?

Resources