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
Related
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.
I am using React-admin for a project where for some resources, I use the tabbed form to better organize the fields and inputs. I created a fairly simple custom map component based on react-leaflet, which I am using in these tabbed forms.
I am facing a weird issue where when the map is on other than the first tab, its contents do not display correctly. It appears as though the map "thinks" the viewport is much smaller than it actually is. Reloading the page (or even just opening developer tools in Chrome) forces re-render of the page and causes the map to start behaving correctly.
To better demonstrate, I created this simple Codesandbox project. It has a user resource from the RA tutorial with two tabs. Both contain an instance of the map component, but while the map on the first tab works correctly right away, the one on the second tab renders incorrectly.
I confess I am still kind of a noob at these things so I may well be doing something wrong, but I've been scratching my head for quite a few hours over this and I'd like to start eliminating possible culprits.
Any insight would be most welcome.
Thanks very much in advance for your time.
This issue has been discussed a lot in SO. If you search a bit you can find the reason. What you actually need to do is two things:
use setInterval in combination with map's invalidateSize method when switching a tab and then clean it on component unmount
use useEffect to change mapZoom and view since they are immutable.
SO since you use react-leaflet version 2.7.x you need to take the map instance using a ref:
const mapRef = useRef();
useEffect(() => {
if (!mapRef.current) return;
const map = mapRef.current.leafletElement;
const mapZoom = zoom || defaultZoom;
let intervalInstance;
if (!center && marker) {
map.setView(marker, mapZoom);
intervalInstance = setInterval(() => map.invalidateSize(), 100);
} else if (!center) {
map.setView([0.0, 0.0], mapZoom);
}
map.setZoom(mapZoom);
return () => clearInterval(intervalInstance);
}, []);
<LeafletMap
ref={mapRef}
center={marker}
zoom={defaultZoom}
className={classes.leafletContainer}
>
Demo
Trying to implement a custom cell framework renderer with React and it seems simple enough. Create the React component, register it with frameworkComponents.
The data that populates rowData is coming from my redux store. The custom cell renderer is a functional react component.
The issue is that because I'm using a frameworkComponent - a React component in my case - as a cellRenderer, it seems that any change in the data for the grid the I'm getting via useSelector(selectorForMyData) causes a re-render of my frameworkComponent, which on the browser, looks like a random, annoying flicker. The application is heavily wired into redux
Two questions:
1 - How come when I natively use ag grid to render this cell using a AgGridColumn without any custom cell renderers, it doesn't cause this re-rendering behavior on the same store changes? I have a click event bound to the main page that toggles a flag to false (in the case a snackbar alert was open).
2 - Is there any way to get around this? I've tried wrapping my return statement in the framework cell renderer component with a useMemo with the params as a dependency, but that doesn't seem to work. Also tried making a render function via useCallback with the same idea as useMemo and that doesn't help either :/
Thanks
pseudo-code for situation:
App.tsx:
<MyAgGrid/>
MyAgrid.tsx:
const MyAgGrid = () => {
const data = useSelector(selectorForData);
return (
<AgGridReact
rowData={data}
frameworkComponents={
{'myCustomRenderer': CustomRendererComponent}
}
columnDefs={
['field': 'aField', cellRenderer: 'myCustomRenderer']
} />
);
};
CustomCellRendererComponent.tsx:
const CustomCellRendererComponent = (params) => {
console.log("params", params) //logs on every redux store update
return (
<div>HELLO WORLD</div>
);
};
The cells that are rendered via the CustomCellRendererComponent are re-rendered on any redux store change. I'm guessing it's due to useSelector causing the re-render on store changes, which is expected, but then it doesn't answer my first question.
EDIT:
I went "function as class" route shown here ("MyCellRenderer") and so far am not seeing the re-rendering issue, so I will stick with that for now even though it's god ugly. This leads me to believe my issue is trying to fit a React component/hooks, with its lifecycle nuances, as a cell renderer is causing problems. Still, I feel like there should be a way to prevent the behavior of constant re-rendering, otherwise it's a pretty useless feature
EDIT pt 2:
I dug deeper and while I haven't found an out of the box solution for it, I added the reselect library to memoize some of my selectors. The selector I use to get rowData is now memoized, and I'm no longer seeing this issue. Will mark as answer in a few days if no one provides a better, ideally out of the box (with redux or ag grid), solution for it.
As I stated in one of my edits. I figured it out, kind of.
I added the library reselect to the application, and it fixes the symptoms and I'm content with it going forward. It allows you to memoize your selectors, so that it only registers changes/"fires" (leading to a re-render) only if the any of the dependency selectors you hook it into changes/fires, so now, my grid doesn't "flicker" until the actual row data changes since my selectorForRowData is memoized!
I'm still uncertain why prior to using a frameworkComponent (React Component) as a cell renderer I wasn't seeing the symptoms, but I'm happy to just assume Ag-Grid has a lot of performance tricks that clients may lose when plugging in their own custom functionality, as is the case in a custom cell renderer.
Whenever I update the datasets of my CanvasJS component, all lines get re-drawn (in an animated fashion) instead of the points simply being added to the end. How can I prevent this?
https://imgur.com/4dqUMwT (short clip of the re-drawing, can't inline this it seems.)
I'm currently calling setState every 5 seconds through an interval (which polls an API). In the top of my render method I perform some transformations to the array saved in state (e.g. .map((e) => { return { x: e.x, y: e.y }; });), and then I pass the resulting array to data of the CanvasJSChart.
Edit:
I have just found the following page: https://canvasjs.com/react-charts/dynamic-live-line-chart/. This however sounds like a terrible anti-pattern. I'm going to see about wrapping this in a component which returns false on shouldComponentUpdate, but I'm not sure if that will work. If anyone has a better idea, please let me know.
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.