Parent component unnecessarily re-rendering child on parent state change - javascript

I am creating a simple Magic The Gathering search engine. The vision is to have a list of search results, and when a search result is clicked the main display renders extended information about the card selected.
You can see it here
The top level App component contains the state of what card is to be displayed and the ScrollView component maintains the state of the card selected for only the highlighting of the selected card in the list. I propagate down the setDisplayCard handler so that when a card is clicked in the list, I can set the display card as a callback.
function App(props) {
const [displayCard, setDisplayCard] = useState(null)
return (
<div className="App">
<SearchDisplay handleCardSelect={setDisplayCard}/>
<CardDisplay card={displayCard} />
</div>
);
}
function SearchDisplay({handleCardSelect}) {
const [cards, setCards] = useState([]);
useEffect(() => {
(async () => {
const cards = await testCardSearch();
setCards(cards);
})();
}, []);
async function handleSearch(searchTerm) {
const searchCards = await cardSearch({name: searchTerm});
setCards(searchCards)
};
return (
<StyledDiv>
<SearchBar
handleSubmit={handleSearch}
/>
<ScrollView
handleCardSelect={handleCardSelect}
cards={cards}
/>
</StyledDiv>
);
}
function ScrollView({cards, handleCardSelect}) {
const [selected, setSelected] = useState(null);
return (
<ViewContainer>
{cards.map((card, idx) =>
<li
key={idx}
style={selected === idx ? {backgroundColor: "red"} : {backgroundColor: "blue"}}
onClick={() => {
setSelected(idx);
handleCardSelect(card);
}}
>
<Card card={card} />
</li>
)}
</ViewContainer>
);
}
The issue I am having is that calling setDisplayCard re-renders my ScrollView and eliminates its local state of the card that was selected so I am unable to highlight the active card in the list. Based on my understanding of react, I don't see why ScrollView re-renders as it does not depend on the state of displayCard. And I am not sure what approach to take to fix it. When I click on a card in the list, I expect it to highlight red.

A child component's render method will always be called, once its parent's render method is invoked. The same goes for if its props or state change.
Since you're using functional components, you could use the React.memo HOC to prevent unnecessary component re-renders.
React.memo acts similar to a PureComponent and will shallowly compare ScrollView's old props to the new props and only trigger a re-render if they're unequal:
export default React.memo(ScrollView);
React.memo also has a second argument, which gives you control over the comparison:
function areEqual(prevProps, nextProps) {
// only update if a card was added or removed
return prevProps.cards.length === nextProps.cards.length;
}
export default React.memo(ScrollView, areEqual);
If you were to use class-based components, you could use the shouldComponentUpdate life cycle method as well.

By default (stateless) components re-render under 3 conditions
It's props have changed
It's state has changed
It's parent re-renders
This behavior can be changed using either shouldComponentUpdate for components or memo for stateless-components.
// If this function returns true, the component won't rerender
areEqual((prevProps, nextProps) => prevProps.cards === nextProps.card)
export default React.memo(ScrollView, areEqual);
However I don't think this is your problem. You are using an array Index idx as your element key which can often lead to unexpected behavior.
Try to remove key={idx} and check if this fixes your issue.

So your App component is supposed to hold the state of the card the user clicked? Right now your App component is stateless. It's a functional component. Try converting it to a class component with an initial, and maintained, state.
What is the logic of your setDisplayCard()?
I've heard that in React 16? there is something like 'useState()' and 'hooks', but I'm not familiar with it.
This person seemed to be having a similar problem,
React functional component using state

Related

Re-rendering on key-value pair object components

