I have a WMSLayer which takes a string of comma-separated layer names to make a call to fetch tiles.
import React, { Component } from "react";
import { WMSTileLayer } from "react-leaflet";
class WMSLayers extends Component {
render = () => (
<WMSTileLayer
format="image/png"
layers={this.props.layers}
url="https://ws.topogrids.de/geoserver/ows"
/>
);
}
This component begins network requests when the layers prop changes. This means as soon as a new layers prop is passed, the current WMSLayer is erased, and new tiles begin to draw as they are received. This of course makes total sense.
What I would like to do is somehow retain the old component until the new tiles are fetched, and then render the new tiles all at once. This way, if I simply turn one more layer on, the current layers do not disappear.
I realize I can accomplish this by creating a separate WMSTileLayer for each layer, but this leads to many API calls per zoom/pan if multiple layers are switched on.
The layer does provide an onLoad event that gets fired when the tiles are done loading, but I don't know how to keep the old layers drawn until this is done, if it's even possible.
Advice would be greatly appreciated!
Also, this is not a duplicate of How to keep old tiles until each new tile is loaded when redrawing a layer in Leaflet? - that question is about using multiple layers with one map, but this question is about using one layer with a map (I realize the WMSTileLayer layers prop name might make this seem confusing)
Related
I currently have a Radar chart in chart.js using the react integration.
I was suprised to note that, when I updated the data, instead of showing a completely new plot, it just transitioned smoothly from the previous dataset to the new one.
What I am interested in is to understand how it works under the hood, which honestly I can't understand, at least from looking at the code.
First: my understanding of React is that, when a prop or state changes, it computes the new DOM, and then merges the new DOM and the current DOM, applying only the difference between the two. However, chartjs seem to be implemented as a Canvas element.
The chartjs integration with react does not do much. Taking the Radar plot, this is what it does
export const Radar = /* #__PURE__ */ createTypedChart('radar', RadarController);
which is nothing but declare a <Chart> element and leave it to ChartJS to plot it. In fact, in ChartJS, we have this code, which basically manages the Canvas element and it is smart to perform transitions using animations and so on. This I understand (relatively): a lot of animation and transition helper functions, but this makes sense to me. However, this part is pure JavaScript. There's nothing that is aware of React.
What does not make sense is therefore how the react synchronization system is integrated with this JavaScript library so that the state invalidation of the props/state is synchronised to an animation, instead of a complete rewrite of the Canvas element. I don't seem to find where this magic happens in react-chartjs-2.
As you explained the canvas element does not get changed so it gets reused. To animate the chart chart.js itself has an update method. React-chartjs-2 uses a useeffect function that checks if the data you pass it has changed. If this is the case it calls the update function from chart.js itself and they handle the animations and updates itself:
useEffect(() => {
if (!chartRef.current) return;
if (redraw) {
destroyChart();
setTimeout(renderChart);
} else {
chartRef.current.update();
}
}, [redraw, options, data.labels, data.datasets]);
https://github.com/reactchartjs/react-chartjs-2/blob/4a010540ac01b1e4b299705ddd93f412df4875d1/src/chart.tsx#L78-L87
This is my understanding of the whole process after diving into the code base quite a bit. I've tried to be as detailed as possible with links to the exact line of code I am talking about. Hope this helps:
Beginning with the code snippet you shared:
export const Radar = /* #__PURE__ */ createTypedChart('radar', RadarController);
If you follow the RadarController via the import statement, you see that it is fetched from chart.js
Now we move to the Chart.js code and look for this controller RadarController. It is found in a file called src/controllers/controller.radar.js.
Within that file, you see an update function
This function then calls updateElements with the points information
This function gets the new point position which is then set in properties and passed into the updateElement function
This updateElement function directly takes us to the core.datasetController
Here you see a condition to check if the chart is in directUpdateMode. If not, it calls a function to _resolveAnimations
Within this function, you will see the new Animations(args) object
This eventually brings us to the core.animations file which consists of all the animation related information and processing.
One interesting bit I found here was: this is what seems to be making the beautiful movement of points to the changed location.
You can explore this Animations class further for more detailed understanding
So yeah essentially, it is the js part under the hood that facilitates the smooth transitions and this is how it does it. React code is essentially just like a wrapper of Chart.js calling this update method with the new values.
You can see here: https://github.com/reactchartjs/react-chartjs-2/blob/master/src/chart.tsx
The react-chartjs-2 library creates a component that adds a canvas and when the props update the component creates/updates an internal Chart object that uses the rendered canvas.
From what I saw the animation starts when the props are changed.
The path is props->react-chartjs-2 component->chart object->animation
We're using react-konva, which renders a <Stage> component that manages an underlying canvas/context.
We want to use canvas2svg with it, which is basically a wrapper around a canvas context that tracks all draws to the canvas and maintains an svg representation for when you're ready to access it. It doesn't modify the canvas api, so in theory, Konva would be unaffected in that its canvas draw calls would be the same - they'd just need to be on the context generated by canvas2svg, rather than the one konva automatically generates.
We're looking for something like this, but it doesn't seem to exist. Are there any ways we could use a ref or otherwise hack Konva into using a C2S context? Or maybe we're missing a built-in property.
var c2sContext = new C2S(500,500);
<Stage context={c2sContext}>
canvas2svg doesn't actually work alongside a normal rendered canvas (it's an alternative to one - for example, the arc() method just renders to svg, not svg + canvas), so you need to replace the render context with a c2s instance, call render so the methods like arc() get called, and then set it back.
You can use a ref on the konva Layer and run code like the following on a button press:
setTimeout(() => {
var oldContext = this.layerRef.canvas.context._context;
var c2s = this.layerRef.canvas.context._context = C2S({...this.state.containerSize, ctx: oldContext});
this.forceUpdate();
setTimeout(() => {
console.log(c2s.getSerializedSvg());
this.layerRef.canvas.context._context = oldContext;
this.forceUpdate();
})
}, 5000);
Two possible solutions that skirt around the need for canvas2svg:
Generating an SVG string manually without rendering svg elements
Rendering svg elements rather than canvas elements, and converting the dom to an html string
Obviously #2 has performance issues and #1 requires messier code - and #2 still requires some additional code/messiness, but both are good options to be aware of.
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
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'm loading about 10MB worth of GLTF models into a three.js scene within React. As a user goes through the website experience, they hit the 3D model "scene", and my GLTF models are loaded. When they finish this scene, I do a little threejs cleanup, before I unmount the react component. However, if a user re-visits this scene - it goes through that whole loading process again.
Is there a way to preserve the loaded objects in the browser cache somehow so that even after the react component is unmounted, the loaded model data is accessible to subsequent visits / component mounts (even if it's only within the same browser session, before refresh, etc)?
Here's the model loading code:
loadGLB = loader => {
if (sceneConfig.modelFormat === 'glb') {
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('three/examples/js/libs/draco/');
loader.setDRACOLoader(dracoLoader);
}
};
const loader = new GLTFLoader().setPath(models.modelPath);
this.loadGLB(loader);
loader.load(`${models.modelToLoad}.${sceneConfig.modelFormat}`, glb => {
this.object = glb.scene;
...
});
and the cleanup:
componentWillUnmount() {
cancelAnimationFrame(this.frameId);
window.removeEventListener('resize', this.handleWindowResize);
this.container.removeChild(this.renderer.domElement);
this.renderer.forceContextLoss();
}
Re-initiating a new WebGL context several times on the same page has some performance drawbacks. Most importantly, each new context has to re-upload all textures and geometries to the GPU, which is a bottleneck that leads to frame staggering and flickering. Not to mention all the memory and GC overhead you'll accumulate by cleaning + rebuilding the renderer, shaders, geometry, textures, etc. This is anecdotal, but FireFox usually gets bogged down for me by the time it's created its 3rd or 4th WebGL context, and I've had mobile devices just reload the page altogether when memory consumption gets too high.
Have you tried using Portals so the <canvas> is outside of the React hierarchy? This way you can place the canvas wherever you'd like (in the background, for instance) and you can turn its visibility on/off without having to initiate a new WebGL context each time you want to show/hide it. You can simply stop the rendering and set display: none in CSS to hide the canvas, while still keeping everything in standby for when you need it again.
Something like this:
if (enabled) {
renderer.render(scene, cam);
renderer.domElement.style.display = "block";
} else {
// Not rendering when canvas is hidden
renderer.domElement.style.display = "none";
}
This is the approach we used on https://madeinhaus.com/ We have a single canvas in the background, and we only initiate it once. Then we show/hide it as needed without mounting and unmounting a new component each time.