“Semi-controlled” component in react
“Semi-controlled” component in react
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 (
Parent modify
Force rerender
alert(val)} />
);
}
}
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 (
Child component
value: {this.state.value}
Child modify
this.props.onDone(this.state.value)}>
End modification
);
}
}
Link to working example
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.
state.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:
getDerivedStateFromProps
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!
the value is modified in the parent component and this change should be reflected in the child component
after the update the childs value is overwritten by the parents value
why dont you just make it a completely controlled component?
– John Ruddell
Jun 29 at 8:34
@cosh It should only be overwritten, when it is intended, but now it is done on every render, e.g. when state changes that might have nothing to with this value.
– Tek
2 days ago
@JohnRuddell Because the child component takes care of many additional things that I sometimes want to influence and sometimes not. By making it completely controlled, it would "pollute" the parents component state, even if it is not necessary.
– Tek
2 days ago
Then you should just make it controlled from the child
– John Ruddell
2 days ago
3 Answers
3
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:
getDerivedStateFromProps()
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:
key
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.
Thanks for your input! Fun fact: I read this article right before I made my post and that was the reason I made it in the first place.
– Tek
2 days ago
Whenevent you want to update the state from parent,
add timestamp
state and update it with each state update.
timestamp
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.
timestamp
increment
static getDerivedStateFromProps(props, state) {
return (props.timestamp > state.timestamp) ? { value: props.value, parentValue: props.value,timestamp : props.timestamp } : null;
}
Thanks! That would definitely work, but has a "workaroundish" smell to it. I hoped that there might be a really nice and clean solution to the problem, but it doesn‘t seem like it.
– Tek
2 days ago
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!
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
I don't understand. First you say
the value is modified in the parent component and this change should be reflected in the child component
but then there's an error becauseafter the update the childs value is overwritten by the parents value
. Which one is it?– cosh
Jun 29 at 8:32