I want to avoid re-render of my child component <ChildComponent/> whenever I update my state using a onClick in <ChildComponent/>.
I have my callback function in <ParentComponent/> which updates one of the values for the key-value pair object.
In the parent component
const _keyValueObject = useMemo(() => utilityFunction(array, object), [array, object])
const [keyValueObject, setKeyValueObject] = useState<SomeTransport>(_keyValueObject)
const handleStateChange = useCallback((id: number) => {
setKeyValueObject(keyValueObject => {
const temp = { ... keyValueObject }
keyValueObject[id].isChecked = ! keyValueObject[id].isChecked
return temp
})
}, [])
return(
<Container>
{!! keyValueObject &&
Object.values(keyValueObject).map(value => (
<ValueItem
key={value.id}
category={value}
handleStateChange ={handleStateChange}
/>
))}
</Container>
)
In child component ValueItem
const clickHandler = useCallback(
event => {
event.preventDefault()
event.stopPropagation()
handleStateChange(value.id)
},
[handleStateChange, value.id],
)
return (
<Container>
<CheckBox checked={value.isChecked} onClick={clickHandler}>
{value.isChecked && <Icon as={CheckboxCheckedIcon as AnyStyledComponent} />}
</CheckBox>
<CategoryItem key={value.id}>{value.title}</CategoryItem>
</Container>
)
export default ValueItem
In child component if I use export default memo(ValueItem), then the checkbox does not get updated on the click.
What I need now is to not re-render every child component, but keeping in mind that the checkbox works. Any suggestions?
Spreading (const temp = { ... keyValueObject }) doesn't deep clone the object as you might think. So while keyValueObject will have a new reference, it's object values will not be cloned, so will have the same reference, so memo will think nothing changes when comparing the category prop.
Solution: make sure you create a new value for the keyValueObject's id which you want to update. Example: setKeyValueObject(keyValueObject => ({...keyValueObject, [id]: {...keyValueObject[id], isChecked: !keyValueObject[id].isChecked})). Now keyValueObject[id] is a new object/reference, so memo will see that and render your component. It will not render the other children since their references stay the same.
Working Codesandbox
Explanation
What you need to do is wrap the child with React.memo. This way you ensure that Child is memoized and doesn't re-render unnecessarily. However, that is not enough.
In parent, handleStateChange is getting a new reference on every render, therefore it makes the parent render. If the parent renders, all the children will re-render. Wrapping the handleStateChange with useCallback makes sure react component remembers the reference to the function. And memo remembers the result for Child.
Useful resource

React functional components in Array.map are always rerendering when getting passed a function as props

I am trying to render multiple buttons in a parent component that manages all child states centrally. This means that the parent stores e.g. the click state, the disabled state for each button in his own state using useState and passes it as props to the childs. Additionally the onClick function is also defined inside of the parent component and is passed down to each child. At the moment I am doing this like the following:
const [isSelected, setIsSelected] = useState(Array(49).fill(false));
...
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
}, []);
...
(In the render function:)
return isSelected.map((isFieldSelected, key) => {
<React.Fragment key={key}>
<TheChildComponent
isSelected={isFieldSelected}
onClick={onClick}
/>
</React.Fragment/>
})
To try to prevent the child component from rerendering I am using...
... useCallback to make react see that the onClick function always stays the same
... React.Fragment to make react find a component again because otherwise a child would not have a unique id or sth similar
The child component is exported as:
export default React.memo(TheChildComponent, compareEquality) with
const compareEquality = (prev, next) => {
console.log(prev, next);
return prev.isSelected === next.isSelected;
}
Somehow the log line in compareEquality is never executed and therefore I know that compareEquality is never executed. I don't know why this is happening either.
I have checked all blogs, previous Stackoverflow questions etc. but could not yet find a way to prevent the child components from being rerendered every time that at least one component executes the onClick function and by doing that updated the isSelected state.
I would be very happy if someone could point me in the right direction or explain where my problem is coming from.
Thanks in advance!
This code will actually generate a new onClick function every render, because useCallback isn't given a deps array:
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
});
The following should only create one onClick function and re-use it throughout all renders:
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
}, []);
Combined with vanilla React.memo, this should then prevent the children from re-rendering except when isSelected changes. (Your second argument to React.memo should have also fixed this -- I'm not sure why that didn't work.)
As a side note, you can simplify this code:
<React.Fragment key={key}>
<TheChildComponent
isSelected={isFieldSelected}
onClick={onClick}
/>
</React.Fragment/>
to the following:
<TheChildComponent key={key}
isSelected={isFieldSelected}
onClick={onClick}
/>
(assuming you indeed only need a single component in the body of the map).
Turns out the only problem was neither useCallback, useMemo or anything similar.
In the render function of the parent component I did not directly use
return isSelected.map(...)
I included that part from a seperate, very simple component like this:
const Fields = () => {
return isSelected.map((isFieldSelected, i) => (
<TheChildComponent
key={i}
isSelected={isFieldSelected}
onClick={onClick}
/>
));
};
That is where my problem was. When moving the code from the seperate component Fields into the return statement of the parent component the rerendering error vanished.
Still, thanks for the help.

