I am using a React hook for parent/child component.
Now I have state in my parent component (companyIcon), which I need to update based on some validation in the child component. I pass validationCallback as a callback function to the child component and update my parent state based on the value I get from the child.
Now the issue is after I update the parent state, the state value in my child component gets reset. What I am doing wrong in the below implementation ?
function ParentComp(props) {
const [companyIcon, setCompanyIcon] = useState({ name: "icon", value: '' });
const validationCallback = useCallback((tabId, hasError) => {
if (hasError) {
setCompanyIcon(prevItem => ({ ...prevItem, value: 'error'}));
// AFTER ABOVE LINE IS EXECUTED, my Child component state "myAddress" is lost i.e. it seems to reset back to empty value.
}
}, []);
const MyChildCmp = (props) => {
const [myAddress, setmyAddress] = useState('');
useEffect(() => {
if (myAddressExceptions.length > 0) {
props.validationCallback('MyInfo', true);
} else {
props.validationCallback('MyInfo', false);
}
}, [myAddressExceptions])
const handlemyAddressChange = (event) => {
//setmyAddress(event.target.value);
//setmyAddressExceptions(event.target.value);
console.log(myAddressExceptions);
}
return (
<>
<div className="row" style={{ display: 'flex', flexDirection: 'row', width: '1000px'}}>
<div style={{ width: '20%'}}>
<FormField
label='Company Address'
required
helperText={mergedErrorMessages(myAddressExceptions)}
validationState={
myAddressExceptions[0] ? myAddressExceptions[0].type : ''
}
>
<Input id='myAddress'
value={myAddress}
//onChange={handlemyAddressChange}
onChange={({ target: { value } }) => {
validateInputValue(value);
}}
onBlur={handleBlur}
inputProps={{maxLength: 9}} />
</FormField>
</div>
</div>
</>
);
}
return (
<div className="mainBlock">
Parent : {companyIcon}
{displayMyChild && <MyChildCmp validationCallback={validationCallback}/>}
</div>
)
}
export default withRouter(ParentComp);
Here are some reasons why you can lose state in child (there could be more, but these apply to you most):
{displayMyChild && <MyChildCmp validationCallback={validationCallback}/>}
Here if at one point displayMyChild is truthy, then made falsy, this means the component MyChildCmp will get unmounted, hence all its state will be gone.
But now, even if you didn't have that condition and rendered the MyChildCmp always you would still run into similar problem, this is because you defined MyChildCmp inside another component. When you do that, on each render of the parent component, the function MyChildCmp is recreated, and the reconciliation algorithm of react thinks you rendered a different component type on next render, so it will destroy the component instance. Move definition of that component outside the parent component.
Related
I have a simple question that has to do with React rendering. Here's the link to the code sandbox before I explain: https://codesandbox.io/s/list-rerendering-y3iust?file=/src/App.js
Here's the deal, I have an array of objects stored in a parent component called App. Each object has a 'checked' field which I want to toggle on clicking on that object's respective checkbox. I loop through the array of objects and display them each within a Child component. When I click on a checkbox, the handleChange function executes and that list is updated within the parent component, causing App to rerender along with ALL of the Child components. The problem I want to solve is, how can I make it so I only rerender the Child component that was clicked instead of all of them?
I tried using useCallback along with a functional update to the list state, but that didn't do anything and it still rerenders all of the child components instead of the one being toggled. I have a hunch that I'm using useCallback incorrectly, and that a brand new function is being created. I'd like an explanation of how React does it's rerendering when it comes to arrays, comparing the previous array against the new array. I understand that in my code I'm providing a copy of the original list by destructuring it and then putting it inside a new array, which obviously is not a reference to the original list so React sets the copy as the new state:
App.js
import { useCallback, useState } from "react";
import Child from "./Child";
import "./styles.css";
const mockList = [
{ text: "1", id: 1, checked: false },
{ text: "2", id: 2, checked: false },
{ text: "3", id: 3, checked: false },
{ text: "4", id: 4, checked: false },
{ text: "5", id: 5, checked: false }
];
export default function App() {
const [list, setList] = useState(mockList);
const handleChange = useCallback((checked, id) => {
setList((oldList) => {
for (let i = 0; i < oldList.length; i++) {
if (oldList[i].id === id) {
oldList[i].checked = checked;
break;
}
}
return [...oldList];
});
}, []);
return (
<div className="App">
{list.map((item) => (
<Child
key={item.id}
text={item.text}
checked={item.checked}
handleChange={(checked) => handleChange(checked, item.id)}
/>
))}
</div>
);
}
Child.js
const Child = ({ text, checked, handleChange }) => {
console.log("Child rerender");
return (
<div
style={{
display: "flex",
border: "1px solid green",
justifyContent: "space-around"
}}
>
<p>{text}</p>
<input
style={{ width: "20rem" }}
type="checkbox"
checked={checked}
onChange={(e) => handleChange(e.checked)}
/>
</div>
);
};
export default Child;
Here's how you optimize it, first you use useCallback wrong, because every rerender the (e) => handleChange(e.checked) is a new instance, hence even if we memo the Child it will still rerender because props is always new.
So we need to useCallback to the function that invoke handleChange see my forked codesandbox
https://codesandbox.io/s/list-rerendering-forked-tlkwgh?file=/src/App.js
React has nothing to do with how you manipulate your arrays or objects. It simply goes on rendering your app tree and there are certain rules that decide when to stop rerendering a certain branch within the tree. Rendering simply means React calls the component functions which themselves return a sub-tree or nodes. Please see https://reactjs.org/docs/reconciliation.html
Try wrapping the Child with React.memo():
const Child = ({ text, checked, handleChange }) => {
console.log("Child rerender");
return (
<div
style={{
display: "flex",
border: "1px solid green",
justifyContent: "space-around"
}}
>
<p>{text}</p>
<input
style={{ width: "20rem" }}
type="checkbox"
checked={checked}
onChange={(e) => handleChange(e.checked)}
/>
</div>
);
};
export default React.memo(Child);
What this React.memo() does is essentially compare previous props with next props and only rerenders the Child component if any of the props have changed and doesn't re-render if the props have stayed the same.
As a side note: Please read this https://kentcdodds.com/blog/usememo-and-usecallback
TLDR: Over optimisation at the cost of code complexity is not recommended. And preventing “unnecessary” renders probably will have a drawback in the form of extra memory usage and computation: so only need to do it if very sure it will help in a specific problem.
I have the following child component:
class Adjust extends Component {
render() {
const { values, toggleSelector, SELECTOR } = this.props;
console.log("values are", values"); //this is only being entered once right now
return(
<div
key={values}
style={{
marginBottom: "25px",
width: "80vw",
}}>
<button
buttonSelected={values[index]?.add}
onClick={() => {
toggleSelector(index, SELECTOR.ADD);
}}
>
"Add"
</button>
</div>
)}}
And this is how I pass props to this child
<Adjust values={values} SELECTOR={SELECTOR} toggleSelector={this.toggleSelector}> </Adjust>
I have a console log in my toggleSelector function in my parent component, which shows that it is being entered, but even though the state is changing, the render of Adjust component is not being entered again.
I have tried adding a key to it but that didn't help.
This is the toggleSelector function
toggleSelector(index, selector) {
console.log("enter");
let { values } = this.state;
values[index][selector] = true;
if (selector === SELECTOR.ADD) {
values[index].subtract = false;
} else if (selector === SELECTOR.SUBTRACT) {
values[index].add = false;
}
this.setState({
values,
});
}
This was all working fine when Adjust was not a child component but within the parent. Had to make it a child for better code readability
You don't manage values like an immutable variable, values is the same array between renders, you only update it's content.
You can find more information there Correct modification of state arrays in React.js
i have a parent component and i have called the child component in its HTML like this.
render() {
<EditBank
deal={dealDetailData && dealDetailData.id}
open={editBankModalStatus}
headerText={"Edit Bank Account"}
getInvestmentOfferingDetailsById = {() =>this.props.getInvestmentOfferingDetailsById({
id: this.props.match.params.id
})}
bankDetail={bankDetails}
toggleEditModal={() => this.handleEditModal("editBankModalStatus", {})}
/>
}
EditBank component is a modal which is only shown when editBankModalStatus is true and it is set to be true on a button click.
Now i want to set the state of EditBank only when button is clicked and whatever bankDetails has been passed to it.
I have tired componentDidMount lifecycle hook but it updated when component is rendered only.
I want to update the state of EditBank component when it is shown on screen only. Any help is appreciated.
Try do this :
render(){
return (
<div>
{editBankModalStatus === true &&
<EditBank
deal={dealDetailData && dealDetailData.id}
open={editBankModalStatus}
headerText={"Edit Bank Account"}
getInvestmentOfferingDetailsById = {() =>this.props.getInvestmentOfferingDetailsById({
id: this.props.match.params.id
})}
bankDetail={bankDetails}
toggleEditModal={() => this.handleEditModal("editBankModalStatus", {})}
/>
}
</div>
);
}
so EditBank will shown only if editBankModalStatus is true.
I have a form with several layers of child components. The state of the form is maintained at the highest level and I pass down functions as props to update the top level. The only problem with this is when the form gets very large (you can dynamically add questions) every single component reloads when one of them updates. Here's a simplified version of my code (or the codesandbox: https://codesandbox.io/s/636xwz3rr):
const App = () => {
return <Form />;
}
const initialForm = {
id: 1,
sections: [
{
ordinal: 1,
name: "Section Number One",
questions: [
{ ordinal: 1, text: "Who?", response: "" },
{ ordinal: 2, text: "What?", response: "" },
{ ordinal: 3, text: "Where?", response: "" }
]
},
{
ordinal: 2,
name: "Numero dos",
questions: [
{ ordinal: 1, text: "Who?", response: "" },
{ ordinal: 2, text: "What?", response: "" },
{ ordinal: 3, text: "Where?", response: "" }
]
}
]
};
const Form = () => {
const [form, setForm] = useState(initialForm);
const updateSection = (idx, value) => {
const { sections } = form;
sections[idx] = value;
setForm({ ...form, sections });
};
return (
<>
{form.sections.map((section, idx) => (
<Section
key={section.ordinal}
section={section}
updateSection={value => updateSection(idx, value)}
/>
))}
</>
);
};
const Section = props => {
const { section, updateSection } = props;
const updateQuestion = (idx, value) => {
const { questions } = section;
questions[idx] = value;
updateSection({ ...section, questions });
};
console.log(`Rendered section "${section.name}"`);
return (
<>
<div style={{ fontSize: 18, fontWeight: "bold", margin: "24px 0" }}>
Section name:
<input
type="text"
value={section.name}
onChange={e => updateSection({ ...section, name: e.target.value })}
/>
</div>
<div style={{ marginLeft: 36 }}>
{section.questions.map((question, idx) => (
<Question
key={question.ordinal}
question={question}
updateQuestion={v => updateQuestion(idx, v)}
/>
))}
</div>
</>
);
};
const Question = props => {
const { question, updateQuestion } = props;
console.log(`Rendered question #${question.ordinal}`);
return (
<>
<div>{question.text}</div>
<input
type="text"
value={question.response}
onChange={e =>
updateQuestion({ ...question, response: e.target.value })
}
/>
</>
);
};
I've tried using useMemo and useCallback, but I can't figure out how to make it work. The problem is passing down the function to update its parent. I can't figure out how to do that without updating it every time the form updates.
I can't find a solution online anywhere. Maybe I'm searching for the wrong thing. Thank you for any help you can offer!
Solution
Using Andrii-Golubenko's answer and this article React Optimizations with React.memo, useCallback, and useReducer I was able to come up with this solution:
https://codesandbox.io/s/myrjqrjm18
Notice how the console log only shows re-rendering of components that have changed.
Use React feature React.memo for functional components to prevent re-render if props not changed, similarly to PureComponent for class components.
When you pass callback like that:
<Section
...
updateSection={value => updateSection(idx, value)}
/>
your component Section will rerender each time when parent component rerender, even if other props are not changed and you use React.memo. Because your callback will re-create each time when parent component renders. You should wrap your callback in useCallback hook.
Using useState is not a good decision if you need to store complex object like initialForm. It is better to use useReducer;
Here you could see working solution: https://codesandbox.io/s/o10p05m2vz
I would suggest using life cycle methods to prevent rerendering, in react hooks example you can use, useEffect. Also centralizing your state in context and using the useContext hook would probably help as well.
laboring through this issue with a complex form, the hack I implemented was to use onBlur={updateFormState} on the component's input elements to trigger lifting form data from the component to the parent form via a function passed as a prop to the component.
To update the component's input elelment, I used onChange={handleInput} using a state within the compononent, which component state was then passed ot the lifting function when the input (or the component as a whole, if there's multiple input field in the component) lost focus.
This is a bit hacky, and probably wrong for some reason, but it works on my machine. ;-)
I want to render for drag only once, but renders the infinite loops.
i'm use The react Dnd method for this project
this warning is Show : Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
chichihandler = (id) => {
console.log('inApp', id);
this.setState({
hoverdID: 123
})
console.log("hoverd", this.state.hoverdID)
}
render() {
return (
<div className="all">
<Header />
<div className='Products_list' >
{this.state.productData.map((item) => (
<Products key={item.id} item={item} handleDrop={(productId) => this.addItem(productId)} />
))}
</div>
<div className='Store_list' >
<div className="storeName" >Store Name</div>
{this.state.storeData.map((itemS) => (
<Store key={itemS.code} itemS={itemS} chichi={(id) => this.chichihandler(id)} />
))}
</div>
</div>
)
}
storeData Code:
import React, { Component } from 'react'
import { DropTarget } from 'react-dnd'
function collect(connect, monitor) {
return {
connectDropTarget: connect.dropTarget(),
hovered: monitor.isOver(),
item: monitor.getItem()
}
}
class Store extends Component {
render() {
const { connectDropTarget, hovered, itemS } = this.props
const backcgroundColor = hovered ? 'lightgreen' : ''
if (hovered) {
this.props.chichi(itemS.name)
console.log(itemS.name)
}
return connectDropTarget(
<div>
<div id={itemS.code} className='Store' style={{ background: backcgroundColor }}>
{this.props.itemS.name}
</div>
</div>
)
}
}
export default DropTarget('item', {}, collect)(Store)
The loop occurs in the render method of your Store component, where it
calls this.props.chici(itemS.name), which
calls your chichiHandler() function, which
calls this.setState() on the parent component, which
triggers a re-render, which
causes Store to re-render, which...
It looks like you want the chichi function to be called when the user hovers over something, in which case you're better off using the onMouseOver prop on the element in question, rather than try to do it with props (see https://reactjs.org/docs/events.html#mouse-events for more info).
In general you should never call setState() from with in a render(), because it tends to cause these sorts of loops.