How to prevent component re-rendering, when i have an hierarchical array of objects (array of objects) using redux dispatching and memo
for example i have like this list of list of components
like
Elements =[ // an array in store.state variable
{name='a',
subEelemets:[
{name:'a-a',
components:[
{
name:'a-a-1',
component:'TextFiled',
value:'default value...'
},
]
},
]
},
]
i used redux to update components values inside my Elements array using dispatch but the performance downgrades because of many re-rendering,
i tried with memo to prevent re rendering but it also updates the entire Elements array because i used useSelector,
const ImportedComponent = memo((props) => {
const LoadableComponent = loadable(() =>
import("../lib" + props.compName))
);
return (
<LoadableComponent
{...props}
id={props.id}
/>)},(a,b)=>a.id===b.id?true:false)
here the code how i redner my compoents
const selectTodoIds = state => state.components.map((todo,index) => todo)
const updateComp = () => {
const Components = useSelector(selectCompsIds, shallowEqual)
const renderedListItems = Components.map((CompId) =>
<ImportedComponent key={CompId} elementID={CompId} />
)
return <ul className="todo-list">{renderedListItems}</ul>
}
it works when i used a flat array as presented in the tutorial https://redux.js.org/tutorials/fundamentals/part-5-ui-react but not for hierarchical one, because i should use the "name" to define which subelement i'm updating,
is there any strategy to solve this problem?
ps: the code is just for clarification,
Rather than memo you could try useMemo:
const ImportedComponent = (props) => {
const LoadableComponent = useMemo(() => {
return loadable(() => import("../lib" + props.compName));
},[props.compName]);
...
memo only helps when there's lots of re-renders with the same props, which it doesn't appear you have.
That's a complete shot in the dark, though. Can you post a link to a stackblitz, maybe? It'd be nice to see more of your code in context.
Related
Is it legit to store data outside of a react component and change it from inside the component and derive state from it? This could help when updating complex deep-nested states.
import React from "react";
let globalState = {
foo: { bar: { baz: { value: 0 } } },
};
const Component = () => {
const [state, setState] = React.useState(globalState);
let baz = globalState.foo.bar.baz;
const changeValue = () => {
baz.value += 1;
setState({ ...globalState });
};
return (
<div>
<label>{state.foo.bar.baz.value}</label>
<button onClick={changeValue}>change</button>
</div>
);
};
Update:
My main intention of this aproach is to get a cleaner way of updating nested state properties. In my current component I use immer-produce to update nested values of the state, but even with produce it becomes extremely nasty when coming to nested state with arrays, indices etc.
This is an actual code snippet from my application:
const changeAction = useCallback((newAttrs) => {
setState(
produce((draft) => {
draft.newOrders[draft.selectedOrderIndex].nodes[
draft.selectedNodeIndex
].actions[draft.selectedActionIndex] = {
...draft.newOrders[draft.selectedOrderIndex].nodes[draft.selectedNodeIndex]
.actions[draft.selectedActionIndex],
...newAttrs,
};
})
);
}, [setState]);
Is there any other aproach to clean this up?
This sounds like something that would ideally be handled with the use of Context.
Using context can help you manage the state globally rather than mutating objects and passing them up and down the tree all the time, making them difficult to manage.
Component diagram:
"Main"
|--"Side"--"CategoryPicker"
|
|--"ItemBoard"
categoryPicker gets the chosen value.
const filterResultHandler = (e) => {
props.onFilterChange(e.target.value);}
...
onChange={filterResultHandler}
And lift up the value to Side.
const [filterState, setFilterState] = useState("all");
const onFilterChangeHandler = () => { props.onPassData(setFilterState);};
...
<CategoryPicker selected={filterState} onFilterChange={onFilterChangeHandler} />
Then I repeat to lift value to the Main.
(Up to this point I have console.log the value and it seemed OK.)
const [recData, setRecData] = useState("all");
const onFilterChangeHandler = (passedData) => {
setRecData(passedData);};
<Side onPassData={onFilterChangeHandler} selected={recData} />
Then pass it down to Itemboard as a prop.
<ItemBoard items={items} recData={recData} />
In ItemBoard I am trying to filter the array then compare to later map it and display filtered components.
const filteredProducts = props.items.filter((product) => {
return (product.cat.toString() === props.recData)})
{filteredProducts.map((product, index) => (
<Item cat={product.cat} />
))}
Warning: Cannot update a component (Side) while rendering a different component (Main). To locate the bad setState() call inside Main
Where am I loosing my logic?
PS.
Trying to focus on understanding how lifting up and passing props works, not looking for better solutions to the problem right now.
you have the bad setState use in this code:
const [recData, setRecData] = useState("all");
const onFilterChangeHandler = (passedData) => {
setRecData(passedData);};
<Side onPassData={onFilterChangeHandler} selected={recData} />
why you are passing this selected={recData} data again to Side component, you are updating the state of Main component from Side component and passing the selected={recData} again, remove this and try again
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
I was playing around with react-dev-tools chrome extension and found out that all my components are re-rendering.
App.js
import React from 'react';
import './App.css';
import Header from './components/molecules/Header/Header';
// import { colorListGenerator } from './core-utils/helpers';
import ColorPalette from './components/organisms/ColorPalette/ColorPalette';
export const colorListGenerator = (n) => {
let colorArray = []
for(let i=0; i<n; i++) {
let randomColor = '#'+Math.floor(Math.random()*16777215).toString(16);
let id="id" + Math.random().toString(16).slice(2)
console.log(typeof(id), id)
let color = {
id: id,
hex: randomColor
}
colorArray.push(color);
}
return colorArray
}
const App = () => {
const colors=colorListGenerator(10);
return (
<div className="App">
<Header/>
<ColorPalette colorPalette={colors} />
</div>
);
}
export default App;
ColorPalette.js
/* eslint-disable eqeqeq */
import React from 'react';
import Color from '../../atoms/Color';
import './ColorPalette.css';
const ColorPalette = ({ colorPalette }) => {
const [colors, setColors] = React.useState(colorPalette);
// const handleColorClick = (event) => {
// const id = event.currentTarget.getAttribute('id')
// const index = colors.findIndex(item => item.id == id);
// setColors(colors.filter(item => item.id != id))
// }
const deleteItem = (id) => {
setColors(colors.filter(item => item.id != id))
}
return (
<div className={'colorPalette'}>
{colors && colors.map((color, index) => {
// const key = index
const key = color.id
return <Color
key={key}
color={color.hex}
colorKey={key}
handleColorClick = {() => {deleteItem(color.id)}}
/> })}
</div>
)
}
// export default React.memo(ColorPalette);
export default ColorPalette;
Color.js
import React from 'react';
import './Color.css';
import deleteIcon from '../../../delete-icon.png'
const Color = ({ color, colorKey, handleColorClick }) => {
return (
<div className="color"
style={{ backgroundColor: color }}
// no need to mention key here
// key={colorKey}
id={colorKey}
onClick={handleColorClick} >
<p> {colorKey} </p>
<img src={deleteIcon}
alt={'delete'}
className="delete"
/>
</div>
)
}
// export default React.memo(Color);
export default Color;
When I use the profiler to check why all my 'Color' components have re-rendered after deleting a single item, it complains that handleColorClick prop has changed. I changed the deleteItem to handleColorClick which isn't an arrow function, but the result is the same. I'm also passing unique ids. Interestingly, when I pass const key = Math.random() instead of const key = color.id my Color components are not rerendering. So it has something to do with the keys. I want to understand why my components are rerendering when I pass unique ids as keys.
The only way a React functional component will be prevented from rerendering is by using React.memo to memoize the component. Memoization here means that if the component's props do not change - they are strictly equivalent to each other using the === operator - then the component's last render output will be re-used instead of rerendering the entire component.
However, React.memo itself gets tricky when you're talking about props that are object or functions - values for which the strict === comparison checks referential equality. That means that for functions like deleteItem need to use something like React.useCallback to memoize the references themselves so that they themselves do not change between renders, which will trip up React.memo and lead to rerenders in situations where intuitively it seems like it shouldn't.
As you can see, it quickly starts to get quite complicated, as you try to keep track of memoizing your functions, your objects, your components, etc.
And really, what's the point?
The performance gains you get from memoization - if they even materialize - are miniscule. This is a classic case of premature optimization, sometimes called the "root of all evil" because of what an unnecessary time sink it is, for little to no gain, and the cost of added complexity.
React itself in its optimized production build is insanely fast, good at resolving diffs, and in most cases could rerender your entire app dozens of times per second without any perceivable slowdown. You should ONLY start optimizing your app with things like memoization when you have ACTUAL, MEASURABLE impacts to performance that you need to address.
In short, you do not need to worry about "unnecessary" rerenders.
I'll say it again for emphasis:
DO NOT WORRY ABOUT "UNNECESSARY" RERENDERS.
Seriously.
PS: The reason using a random value for key makes it seem like unnecessary rerenders are eliminated is because every time a component renders it is literally a brand new instance of that component, not the same component being rerendered. React uses the key prop under the hood to track which component is which between renders. If that value is unreliable, it means that React is literally rendering NEW components every time. You're basically destroying all the old components and recreating them from scratch, albeit with the same props or whatever, but make no mistake, they are NOT the same components between renders. (Even their internal state including hooks will be erased)
As per what you said handleColorClick prop has changed, which is why the components are getting re-rendered. Since you are using functional component and hooks in the component, when the component is getting re-rendered the function handleColorClick is redefined again and the reference is getting changed. That's the reason why the components are getting re-rendered even though you pass unique ids as keys.
In order to avoid that you can use useCallback hook which will help you not to get a new function reference unless there's a change in the dependencies provided to the useCallback hook
/* eslint-disable eqeqeq */
import React, {useCallback} from 'react';
import Color from '../../atoms/Color';
import './ColorPalette.css';
const ColorPalette = ({ colorPalette }) => {
const [colors, setColors] = React.useState(colorPalette);
// const handleColorClick = (event) => {
// const id = event.currentTarget.getAttribute('id')
// const index = colors.findIndex(item => item.id == id);
// setColors(colors.filter(item => item.id != id))
// }
const deleteItem = useCallback((id) => {
setColors(colors.filter(item => item.id != id))
}, [])
return (
<div className={'colorPalette'}>
{colors && colors.map((color, index) => {
// const key = index
const key = color.id
return <Color
key={key}
color={color.hex}
colorKey={key}
handleColorClick = {() => {deleteItem(color.id)}}
/> })}
</div>
)
}
// export default React.memo(ColorPalette);
export default ColorPalette;
I have React component. This components take 'units' - (array of objects) prop. Based on that I render component for each of item. I want to sort my components based on 'price' value, which is one of state items property. But when i trigger the sorting - state changes correctly but my components order not changing.
const SearchBoxes = ({units}) => {
const [unitsState, setUnitsState] = useState([])
useEffect(() => {
setUnitsState(units)
}, [units])
const sortByPrice = () => {
const sortedUnits = sort(unitsState).desc(u => u.price); // sorting is correct
setUnitsState(sortedUnits) // state is changing correctly
}
return (
<div>
{unitsState.map((u,i) => {
return <UnitBox key={u.price} unit={u} />
})}
</div>
)
}
Can somebody help me, please ?
Why my components order do not changing when the state is changing after sort triggering ?
You aren't calling sortByPrice anywhere--all you've done is to define the function. I haven't tried it, but what if you changed useEffect to:
useEffect(() => {
setUnitsState(sort(unitsState).desc(u => u.price));
}, [units])
Then you don't need the sort method at all.