I am trying to change an input inside a GrandChild class and a Bootstrap Table inside Parent class*. An user would change the input inside **GrandChild class then save it, so the changes are seen in the Bootstrap Table in Parent class; however, I am seeing this weird behavior where my props are changing before I call the .onChange (which is my save). I believe this is causing my inputs to not save or setting the state properly.
Data being passed down hierarchy: GrandParent => Parent => Child => GrandChild
It is occurring at the Child class's handleSave() function:
export class Child extends React.Component {
constructor(props){
this.state = {
data:this.props.data
}
}
handleChange = (name, value) => {
this.setState((prevState) => {
let newState = {...prevState};
newState.data.dataList[0][name] = value; // data
return newState;
});
};
handleSave(){
let dataList = this.state.data.dataList.slice();
console.log("dataList state-dataList:", dataList);
console.log("dataList before onChange 2:", this.props.data.dataList); //Same as dataList or this.state.data.dataList
this.props.onChange("dataList", dataList);
console.log("dataList onChange 3:", this.props.data.dataList); //Same as dataList or this.state.data.dataList
}
render() {
return (
<div>
<GrandChild data={this.state.data} onChange={this.handleChange} />
</div>
)
}
Child class's this.props.onChange gets sent back to the Parent class:
export class Parent extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
columns = [
{dataField: '..', text: '...' },
{dataField: '..', text: '...' },
{dataField: '..', text: '...' },
{dataField: '..', text: '...'}];
handleChange = (name, value) => {
this.props.onChange(name, value);
};
render() {
return (
<div>
<BootstrapTable
hover
condensed={true}
bootstrap4={true}
keyField={'id'}
data={this.props.data.dataList}
columns={this.columns}
/>
<Child data={this.props.data} onChange={this.handleChange} />
</div>
);
}
}
Then Parent class's this.props.onChange* gets sent to GrandParent Class:
export class GrandParent extends React.Component {
constructor(props) {
super(props);
this.state = {
data: {...this.props.location.state.data}
};
this.handleChange = this.handleChange.bind(this);
}
handleChange = (name, value) => {
this.setState((prevState) => {
let newState = {};
let data = Object.assign({}, prevState.data);
data[name] = value;
newState.data = data;
return newState;
});
};
render() {
return (
<div>
<Form>
<Parent data={this.state.data} onChange={this.handleChange} />
</Form>
</div>
)
}
This is the GrandChild's class:
export class GrandChild extends React.Component {
constructor(props) {
super(props);
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange = (event) => {
const target = event.target;
const value = target.type === 'checkbox' ?
target.checked :
target.value;
const name = target.name;
this.props.onChange(name, value);
};
render() {
return (
<div>
<Form.Row>
<Form.Group as={Col}>
<Form.Label>Label Name</Form.Label>
<Form.Control name="labelName" value={this.props.data.[0]labelName || ""} //ignore the index for now
onChange={this.handleInputChange}/>
</Form.Group>
</Form.Row>
</div>
)
}
}
I expected that console.logs() of the dataLists to be different; however, they give the same exact object before it even runs the this.props.onChange("dataList", dataList);
Potentially, the third dataList console.log might be same as the state dataList because of setState being asynchronous.
It looks like the main issue is that you're mutating state/props in Child
:
handleChange = (name, value) => {
this.setState((prevState) => {
// {...prevState} makes a shallow copy of `prevState`
let newState = {...prevState};
// Next you're modifying data deep in `newState`,
// so it's mutating data in the `dataList` array,
// which updates the source of truth for both props and state.
newState.data.dataList[0][name] = value;
return newState;
});
};
One way to do this (avoiding mutation) is like this:
handleChange = (name, value) => {
this.setState(prevState => ({
data: {
...prevState.data,
dataList: [
{
...prevState.data.dataList[0],
[name]: value
},
...prevState.data.dataList.slice(1)
]
}
}));
};
If that's more verbose than you'd like, you could use a library like immutable-js
.
Another issue that could cause you bugs is copying props into state. This article gives some explanation of why that's bad: https://overreacted.io/writing-resilient-components/#dont-stop-the-data-flow-in-rendering
Basically: If you set props in state and then update state and pass props down to a child, the data you're passing down will be stale. It doesn't look like you're doing that here, but it would be easy to miss. An easy way to avoid this is to name any props you plan on setting in state initialProp
If your prop is named initialData
, it will be clear that from that point in the tree you should rely on the value in state rather than props.
Also, handleChange
in Grandparent
can be written more simply:
handleChange = (name, value) => {
this.setState(prevState => ({
data: {
...prevState.data,
[name]: value
}
}))
};