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

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.

Related

Absolute positioning of image based upon size of user's resolution (Gatsby)

I am attempting to place an image (using absolute positioning) above the fold on the initial render of a page I am building using React (Gatsby SSR). The issue I am having is that the useWindowSize hook fires immediately and then erroneously places the image in the wrong position.
My current solution determines whether the component exists in the vDOM and then uses a setTimeout , before pushing the new position values into state.
// On initial render, position hero container background
useEffect(() => {
if (elementRef && inView) {
setTimeout(() => {
console.log("FIRED");
const boundingClientRect = elementRef.current.getBoundingClientRect();
setContainerWidth(boundingClientRect.width);
setContainerHeight(boundingClientRect.height);
setContainerDistanceFromTop(boundingClientRect.top);
setContainerDistanceFromLeft(boundingClientRect.left);
}, 2000)
}
}, [inView]);
Obviously there are many, many flaws with this approach (won't trigger on slow devices) - but I'm struggling to think of the most optimal way to cause a re-render of the image.
Another solution would be to repeatedly check if the state has changed for a period of time (every second for 10 seconds), but this still doesn't feel very optimal.
I am sure there's a far more elegant approach out there, would be grateful if anybody could assist?
Thanks
Well, you can avoid 4 of your useStates using one single useState that contains an object with all your properties. In addition, you should be able to get rid of the setTimeout because using the empty deps ([]) will ensure you that the DOM tree is loaded (hence your element is in the view).
const [properties, setProperties]= useState({});
useEffect(() => {
if(elementRef.current){
const boundingClientRect = elementRef.current.getBoundingClientRect();
setProperties({
width: boundingClientRect.width,
height: boundingClientRect.height,
top: boundingClientRect.top,
left: boundingClientRect.left
})
}
}, []);
It's important to set initially the elementRef as null the avoid React's memoization and setting initially the value as null before rehydration. In that way, you only need to check for the elementRef.current and setting all properties at once using one single useState. After that, you only need to access each property like: properties.width, and so on.
The inView boolean is also unnecessary since the empty deps ([]) will fire your effect once the DOM tree is loaded.

React Swiper.js Deleting slide at index rendered from array

I can't seem to figure out how to delete a slide at the current index which is rendered from an array using Swiper.js. Code Sandbox link here.
I have tried:
function deleteSlide() {
swiper.current.swiper.removeSlide(currentIndex);
var temp = slides;
temp.splice(currentIndex, 1);
setSlides(temp);
}
But the problem is that it does a double delete when the state updates. If I only run removeSlide, it removes the slide but my array is not updated. If I do anything else, my array updates but the slides don't update until I do a swipe. Adding slides to the end of the array so far works fine.
Without explicitly calling removeSlide, I tried the following:
useEffect(() => {
swiper.current.swiper.update();
}, [slides]);
But I'm not quite sure why this isn't actually updating the Swiper instance, according to the docs that's what I believe it should be doing. I recognize that this isn't being called when the state updates in deleteSlide either, and I'm not entirely sure why.
The ideal behavior is that the current view stays put, and when the current slide is deleted, the slide at index + 1 slides into position. If you delete the very last slide, then you slide to the 2nd last slide and the last slide gets deleted. I can take care of this as long as I can actually update the Swiper instance correctly.
How should I go about this? Appreciate any help, thank you.
Managed to solve this:
function deleteSlide() {
var temp = [...slides];
temp.splice(currentIndex, 1);
setSlides(temp);
}
Just had to use the spread ... operator to copy the array correctly. Although it is worth noting that this resets the state for all the other rendered slides, I need to figure out how to fix that.
EDIT: Updated Code Sandbox. To prevent re-rendering of all other array elements, the key of each element must be unique across renders. Fixed by setting the key to be a count, which is strictly increasing.

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

How to hide or remove an element onClick in React?

I am trying to hide an element 'GorillaSurfIn' after I click on it.
But also it should fire the 'setShouldGorillaSurfOut' to 'true'. The second part works, but after I added this function:
function hideGorillaSurfIn() {
let element = document.getElementById('gorilla-surf-in');
ReactDOM.findDOMNode(element).style.display =
this.state.isClicked ? 'grid' : 'none';
}
After I click, the code falls apart.
Once I click, the element should be hidden/removed till the next time the App restarts.
Here is the Code Sandbox Link for further explanation.
I am open to any solutions, but also explanations please, as I am still fresh in learning React.
I have changed your code a bit to make it work. You can make further changes according to your need. A few things that I would like to add: -
You should avoid using findDOMNode (in most cases refs can solve your problem) as there are certain drawbacks associated with findDOMNode, such as the react's documentation states "findDOMNode cannot be used with functional components".
I've used refs (forward ref in this case) to make it work.
GorillaSurfIn was called twice, so there were two Gorilla gifs on the screen with same IDs. Not sure if that was the intended behaviour but each element should have unique ID.
Check out the code sandbox.

How to get height of an element at each page update

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.

Resources