I have a function which basically generates dynamic dom as below
const arrMarkup = [];
const getMarkup = () => {
{
if(true){
arrMarkup.push(<Accordion expanded={expanded === cust.name} onChange={handleChange(cust.name)}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1bh-content"
id="panel1bh-header"
>
<Typography className={salesEvent.name && classes[salesEvent.name]}></Typography>
</AccordionSummary>
</Accordion>
)
})
})
}
return <div>{arrMarkup}</div> ;
}
Now, i am trying to execute this function is on useEffect as below
useEffect(() => {
getMarkup();
}, [cust]);
and trying to add in return of JSX as
return (
<div>
{arrMarkup}
</div>
)
but can not see the markup added, however i can see its added in array arrMarkup. What's wrong?
React only re-renders your component when its state or props change. As far as one can tell from your question, arrMarkup isn't either a state member or a prop. (If it were, directly modifying it would be against the React rules, because you must not directly modify state or props.)
It's hard to tell you what to do without more information, but you probably want arrMarkup to be a state member, for instance (in a functional component using hooks):
const [arrMarkup, setArrMarkup] = useState([]);
and then to update it appropriately, for instance:
setArrMarkup(current => [...current, <NewStuff>goes here</NewStuff>]);
Note that I used the callback version of the state setter. That's important when you're updating state based on existing state, since state updates may be asynchronous and can be batched (so the state information you already have can be stale).
FWIW, a couple of other observations:
It's unusual to have the useEffect dependency be cust (a single object as far as I can tell) and have triggering the effect add an entry to an array that has previous entries for previous values of cust which (apparently) you're no longer storing in the component's state anywhere. That just feels very off, without more context.
You haven't shown the definition of handleChange, but onChange={handleChange(cust.name)} looks like it's probably incorrect. It calls handleChange, passing in cust.name, and the uses its return value os the change handler. Did you mean onChange={() => handleChange(cust.name)}, so that handleChange is called when the event occurs?
Related
I have a react app that renders a list of cards in a container widget. The container widget has a useEffect where we subscribe to an observable and update the array that is then used to render the cards inside the component. Each time any of the cards change, the observable emits new values resulting in creating of the array all over again and thus all the cards are re-rendered. However this re-render causes a noticeable UI lag.
Here is the stripped down version of code from the container component.
const obsRef = useRef<Subscription>(null);
const [apiResArray, setApiResArray] = useState([]);
useEffect(() => {
if(obsRef.current) obsRef.current.unsubscribe();
const mySubscription = myObservable$(changingValue)
.subscribe((val) => {
setApiResArray(val.apiArray);
});
obsRef.current = mySubscription;
return () => {
obsRef.current && obsRef.current.unsubscribe();
};
}, [changingValue])
return (
<CardWrapper $showMenu={showMenu}>
{ apiResArray.map((res, resIndex) => {
return (
<Card
data={{
// a json object with props
}}
key={res?.hKey}
/>
);
})}
</CardWrapper>
);
The Card component here simply renders the content based on props passed to it. I know that since data is an Object, referential equality may fail and I have tried memoizing the component but even that does not help.
There is a lot more to these components but posting all the code won't make sense. I wish to understand what possibly might be causing the list re-render to be such a heavy operation that the whole UI gets stuck for a second or two. The array contains around only 100 objects or so. It happens whenever changingValue changes. I can share more information as required.
Any suggestions on improving the performance are highly appreciated.
React will mount and unmount the component in DEV mode to validate effect cleaner and any side effect.
I'm concerned about that subscribe and unsubscribe in your effect.
If you want changingValue to be defined why not just wrap it inside an if statement ?
if (changingValue) {
const mySubscription = myObservable$(changingValue)
.subscribe((val) => {
setApiResArray(val.apiArray);
});
obsRef.current = mySubscription;
}
Another observation is at render of items
<Card
data={
{
// a json object with props
}
}
key={res?.hKey} // Why is this a falsy value ?
/>
Falsy value can make key prop change those re-rendering the component, one with React internal key value and another with your implementation value
I have an array with thousands of strings and is passed to a component:
Main component:
const array = ['name1', 'name2', 'name3'];
const [names, setNames] = useState(array);
const onClick = (index) => {
setNames(names.map((name, i) => {
if (i === index) {
return 'name changed';
}
};
};
return (
<ul>
{array.map((name, index) => (
<li key={index}>
<ShowName name={name} key={index} onClick={() => onClick(index)} />
</li>
)}
</ul>
);
ShowName component:
let a = 0;
export default function ShowName({ name, onClick }) {
a += 1;
console.log(a);
return (
<button type="button" onClick={onClick}>{name}</button>
);
}
There's also a button which changes a name randomly. But whenever the button is pressed, all the ShowName components are rerendering. I've been trying to use useCallback and useMemo, but the components are still rerendering x times (x is the length of the array).
const ShowNameHoc = ({ name }) => {
return <ShowName name={name} />
};
return (
<div>
{array.map((name, index) => <ShowNameHoc name={name} key={index} />)}
</div>
);
What should I do if I only want to rerender the component with a change?
You have a misunderstanding in the concepts here. The default is for React to call render on all children, regardless of whether the props changed or not.
After that happened, React will compare that new Virtual DOM to the current Virtual DOM and then only update those parts of the real DOM that changed.
That's why the code in a render method should be quick to execute.
This behavior can be changed by using features like useMemo, PureComponents or shouldComponentUpdate.
References:
https://reactjs.org/docs/rendering-elements.html (Bottom):
Even though we create an element describing the whole UI tree on every tick, only the text node whose contents have changed gets updated by React DOM.
https://reactjs.org/docs/optimizing-performance.html#avoid-reconciliation
Even though React only updates the changed DOM nodes, re-rendering still takes some time. In many cases it’s not a problem, but if the slowdown is noticeable, you can speed all of this up by overriding the lifecycle function shouldComponentUpdate, which is triggered before the re-rendering process starts.
...
In most cases, instead of writing shouldComponentUpdate() by hand, you can inherit from React.PureComponent. It is equivalent to implementing shouldComponentUpdate() with a shallow comparison of current and previous props and state.
Also, read this for some more background info: https://dev.to/teo_garcia/understanding-rendering-in-react-i5i
Some more detail:
Rendering in the broader sense in React means this (simplified):
Update existing component instances with the new props where feasible (this is where the key for lists is important) or create a new instance.
Calling render / the function representing the component if shouldComponentUpdate returns true
Syncing the changes to the real DOM
This gives you these optimization possibilities:
Ensure you are reusing instances instead of creating new ones, e.g. by using a proper key when rendering lists. Why? New instances always result in the old DOM node to be removed from the real DOM and a new one to be added. Even when unchanged. Reusing an instance will only update the real DOM if necessary. Please note: This has no effect on whether or not render is being called on your component.
Make sure your render method doesn't do heavy lifting or if it does, memoize those results
Use PureComponents or shouldComponentUpdate to prevent the call to render altogether in scenarios where props didn't change
Answering your specific question:
To actually prevent your ShowName component from being rendered - into the Virtual DOM - if their props changed, you need to perform the following changes:
Use React.memo on your ShowName component:
function ShowName({ name, onClick }) {
return (
<button type="button" onClick={onClick}>{name}</button>
);
}
export default memo(ShowName);
Make sure the props are actually unchanged by not passing a new callback to onClick on each render of the parent. onClick={() => onClick(index)} creates a new anonymous function every time the parent is being rendered.
It's a bit tricky to prevent that, because you want to pass the index to this onClick function. A simple solution is to create another component that is passed the onClick with the parameter and the index and then internally uses useCallback to construct a stable callback. This only makes sense though, when rendering your ShowName is an expensive operation.
That is happening because you are not using the key prop on <ShowName/> component.
https://reactjs.org/docs/lists-and-keys.html
it could look something like this
return (
<div>
{array.map(name => <ShowName key={name} name={name} />)}
</div>
);
Let's define a Tags component (a fancy checkbox group).
const Tags = ({ tags, selectedIds, onSelectionChange }) => {
const createClickHandler = (id) => () => {
const newSelectedIds = xor(selectedIds, [id]);
const selectedTags = newSelectedIds.map((id) =>
tags.find((tag) => tag.id === id)
);
onSelectionChange(selectedTags);
};
const isSelected = (id) => selectedIds.includes(id);
return (
<div>
{tags.map(({ id, text }) => (
<button
key={id}
type="button"
style={{ backgroundColor: isSelected(id) ? "gray" : "white" }}
onClick={createClickHandler(id)}
>
{text}
</button>
))}
</div>
);
};
This allows us to consume it like this:
export default function App() {
const tags = someUsers.map((user) => ({
id: user.id,
text: user.name,
value: user
}));
const [selectedTags, setSelectedTags] = useState([]);
const selectedIds = selectedTags.map((tag) => tag.id);
return (
<div>
<Tags
tags={tags}
selectedIds={selectedIds}
onSelectionChange={setSelectedTags}
/>
</div>
);
}
You can test this in https://codesandbox.io/s/musing-goldwasser-nmm13
I believe this is a decent design of a component and its props (the main focus is on the ease of consuming for the other components). We could perhaps remove selectedIds and add a selected flag in the tags prop, however this is beyond the question scope.
My colleague on the other hand insists that this can lead to bugs and should be avoided.
The reasoning is as follows: if we want to update the state we must use appropriate API - setState(oldState => //data manipulation to produce new state) from useState (https://reactjs.org/docs/hooks-reference.html#functional-updates)
Since the parent passes the state directly to the children we can't be sure that the child component filters data based on the latest data. Basically, this issue: https://reactjs.org/docs/faq-state.html#why-is-setstate-giving-me-the-wrong-value
His implementation would be something along these lines:
const Tags = ({ tags, selectedIds, onTagClick }) => {
const isSelected = (id) => selectedIds.includes(id);
return (
<div>
{tags.map(({ id, text }) => (
<button
key={id}
type="button"
style={{ backgroundColor: isSelected(id) ? "gray" : "white" }}
onClick={() => onTagClick(id)}
>
{text}
</button>
))}
</div>
);
};
In this case, we lift the whole filtering to a parent component
const handleTagClick = (id) =>
setSelectedTagsIds((oldIds) => {
if (oldIds.includes(id)) return oldIds.filter((oldId) => oldId !== id);
return [...oldIds, id];
});
You can test this in: https://codesandbox.io/s/kind-cdn-j7cg3
or another version:
const Tags = ({ tags, selectedIds, setSelectedIds }) => {
const isSelected = (id) => selectedIds.includes(id);
const handleTagClick = (id) =>
setSelectedIds((oldIds) => {
if (oldIds.includes(id)) return oldIds.filter((oldId) => oldId !== id);
return [...oldIds, id];
});
return (
<div>
{tags.map(({ id, text }) => (
<button
key={id}
type="button"
style={{ backgroundColor: isSelected(id) ? "gray" : "white" }}
onClick={() => handleTagClick(id)}
>
{text}
</button>
))}
</div>
);
};
in this case, we leave the filtering to the Tags component however we pass the function which allows modification of state based on old state.
You can test this code in https://codesandbox.io/s/relaxed-leftpad-y13wo
In my opinion, this case is a completely different scenario that React docs never specifically address.
As far as I understand React rendering engine will always ensure that the child nodes get the newest props so a situation where a child component filters (or does other manipulation) with stale data is simply impossible. I would like to quote some docs for this however I haven't found any information on this specific situation.
All I know is:
with my many years of React experience I have yet to encounter any bugs with my approach
other 3rd party libraries use the same design
Can someone (with deep React knowledge) provide more insight why I am correct or wrong in this instance?
For you to notice the difference, you could simulate a delay in the update of the selection. i.e., the user of your component needs to do some async stuff when selecting a tag
const [selectedTags, setSelectedTags] = useState([]);
const selectedIds = selectedTags.map((tag) => tag.id);
const asyncSelection = (tags) => {
setTimeout(() => setSelectedTags(tags), 1000);
};
...
<Tags
tags={tags}
selectedIds={selectedIds}
onSelectionChange={asyncSelection}
/>
You can try here clicking each option one by one, and when all updates run, not all options will be selected (which is not expected). Since the component didn't render immediately, the handler was not updated and the second click is executed with an old state, therefore, the sequences of the selections are not correctly synced. Of course, this is a contrived example, but it could be the case in a very heavy UI that 2 clicks happen without the Tags component being rerendered.
On the other hand, letting the user have more control over the state would be possible to handle this situation. Once again, if you try here clicking each option one by one, in the end, all will be selected as expected
const handleTagClick = (id) => {
setTimeout(() => {
setSelectedTagsIds((oldIds) => {
if (oldIds.includes(id)) return oldIds.filter((oldId) => oldId !== id);
return [...oldIds, id];
});
}, 1000);
};
As far as I can see the things you're talking about are two different issues.
If the props of a child are updated it will trigger a rerender of that component. There are edge cases where that gets tricky like with useRef or some callbacks but that's besides the point. The filtering and things you're doing will never be different or affected in any way as long as it's dependent on the props changing and if the component receives new props it will rerender the child and reapply the filters without any issues.
The second issue is sort of different from the first one. What could happen is that the tag state is repeatedly updated and only one of those states are passed to the child, that's what you want to avoid. Essentially you have to make sure the parent state has actually updated correctly before passing it to a child. The child will always update and filter and do everything correctly exactly on what's passed to it, your problem here is making sure you're actually passing the correct props.
There's no need to move anything to the parent component, the child will update itself correctly when the parent tag state updates and passes that new state to the child, the only thing you have to look out for here is that you don't update the parent state multiple times and cause https://reactjs.org/docs/faq-state.html#why-is-setstate-giving-me-the-wrong-value and end up passing the wrong props to the child. For example if someone spams the group checkbox on and off quickly. Even then if you pass the wrong props to the child the child will still update itself and reapply the filtering and everything, just on the wrong props.
React will do its batch state update on something like a 10ms interval (I'm not exactly sure how long it is). So if someone clicks the checkbox and it updates the tag state at 6/10ms it will rerender the component 4ms later when it does the batch state update. If hypothetically during those 4ms you click it off again, or if straight after it updated you click it off again, it's where weird things start happening. This is why if you use the increment counter (like in that example) multiple times it won't actually increase it by 3, only by 1, since the code will execute all 3 times on 0 before it did the state update 10ms later. That being said even if you spam that checkbox on and off all the time (spamming the tag array state), I don't see any way how it would go out of sync, every 10ms it will update and rerender the child and the moment you stop spamming it the child will finally rerender on the last current parent state and be correct. I don't see how you could really have an issue with that in your example. It could cause an issue with something like a counter but not with your tags because of the fact that a counter is a cumulative addition on previous values whereas your tags is a static set of values (that is the key difference).
I'm having an array data.info that is being updated over time and I'm trying to replace placeholder rendered elements with another. So by default app.js looks like this
return (
<Fragment>
{data.info.map((index) => {
return <Typography key={index} variant="h6" className={classes.title}>Demo</Typography>
})}
</Fragment>
)
Also I have a hook with async function to subscribed to data.info.length.
useEffect(
() => {
if (!initialRender.current) {
if (data.info.length!==0) {
for (let i = data.info.length-iScrollAmount+1 ; i < data.info.length+1; i++) {
firstAsync(i)
}
}
} else {
initialRender.current = false
}
},
[data.info.length]
)
async function firstAsync(id) {
let promise = new Promise(() => {
setTimeout(() => console.log(document.getElementById(id)), 500)
});
}
With document.getElementById() and id I can get to every element that was rendered and change it. And here goes the problems.
I'm using material-ui so I can't get to <Typography/> because it is transformed into <h6/>. Probably that is not a problem since I need to replace contents, so I can find parent element and remove all children. Is that way correct?
After I delete children how do I add content using jsx? What I mean is that in async function I'll get an array that I want to use in new element <NewCard/> to dynamically put into <Fragment/>. Yet I did not find any example how to do that.
It is not a good practice to change DOM Nodes directly in React, and you need to let React do the rendering for you and you just tell react what to do.
in your case you need to define a React State for your data and set your state inside your firstAsync function and then use your state to render whatever html element or React component which you want
React does not encourage the practice of manipulating the HTML DOM nodes directly.
Basically you need to see 2 things.
State which is a special variable whose value is retained on subsequent refresh. Change in reference in this variable will trigger component and its children a refresh/re-render.
Props which is passed to every Component and is read only. Changing in props causes refresh of component by default.
In your example, based on data.info you want to render Typography component.
Solution
First thing is your map function is incorrect. First parameter of map function is item of list and second is index. If you are not sure if info will always be present in data, you may want to have a null check as well.
{(data.info || []).map((info, index) => {
return <Typography key={index} variant="h6" className={classes.title}>{info.text}</Typography>
})}
You should be passing info from map to Typography component. Or use info value in content of Typography as shown above.
Update data.info and Typography will update automatically. For this, please make sure, data.info is a component state and not a plain variable. Something like
const [data, setData] = React.useState({});
And when you have value of data (assuming from API), then
setData(responseApi);
Today I spent quite a bit of time debugging an issue with state resetting itself in child components on every re-render. After eventually solving it I realized that I don't fully understand how React functional components and Array.map() work together, which is why I'm hoping that someone can shed some light on the issue I was having:
Let's say I have an ItemWrapper component that returns (among other things) an ItemListA component. ItemListA maps over an array and returns a list of Item components. Each Item component has its own state that changes on certain actions.
The way I did it at first:
const ItemWrapper = ({ items }) => {
const [someState, setSomeState] = useState(null);
const someFunction = value => setSomeState(value);
...
const ItemListA = () => items.map(item =>
<Item
key={item.id}
item={item}
callback={someFunction}
/>
)
...
return (
<div>
<ItemListA />
</div>
);
};
The problem: whenever the someFunction callback was invoked in one of the Item children, this caused ItemWrapper to re-render and reset the state of all of the other Item children.
Solved it by storing the item list in a local variable rather than a component:
const ItemWrapper = ({ items }) => {
const [someState, setSomeState] = useState(null);
const someFunction = value => setSomeState(value);
...
const itemListB = items.map(item =>
<Item
key={item.id}
item={item}
callback={someFunction}
/>
);
...
return (
<div>
{itemListB}
</div>
);
};
I don't feel like I fully understand what's going on here. My guess would be that storing the item list in a functional component somehow made it so that the ItemListA component and its children (with the exception of the one that triggered the callback - no idea why) were destroyed and then rebuilt whenever the ItemWrapper component was re-rendered, meaning that there was no trace left of their previous state. Is it because ItemListA is a function and every re-render creates a new reference?
Is it because ItemListA is a function and every re-render creates a new reference?
Yep, that's pretty much it.
When figuring out what changes to make to the DOM, react compares the virtual dom before with the virtual dom afterwards, and looks for changes. Doing an exhaustive comparison would be expensive, so they make some assumptions to speed things up. One of those assumptions is that if a component's type has changed, then that entire subtree is assumed to have changed. (For more info see react's article on reconciliation)
So in this case, react sees ItemListA from the first time, and ItemListA from the second time, and they are different component types. They look very similar to our eyes, but they're different references, which means they're different to react. So react has to unmount the old ones and mount the new ones.
With your second code, you're not creating a new type of component on every render, you're just creating an array with elements in it. The two arrays are different references, but that's ok since it's not a type of component.