How to prevent re-rendering of components that have not changed?

I have a component that consists of several other components such as text fields, and when an input is made to the text field, all other components are re-rendered.
I would like to prevent the re-rendering and only re-render the component that actually changes.
I have seen that useCallback is the right way to do this and I have already seen how to use it. However, I'm having some trouble getting useCallBack to work correctly for my case.
Even if I set it up in a simple manner as below, each new character entered into the text field causes the button to be rendered again.
I don't see my mistake.
See working example in sandbox.
const Button = () => {
console.log("Button Rendered!");
window.alert("Button Rendered");
return <button onClick="">Press me</button>;
};
export default function App() {
const [textInput, setTextInput] = useState("Hallo");
const onChangeInput = useCallback(
(e) => {
setTextInput(e.target.value);
},
[textInput]
);
return (
<div>
<input
type="text"
onChange={onChangeInput}
value={textInput}
/>
<Button />
</div>
);
}
I am happy for any calrification.
Personally I would avoid React.memo / React.useRef / React.useCallback.
The simplest solution to your example is just create another component, and store the state with this.
eg.
const Button = () => {
console.log("Button Rendered!");
window.alert("Button Rendered");
return <button onClick="">Press me</button>;
};
const TextInput = () => {
const [textInput, setTextInput] = useState("Hallo");
const onChangeInput = useCallback(
(e) => {
setTextInput(e.target.value);
},
[textInput]
);
return (
<input
type="text"
onChange={onChangeInput}
value={textInput}
/>
);
}
export default function App() {
return (
<div>
<TextInput/>
<Button />
</div>
);
}
In the above if you change Text, there is not State change in App, so Button doesn't get re-rendered, no need for useMemo etc..
You will find React works really well, the more you divide your components up, not only does it solve issues of re-render, but potentially makes it a lot easier to re-use components later.
IOW: keep state as close to the component as possible, and performance will follow.
Of course your example is simple, and in a real app you will have HOC's etc to cope with, but that's another question.. :)
useCallback does not prevent rerenders. React.memo is what prevents renders. It does a shallow comparison of the previous props with the new props, and if they're the same, it skips rendering:
const Button = React.memo(() => {
console.log("Button Rendered!");
window.alert("Button Rendered");
return <button onClick="">Press me</button>;
});
The only role that useCallback plays in this is that sometimes you want to pass a function as a prop to a memoized component. For the memoization to work, props need to not change, and useCallback can help the props to not change.
changing the state causes re-render component along with all his heirs, to prevent re-render some sections, you can use useMemo to prevent unwanted re-rendering...
NOTE: useMemo has some costs... so don't overuse it (In this small example, it is not recommended at all).
in this case, if you do not need to re-rendering, you can use the useRef to save the input reference to get that value whenever you need it.
e.g:
const BlahBlah = () => {
const inputRef = React.useRef(undefined);
return (
<div>
<input ref={inputRef} />
<button onClick={() => console.log(inputRef.current.value)}
</div>
);
};
I had similar problem. I wanted to avoid rendering a component which dependent on part2 of state, when only part1 has changed.
I used shouldComponentUpdate(nextProps, nextState) as described here https://reactjs.org/docs/optimizing-performance.html to avoid rendering it.
I'm using it this way:
const [state_data, set_state_data] = useState([
true, // loading.
{}, // vehicle.
{} // user.
]);
This allows rendering only 1x instead of 3x and React keeps track of the changes. For some reason it won't track the change of objects, but arrays yes.

What is the reason behind react component showing the changes of state inside them?

