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.
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
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 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
I've just started using Recoil on a new project and I'm not sure if there is a better way to accomplish this.
My app is an interface to basically edit a JSON file containing an array of objects. It reads the file in, groups the objects based on a specific property into tabs, and then a user can navigate the tabs, see the few hundred values per tab, make changes and then save the changes.
I'm using recoil because it allows me to access the state of each input from anywhere in my app, which makes saving much easier - in theory...
In order to generate State for each object in the JSON file, I've created an component that returns null and I map over the initial array, create the component, which creates Recoil state using an AtomFamily, and then also saves the ID to another piece of Recoil state so I can keep a list of everything.
Question 1 Is these a better way to do this? The null component doesn't feel right, but storing the whole array in a single piece of state causes a re-render of everything on every keypress.
To Save the data, I have a button which calls a function. That function just needs to get the ID's, loop through them, get the state of each one, and push them into an Array. I've done this with a Selector too, but the issue is that I can't call getRecoilValue from a function because of the Rules of Hooks - but if I make the value available to the parent component, it again slows everything right down.
Question 2 I'm pretty sure I'm missing the right way to think about storing state and using hooks, but I haven't found any samples for this particular use case - needing to generate the state up front, and then accessing it all again on Save. Any guidance?
Question 1
Get accustomed to null-rendering components, you almost can't avoid them with Recoil and, more in general, this hooks-first React world 😉
About the useRecoilValue inside a function: you're right, you should leverage useRecoilCallback for that kind of task. With useRecoilCallback you have a central point where you can get and set whatever you want at once. Take a look at this working CodeSandbox where I tried to replicate (the most minimal way) your use-case. The SaveData component (a dedicated component is not necessary, you could just expose the Recoil callback without creating an ad-hoc component) is the following
const SaveData = () => {
const saveData = useRecoilCallback(({ snapshot }) => async () => {
const ids = await snapshot.getPromise(carIds);
for (const carId of ids) {
const car = await snapshot.getPromise(cars(carId));
const carIndex = db.findIndex(({ id }) => id === carId);
db[carIndex] = car;
}
console.log("Data saved, new `db` is");
console.log(JSON.stringify(db, null, 2));
});
return <button onClick={saveData}>Save data</button>;
};
as you can see:
it retrieves all the ids through const ids = await snapshot.getPromise(carIds);
it uses the ids to retrieve all the cars from the atom family const car = await snapshot.getPromise(cars(carId));
All of that in a central point, without hooks and without subscribing the component to atoms updates.
Question 2
There are a few approaches for your use case:
creating empty atoms when the app starts, updating them, and saving them in the end. It's what my CodeSandbox does
doing the same but initializing the atoms through RecoilRoot' initialState prop
being updated by Recoil about every atom change. This is possible with useRecoilTransactionObserver but please, note that it's currently marked as unstable. A new way to do the same will be available soon (I guess) but at the moment it's the only solution
The latter is the "smarter" approach but it really depends on your use case, it's up to you to think if you really want to update the JSON at every atom' update 😉
I hope it helps, let me know if I missed something 😊
I'm trying to create a stepper form
I store my steps in an array of json with a proprety component ({typeOfComponent, component, key})
It works wells, but:
Everytime i slice my array, like when i move up/down a step or add a new step between two steps.
I lose the states inside my component.
I tried to use memo, i don't understand why it's only when an item position my composent is recreate. Is it possible like a pointer in C to store only his "adress"
the code sandbox exemple =>
https://codesandbox.io/s/infallible-maxwell-zkwbm?file=/src/App.js
In my real projet, the button ADD is a button for chosing the new step type
Is there any solution for manipulates my steps without losing the user data inside ?
Thanks for your help
React is re-mounting the components inside of this every re-render probably due to a variety of reasons. I couldn't get it to work as is, but by lifting the state up from your components, it will work.
You'd likely need to lift the state up anyway because the data isn't where you need it to be to make any use of your form when the user is done with it.
In order to lift the state up, I added the current value to the steps array:
function addNext(step, index) {
componentKey++;
setSteps(prevState => {
let newState = [...prevState];
step = 1;
newState.splice(index + 1, 0, {
stepNumber: step,
component: getStepContent(step, componentKey),
value: getDefaultValue(step),
key: componentKey
});
return newState;
});
}
I also made sure your getStepContent just returned the component rather than a node so you can render it like this:
<step.component
value={step.value}
onChange={handleChange}
data-index={i}
/>
There are definitely a lot of ways to optimize this if you start running into performance issues, of course.
https://codesandbox.io/s/beautiful-river-2jltr?file=/src/App.js