Reputation: 499
I have child component that gets a start value through props. In the component this start value is copied to the local state and all the modification on it are handled in the component itself.
The parent component receives the result of the modified value through an event.
Now it can be possible that the value is modified in the parent component and this change should be reflected in the child component.
So, the child component is not uncontrolled, but also not completely controlled. Semi-controlled you could say.
Here is a basic example:
class ParentComponent extends React.Component {
state = {
value: 5
};
increment = () => {
this.setState({ value: this.state.value + 1 });
};
someOtherAction = () => {
this.forceUpdate();
};
render() {
return (
<div>
<button onClick={this.increment}>Parent modify</button>
<button onClick={this.someOtherAction}>Force rerender</button>
<br />
<br />
<ChildComponent value={this.state.value} onDone={val => alert(val)} />
</div>
);
}
}
class ChildComponent extends React.Component {
state = {
value: this.props.value
};
static getDerivedStateFromProps(props, state) {
return { value: props.value };
}
increment = () => {
this.setState({ value: this.state.value + 1 });
};
render() {
return (
<div>
<b>Child component</b> <br />
value: {this.state.value} <br />
<button onClick={this.increment}>Child modify</button>
<button onClick={() => this.props.onDone(this.state.value)}>
End modification
</button>
</div>
);
}
}
In the example everything works fine, until the parent component gets updated due to an action that has nothing to do with the state.value
field (simulated by a force update), because the childs value is not in sync with the parents value and so after the update the childs value is overwritten by the parents value.
Possible solutions:
Add a "parentValue" state to the child component, which is always set to the value of the parent component. Then I can check in getDerivedStateFromProps
if the new props value is different to the current parentValue and only update the value if this is the case:
static getDerivedStateFromProps(props, state) {
return (props.value !== state.parentValue) ? { value: props.value, parentValue: props.value } : null;
}
Problem: if the value is modified in the child component and I want to reset it in the parent component to the old initial value props.value would be equal to state.parentValue and so no update would happen.
Add an initialValue prop, which is used to initialise the value on the child component and then only update the value of child component if props include a value field.
Do you have any other ideas, how to handle it? Thanks for your help!
Upvotes: 2
Views: 849
Reputation: 499
I guess without implementing some anti-patterns there is not a really good solution to it. What I will do is to make ChildComponent uncontrolled and implement a wrapper that makes ChildComponent completely controlled. So depending on whatever I need, I use either use ChildComponent directly or the wrapper.
Thanks for all of your help!
Upvotes: 0
Reputation: 16309
I think your problem is well described in this official react blog post. It usually occurs when the parent "resets" the child state by passing props.
Make sure to never override state with props when using getDerivedStateFromProps()
without any further checks. Unconditionally copying props to state is considered an anti-pattern:
class EmailInput extends Component {
state = { email: this.props.email };
render() {
return <input onChange={this.handleChange} value={this.state.email} />;
}
handleChange = event => {
this.setState({ email: event.target.value });
};
componentWillReceiveProps(nextProps) {
// This will erase any local state updates!
// Do not do this.
this.setState({ email: nextProps.email });
}
}
For ways to avoid this see Preferred Solutions in the blog post.
Solutions:
The most trivial solution is to make the component fully controlled by entirely removing state from it or lifting it to the parent.
Another solution is to make it fully uncontrolled and give it a unique key
so that it will entirely re-render, when the initial value should change:
class EmailInput extends Component {
state = { email: this.props.defaultEmail };
handleChange = event => {
this.setState({ email: event.target.value });
};
render() {
return <input onChange={this.handleChange} value={this.state.email} />;
}
}
and render it with a key:
<EmailInput
defaultEmail={this.props.user.email}
key={this.props.user.id}
/>
If you now want your component to "reset" just pass the new initial value together with a different key. React will unmount the old component and replace it with a fresh one.
Upvotes: 2
Reputation: 15292
Whenevent you want to update the state from parent,
add timestamp
state and update it with each state update.
increment = () => {
this.setState({ value: this.state.value + 1,timestamp : (new Date()).getTime() });
};
inside render :
<ChildComponent value={this.state.value} timestamp = {this.state.timestamp} onDone={val => alert(val)} />
Now, in child component,
state = {
value: this.props.value,
timestamp : this.prop.timestamp // store the passd value from prop
};
With each re-rendering from parent, definitely updated timestamp
only available
when it is updated from increment
method and not elsewhere.So,you have way to check the parent triggered the update with right action and not with forceUpdate.
static getDerivedStateFromProps(props, state) {
return (props.timestamp > state.timestamp) ? { value: props.value, parentValue: props.value,timestamp : props.timestamp } : null;
}
Upvotes: 2