When we have many components in react project and sometimes we use multiple pre-made components for making a page. While using onChange inside a component and showing the result of the state, in this case, what functionality of components allows the value render of state and how it works when we have multiple components inside other components.
Here is an Ex...
function Component() {
const [value, setValue] = React.useState()
const handleChange = val => {
setValue(val)
}
return (
<React.Fragment>
<Compo1 //perform adding +1
onChange={handleChange}
/>
Value: {value} // 1
{console.log("value", value)} // showing right value
<Compo2>
<Compo3>
<Compo1 //perform adding +1
onChange={handleChange}
/>
Value:{value} // undefined
{console.log("value", value)} // showing right value
</Compo3>
{console.log("value", value)} // showing right value
</Compo2>
</React.Fragment>
)
}
render(<Component />)
In this case why console is showing the right value but the state variable value is showing undefined.
The only way I can get that code to do what you say it does is when you incorrectly use React.memo on Compo3:
const Compo1 = ({ onChange }) => (
<button onClick={() => onChange(Date.now())}>+</button>
);
const Compo2 = ({ children }) => <div>{children}</div>;
const Compo3 = React.memo(
function Compo3({ children }) {
return <div>{children}</div>;
},
() => true//never re render unless you re mount
);
function Component() {
const [value, setValue] = React.useState(88);
const handleChange = React.useCallback(() => {
setValue((val) => val + 1);
}, []);
return (
<React.Fragment>
<Compo1 //perform adding +1
onChange={handleChange}
/>
works: {value}-----
<Compo2>
<Compo3>
<Compo1 //perform adding +1
onChange={handleChange}
/>
broken:{value}-----
</Compo3>
</Compo2>
</React.Fragment>
);
}
ReactDOM.render(
<Component />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Maybe you can do the same if you do some wacky stuff with shouldComponentUpdate
A component will render when:
When the parent renders the child and the child is a functional component (not wrapped in React.memo)
When the parent renders the child with different prop values than the previous render.
When value in [value,setValue]=useState() or when this.state changes (when state changes).
When someContext in value = useContext(someContext) changes (even if value doesn't change).
In most cases when value in value = useCustomHoom() changes but this is not guaranteed for every hook.
When Parent renders and passes a different key prop to Child than the previous render (see 2). This causes the Child to unmount and re mount as well.
In the example the Compo3 wants to re render because Parent is re rendered due to a state change and passes different props (props.children).
Compo3 is not a functional component because it's wrapped in React.memo. This means that Compo3 will only re render if props changed (pure component).
The function passed as the second argument to React.memo can be used to custom compare previous props to current props, if that function returns true then that tells React the props changed and if it returns false then that tells React the props didn't change.
The function always returns true so React is never told that the props changed.

how update the value of useEffect() hook on button click

I have a functional component and I have created a button inside it. I am also using a "Use_effect()" hook. My main is to re-render the functional component, update the use_effect() hook when the button is clicked.
const Emp_list = (props) => {
useEffect(() => {
props.getList(props.state.emp);
}, []);
return (
<div>
{props.state.emp.map((val ) =>
{val.feature_code}
{val.group_code}
<button onClick = {() => props.removeEmpFromList(val.feature_code)} > Remove </button>
<EmpForm empList={props.state.emp}
onChangeText = {props.onChangeText}
/>
</div>
<button onClick= {() => props.getdata (props.state)}>Get Names</button>
<p>
</div>
);
};
export default Emp_list;
removeEmpFromList = (i) => {
const remaining = this.state.emp( c => c.feature_code !== i)
this.setState({
emp: [...remaining]
})
}
When I click the Remove button , it will basically remove the employee from the list. The function removeEmpFromList will update the state.
The functional component EmpForm basically shows the list of all employees.
So I want to re-render the page so that, it updates the state value in useEffect() hook. So when EmpForm is called again on re-rending it shows the updated list.
You didn't provide the code for removeEmpFromList() ... but probably it updates the state by mutation therefor component gets the same object ref - compared shallowly - no difference, no reason to rerender.
Modify removeEmpFromList() method to create a new object for emp - f.e. using .filter.
If not above then passing entire state is the source of problem (the same reason as above).
Simply pass only emp as prop or use functions in setState() (to return a new object for the entire state) this way.
I figured it out! Thanks for the help guys.
So, it was not re-rendering because initally, useEffect() second parameter was [] , if you change it to props.state then it will update the changes made to the state and re-render the component automatically.
useEffect(() => {
props.getList(props.state.emp);
}, [props.state.emp]);

Categories

Resources