How to hold screen position when data added in react? - reactjs

I have a list in react that added more rows with interval,
my problem is that when the list grown the screen going up.
I want to hold the position of screen how he was before I added rows to list.
That is my code:
setInterval(() => {
//this row save the position of the screen before the adding row
setPrevRef({scrollHeight:tabElement.current.scrollHeight,scrollTop:tabElement.current.scrollTop})
props.setFilteredPT (prev=> [newRow,...prev])
}, 10000);
and
useEffect(()=>{
tabElement.current.scrollTop =prevRef.scrollTop + (tabElement.current.scrollHeight - prevRef.scrollHeight )
},[props.filteredPT])
After I wrote this code the screen going up to a second and than comeback to the place.
here is how react does it with class component: https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html#reading-dom-properties-before-an-update
but I want do it with function component.
Somebody can help?

I had found that my problem cause I used for map key the index, when I have changed it to uniq ID it worked good.

Related

Animate entry of (only new) elements in a virtualized list

I am using react-window FixedSizedList and react-virtualized-auto-sizer Autosizer components to build a list/table UI element that may contain thousands of items that also receives new items via a websocket connection and prepends them to the list. I now have a requirement to animate the entry of new elements in this list.
Here is a codesandbox link with a minimal (and not quite working) example: codesandbox.
Note how the .row animation is triggered for every child of FixedSizedList each time the data list receives a new item. Also note how the .row animation is again triggered for every child of FixedSizedList when the list is scrolled.
I understand that the reason this is happening is because of how list virtualization works using absolute positioning of the children rows. Every time a new item is inserted into data, or the list is scrolled, react-window needs to re-compute the style prop for each row, which recreates every DOM element and hence re-triggers the .row animation.
I would like to animate only the new .row DOM elements when they appear.
Things I have tried:
Add animation-iteration-count: 1; to the .row class. This doesn't work for the same reason mentioned above as every element is re-created on a new item insert.
Animate only the first row (example using red background in the code sandbox). While this "works", it's not quite suitable. In my production site, updates are not guaranteed to come through one at a time - multiple rows might be inserted at the same time. All new rows should be animated when they are added to the DOM. This can be replicated in the code sandbox by inserting two UUID's at once in the hook.
Not use list virtualization. This of course works fine, but is not suitable. In my production site this list could contain thousands of items.
Reading this previous question. The previous question is sparse of information, does not have a minimal example, and has no useful answers or comments. Additionally, it is over 5 years old.
How is it possible to achieve the result I'm looking for here?
EDIT:
A further attempt I have tried, extending on 2) above:
Save a copy of the "old" list of items each render. When an update is received, subtract the length of the old list from the length of the new list (call this number n). Animate the top n items of the new list. This "works", but the production system has some intricacies making this solution insufficient - each list item is dated with an ISO timestamp, and the list is sorted according to timestamp new -> old. Updates received via websocket also have a timestamp, but there is no guarantee the new item will always be newer than the current top of the list - in some cases, new items are inserted into position 2, 3 or further down in the list. In this case, animating the top n items based on the length change would not be accurate.
DonĀ“t know how costly it can be for you, but one way to do that is on every state change add a specific className to the new insert rows and a className to the already inserted rows.
So that way you can handle using the className related to the new inserted lines.
Something like this:
const mappedObject = (data, className) => {
return {
item: data,
className
};
};
React.useEffect(() => {
const interval = setInterval(() => {
const alreadyInsertedData = data.map((item) => {
item.className = "already-inserted";
return item;
});
setData((prev) => [
mappedObject(uuidv4(), "first-insert"),
mappedObject(uuidv4(), "first-insert"),
mappedObject(uuidv4(), "first-insert"),
...alreadyInsertedData
]);
}, 3000);
return () => {
clearTimeout(interval);
};
}, [setData, data]);
Here's a code sample to you check.

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

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.

ReactTable setState on columns is not changing layout

first I should mention that I'm very new to react so this might be something silly..
I've struggled to get ReactTable's column to be sortable with the help of mattlockyer's gist
What I don't understand is I call setState on columns and then the render call is triggered correctly but it doesn't update the layout.
The render function "columns" contains an updated object with the correct column order, however render does not change the column order nor does sorting columns content.
This is the code in question,
this.setState({
columns: newColLayout,
}, () => {
this.mountEvents();
});
It will trigger a render, but the columns stay the same.
Ive put together a codepen with my current work and with comments about the code in question.
Any help would be greatly appreciated.
OK so i think i figured out whats going on.
The issue is splice modifies the state without triggering a change event, then when the event is fired from setStat, react doesn't see the change made to the column layout as it matches the current value.
The way i get around this is by cloning the column array using slice.
// Clone the current column state.
const columnClone = this.state.columns.slice(0);
// Remove item from array and stick it in a new position.
columnClone.splice(i, 0, columnClone.splice(this.draggedCol, 1)[0]);
Here is the updated code...
Still might be improved by someone who has more experience with react.

Resources