this is my first post, im using Anychart library with React in a component build, i was able to implementing but the way that anychart recomend to use in every render the chart is duplicated.
This is my component
const Chart: React.FunctionComponent = () => {
function charts() {
// create data
const data = [
{
x: '2022-07-26',
y: '0.29822798232939185',
},
];
// create a chart and set the data
const chart = anychart.line();
chart.data(data);
// set the chart title
chart.title('Sales of the Most Popular Products of ACME Corp.');
// set the titles of the axes
chart.xAxis().title('Year');
chart.yAxis().title('Revenue');
// draw
chart.container('container');
chart.draw();
}
React.useEffect(() => {
charts();
}, []);
return <StyledDiv id="container" />;
};
export default Chart;
As you see, is very simple, but every time the app makes a render this component is duplicated and generate a new chart.
This problem can be solved by returning the chart itself in the charts() function.
function charts() {
//your code here
// draw
chart.container('container');
chart.draw();
//returning chart variable
return chart
}
Then chart.dispose() function must then be returned in a useEffect hook.
useEffect(() => {
const chart = charts();
// Return a cleanup function to remove the chart when the component unmounts
return () => chart.dispose();
}, []);
This problem appears due to React LifeCycle, and it can be solved by deleting duplicates when it has been rendered, which is what the dispose function does. You can use the example of the dispose function, which prevents duplicates in React.
Related
At the outset I'd like to say that I'm a React beginner and I'm stuck! And I've no idea how to proceed.
I'll be very happy if someone can either help me get my current code working or suggest an alternate approach.
So the most simple background is that I have a React app running on Electron framework to build a desktop application that reads in a JSON file and performs various tasks which includes displaying trees and graphs from the JSON data. To show the graph I have a multiple Menu components which allow the user to select what they want to see. The current state of the Menus are maintained by a component called the MainPane which uses a custom hook called useMenuState for maintaining the menu state (this hook uses only useState).
The values from useMenuState hook are passed by the MainPane as props to a component called the TreeViewer which uses another custom hook called useTreeState which uses a useReducer hook (to generate current tree using the information provided in the props). The custom hook returns the tree data and dispatcher function provided by the reducer hook.
To be able to show a different tree when user selects a new menu option, I have added a useEffect hook in the TreeViewer component which calls the dispatch function.
So I've added the values from the prop in the dependancy array of the useEffect function.
But the effect function is not running when the props change? As far as I can tell, all props being passed are created as new objects and no object state is mutated.
Here is a simple representation of my code (it has all the bits I talked about).
// CUSTOM HOOK useMenuState
const useMenuState = (props) => {
const [menu1State, updateMenu1] = useState();
const [menu2State, updateMenu2] = useState();
const handleMenuClick = (pMenuIdx, pClickedVal) = {
// generate and update respective menu state value using the jsonObj in props
};
let menuState = {
menu1: menu1State,
menu2: menu2State
};
return { menuState, menuClickHandler:handleMenuClick};
}
// COMPONENT MainPane
const MainPane = (props) => {
// props here gets a object that manages the data in the JSON file
const {menuState, menuClickHandler} = useMenuState({...props});
let treeViewerProps = {
treeType: menuState.menu1,
treeID: menuState.menu2,
jsonObj: props.jsonObj
};
return (<TreeViewer {...treeViewerProps} />);
}
// CUSTOM HOOK useTreeState
const useTreeState = (props) => {
const updaterFunction = (currentTreeState, action) => {
// update tree state as per action and return
}
let initTreeState = {} // build initial state of the tree
const [treeObj, treeUpdateDispatcher] = useReducer(updaterFunction, initTreeState);
let treeStateObj = { treeData: treeObj, treeUpdater: treeUpdateDispatcher };
return treeStateObj;
}
// COMPONENT TreeViewer
const TreeViewer = (props) => {
let treeStateProps = {
jsonObj: props.jsonObj,
treeType: props.treeType,
treeID: props.treeID
}
useEffect(()=>{
treeUpdater({type:'NEW TREE', id: props.treeID})
}, [props.treeType, props.treeID]); // look for changes in the tree type or ID and call the dispatch function
const { treeData, treeUpdater } = useTreeState(treeStateProps); // custom hook to handle tree State
return (<Tree data={treeData} />);
}
Why isn't the useEffect hook running when the props change?
Is there a better way to handle this?
Any pointers will be hugely appreciated!
Thanks in advance!
I am currently making a Graph component that fetches data from an API, parses the data to be used with a graph library, and then renders the graph. I have all of that working right now, but the issue I am having is with adding the ability to filter. The filtering I am currently doing is done by the parent of the Graph component, which will set the filters prop in the component which is then processed by a useEffect. But this seems causes some portions to re-render and I am trying to prevent. Below is what I have roughly speaking.
Rough example of Parent:
const Parent = (props) => {
const [filters, setFilters] = useState({});
//there are more state values than just this one also cause
//the same problem when their setState is called.
return (
<Graph filters={filters} />
<FilterComponent
onChange={(value) => setFilters(value)}
/>
)
}
export default Parent
Rough example of Child:
const Graph = (props) => {
const [nodes, setNodes] = useState({});
const [links, setLinks] = useState({});
const [velocity, setVelocity] = useState(0.08);
const createGraph = async () => {
//fetches the data, processes it and then returns it.
//not including this code as it isn't the problem
return {
nodes: nodes,
links: links,
};
}
//loads the graph data on mount
useEffect(() => {
const loadGraph = async () => {
const data = await createGraph();
setNodes(data.nodes);
setLinks(data.links);
};
loadGraph();
}, []);
//filters the graph on props change
useEffect(() => {
//this function uses setNodes/setLinks to update the graph data
filterGraph(props.filter);
}, [props.filters]);
return (
<ForceGraph2D
graphData={{
nodes: nodes,
links: links,
}}
d3VelocityDecay={velocity}
cooldownTicks={300}
onEngineStop={() => setVelocity(1)}
/>
);
}
export default Graph
My main issue is that whenever the FilterComponent updates, while I want it to update the graph data, this seems to re-render the Graph component. This causes the graph to start moving. This graph library creates a graph which kinda explodes out and then settles. The graph has a cooldown of 300, and after which it isn't supposed to move, which is where onEngineStop's function is called. But changing the filter state in Parent causes the graph to regain it's starting velocity and explode out again. I want to be able to change the filter state, update the graph data, without re-rendering it. I've looked into useMemo, but don't know if that's what I should do.
I'm fairly new to React having just started two weeks ago, so any help is greatly appreciated! Also, this is my first post on stackOverflow, so I apologize if I didn't follow some community standards.
Edit
I was asked to include the filterGraph function. The function actually was designed to handle different attributes to filter by. Each node/link has attributes attached to them like "weight" or "size". The filterComponent would then pass the attr and the value range to filter by. If a component falls outside that range it becomes transparent.
const Graph = (props) => {
...
//attr could be something like "weight"
//val could be something like [5,10]
const filterGraph = ({ attr, val }) => {
for (const [id, node] of Object.entries(nodes)) {
const value = nodes[id][attr];
if (val.length == 2) {
if (val[0] > value || val[1] < value) {
const color = nodes[id]["color"] || "#2d94adff";
nodes[id]["color"] = setOpacity(color, 0)
);
} else {
const color = nodes[id]["color"] || "#2d94adff";
nodes[id]["color"] = setOpacity(color, 1)
);
}
}
}
setNodes(Object.values(this.nodes));
}
...
}
In your example you mention that filterGraph uses setNodes/setLinks. So everytime the filter changes (props.filters) you will do 2 setState and 2 rerenders will be triggered. It can be that React will batch them so it will only be 1 rerender.
Depending on what filterGraph does exactly you could consider let it return filteredNodes en filteredLinks without putting the filterGraph in a useEffect.
Then pass the filteredNodes en filteredLinks to the graphData like graphData={{
nodes: filteredNodes,
links: filteredLinks,
}}
This way you won't trigger extra rerenders and the data will be filtered on every render. which is already triggered when the props.filters change. This is an interesting article about deriving state https://kentcdodds.com/blog/dont-sync-state-derive-it
Since you also mention that there are more state values in the parent you could make the component a pure component, which means it won't get rerendered when the parent renders but the props that are being passed don't change
https://reactjs.org/docs/react-api.html#reactmemo
Also it's better to include createGraph in the useEffect it's being used or wrap it in a useCallback so it won't be recreated every render.
const Graph = React.memo((props) => {
const [nodes, setNodes] = useState({});
const [links, setLinks] = useState({});
const [velocity, setVelocity] = useState(0.08);
//loads the graph data on mount
useEffect(() => {
const createGraph = async () => {
//fetches the data, processes it and then returns it.
//not including this code as it isn't the problem
return {
nodes: nodes,
links: links,
};
}
const loadGraph = async () => {
const data = await createGraph();
setNodes(data.nodes);
setLinks(data.links);
};
loadGraph();
}, []);
const { filteredNodes, filteredLinks } = filterGraph(props.filter)
return (
<ForceGraph2D
graphData={{
nodes: filteredNodes,
links: filteredLinks,
}}
d3VelocityDecay={velocity}
cooldownTicks={300}
onEngineStop={() => setVelocity(1)}
/>
);
})
export default Graph
Following the offical reference for Higher Level Component Factory to update props for a Control Component
The core APIs export other high-level component factories that can be
used in a similar way.
I've mimicked the example - but I get a syntax error for the following:
import L from "leaflet";
import "leaflet-routing-machine";
import { createControlComponent } from "#react-leaflet/core";
import 'leaflet-routing-machine/dist/leaflet-routing-machine.css'
function setWaypoints(props)
{
return {
waypoints: [
L.latLng(props.startLat, props.startLng),
L.latLng(props.endLat, props.endLng)
],
lineOptions: {
styles: [{ color: "#0500EE", weight: 4 }]
},
show: false,
addWaypoints: false,
routeWhileDragging: true,
draggableWaypoints: true,
fitSelectedRoutes: true,
showAlternatives: false,
createMarker: function() { return null; },
}
}
function createRoutingMachine(props, context)
{
const instance = new L.Routing.control(setWaypoints(props))
return
{
instance, context: { ...context, overlayContainer: instance }
}
}
function updateRoutingMachine(instance, props, prevProps)
{
if (props.endLat !== prevProps.endLat || props.endLng !== prevProps.endLng)
{
instance.setWaypoints(props)
}
}
const RoutingMachine = createControlComponent(createRoutingMachine, updateRoutingMachine)
export default RoutingMachine;
Missing semicolon. (35:25)
33 | return 34 | {
35 | instance, context: { ...context, overlayContainer: instance }
| ^ 36 | }
If I change this to:
function createRoutingMachine(props)
{
const instance = new L.Routing.control(setWaypoints(props))
return instance
}
The compiler is happy, but the component never updates.
I know I'm creating the Control Component incorrectly, but I can't find the information for the correct implementation.
Related:
How to use Leaflet Routing Machine with React-Leaflet 3?
How to extend TileLayer component in react-leaflet v3?
You will notice that in the docs, createcontrolcomponent lists only one argument, which is a function to create the instance. You are expecting it to behave like createlayercomponent, which takes two arguments. In createlayercomponent, the second argument is a function to update the layer component when the props change. However, createcontrolcomponent offers no such functionality. react-leaflet is assuming, much like vanilla leaflet, that once your control is added to the map, you won't need to alter it directly.
This gets a bit confusing in terms of leaflet-routing-machine, because you don't need to change the instance of the control, but rather you need to call a method on it which affects the map presentation.
IMO, the best way to go is to use a state variable to keep track of whether or not your waypoints have changed, and use a ref to access the underlying leaflet instance of the routing machine, and call setWayPoints on that:
// RoutineMachine.jsx
const createRoutineMachineLayer = (props) => {
const { waypoints } = props;
const instance = L.Routing.control({
waypoints,
...otherOptions
});
return instance;
};
// Takes only 1 argument:
const RoutingMachine = createControlComponent(createRoutineMachineLayer);
// Map.jsx
const Map = (props) => {
// create a ref
const rMachine = useRef();
// create some state variable, any state variable, to track changes
const [points, setPoints] = useState(true);
const pointsToUse = points ? points1 : points2;
// useEffect which responds to changes in waypoints state variable
useEffect(() => {
if (rMachine.current) {
rMachine.current.setWaypoints(pointsToUse);
}
}, [pointsToUse, rMachine]);
return (
<MapContainer {...props}>
<RoutineMachine ref={rMachine} waypoints={pointsToUse} />
<button onClick={() => setPoints(!points)}>
Toggle Points State and Props
</button>
</MapContainer>
);
};
Working Codesandbox
Bonus: a cheap and easy way to force a rerender on your <RoutineMachine> component (or any react component) is to assign it a key prop, and change that key prop when you want to rerender it. This might be a uuid, or even a unique set of waypoints ran through JSON.stringify. Just an idea.
I am writing a react application in which I need to set the zoom level of a particular page to 90%. I know that I can do it using document.body.style.zoom = '90%' as given below:
useEffect(() => {
document.body.style.zoom = "90%";
}, []);
It's a basic componentDidMount function in react using useEffect. But the problem is how to set the zoom level back to the default as it was before loading the page using componentDidUnmount?
Add cleanup effect as callback returned from useEffect.
The clean-up function runs before the component is removed from the UI to prevent memory leaks.
const Zoom = () => {
useEffect(() => {
const initialValue = document.body.style.zoom;
// Change zoom level on mount
document.body.style.zoom = "150%";
return () => {
// Restore default value
document.body.style.zoom = initialValue;
};
}, []);
return <></>;
};
Such callback acts like componentWillUnmount.
I'm using ZingChart to visually represent data, but it seems that the chart is not updating when state changes.
My chart config is very simple:
//in constructor()
const { data } = props;
this.state = {
chartConfig: {
type: 'area',
series: [
{values: this.updateValues(data)}
]
}
};
updateValues() returns the values that will be displayed, it works fine.
Now when the parent component changes, it send some new props which I will use to set a new state with :
componentDidUpdate(prevProps) {
if (this.props.data !== prevProps.data) {
const chartConfig = { ...this.state.chartConfig };
chartConfig.series[0].values = this.updateValues(this.props.data);
this.setState({ chartConfig });
console.log(this.state.chartConfig); //<--- state successfully updated
}
}
And this is the render method, I first check if state has been updated (since setState is async) and it seems okay.
render() {
return (
<React.Fragment>
{this.state.chartConfig.series[0].values.length} {/* <--- state here is also up to date */}
<ZingChart data={this.state.chartConfig}/> {/* but not the chart */}
</React.Fragment>
);
}
The problem now is that the chart is not directly updating when state changes, (until I force refresh), so what may be causing this issue? They say in the blog that the chart is reactive to component changes, but it does not seem the case.
Thanks!
#adxl In your code, the componentDidUpdate method contains a spread of this.state.config.
const chartConfig = { ...this.state.chartConfig };
Since this actually maintains a shallow reference to its contents, modifying the values array is passed by reference. If you were to deep clone the config object, then it would work.
const chartConfig = Object.assign({}, this.state.chartConfig);
MDN Docs here -> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign