Updating state on props change in React Form
ReactjsReactjs Problem Overview
I am having trouble with a React form and managing the state properly. I have a time input field in a form (in a modal). The initial value is set as a state variable in getInitialState
, and is passed in from a parent component. This in itself works fine.
The problem comes when I want to update the default start_time value through the parent component. The update itself happens in the parent component through setState start_time: new_time
. However in my form, the default start_time
value never changes, since it is only defined once in getInitialState
.
I have tried to use componentWillUpdate
to force a change in state through setState start_time: next_props.start_time
, which did actually work, but it gave me Uncaught RangeError: Maximum call stack size exceeded
errors.
So my question is, what's the correct way of updating state in this case? Am I thinking about this wrong somehow?
Current Code:
@ModalBody = React.createClass
getInitialState: ->
start_time: @props.start_time.format("HH:mm")
#works but takes long and causes:
#"Uncaught RangeError: Maximum call stack size exceeded"
componentWillUpdate: (next_props, next_state) ->
@setState(start_time: next_props.start_time.format("HH:mm"))
fieldChanged: (fieldName, event) ->
stateUpdate = {}
stateUpdate[fieldName] = event.target.value
@setState(stateUpdate)
render: ->
React.DOM.div
className: "modal-body"
React.DOM.form null,
React.createElement FormLabelInputField,
type: "time"
id: "start_time"
label_name: "Start Time"
value: @state.start_time
onChange: @fieldChanged.bind(null, "start_time")
@FormLabelInputField = React.createClass
render: ->
React.DOM.div
className: "form-group"
React.DOM.label
htmlFor: @props.id
@props.label_name + ": "
React.DOM.input
className: "form-control"
type: @props.type
id: @props.id
value: @props.value
onChange: @props.onChange
Reactjs Solutions
Solution 1 - Reactjs
>>componentWillReceiveProps is depcricated since react 16: use getDerivedStateFromProps instead
If I understand correctly, you have a parent component that is passing start_time
down to the ModalBody
component which assigns it to its own state? And you want to update that time from the parent, not a child component.
React has some tips on dealing with this scenario. (Note, this is an old article that has since been removed from the web. Here's a link to the current doc on component props).
>Using props to generate state in getInitialState
often leads to duplication of "source of truth", i.e. where the real data is. This is because getInitialState
is only invoked when the component is first created.
>
>Whenever possible, compute values on-the-fly to ensure that they don't get out of sync later on and cause maintenance trouble.
Basically, whenever you assign parent's props
to a child's state
the render method isn't always called on prop update. You have to invoke it manually, using the componentWillReceiveProps
method.
componentWillReceiveProps(nextProps) {
// You don't have to do this check first, but it can help prevent an unneeded render
if (nextProps.startTime !== this.state.startTime) {
this.setState({ startTime: nextProps.startTime });
}
}
Solution 2 - Reactjs
Apparently things are changing.... getDerivedStateFromProps() is now the preferred function.
class Component extends React.Component {
static getDerivedStateFromProps(props, current_state) {
if (current_state.value !== props.value) {
return {
value: props.value,
computed_prop: heavy_computation(props.value)
}
}
return null
}
}
Solution 3 - Reactjs
componentWillReceiveProps
is being deprecated because using it "often leads to bugs and inconsistencies".
**If something changes from the outside, consider resetting the child component entirely with key
**.
Providing a key
prop to the child component makes sure that whenever the value of key
changes from the outside, this component is re-rendered. E.g.,
<EmailInput
defaultEmail={this.props.user.email}
key={this.props.user.id}
/>
On its performance:
> While this may sound slow, the performance difference is usually insignificant. Using a key can even be faster if the components have heavy logic that runs on updates since diffing gets bypassed for that subtree.
Solution 4 - Reactjs
The new hooks way of doing this is to use useEffect instead of componentWillReceiveProps the old way:
componentWillReceiveProps(nextProps) {
// You don't have to do this check first, but it can help prevent an unneeded render
if (nextProps.startTime !== this.state.startTime) {
this.setState({ startTime: nextProps.startTime });
}
}
becomes the following in a functional hooks driven component:
// store the startTime prop in local state
const [startTime, setStartTime] = useState(props.startTime)
//
useEffect(() => {
if (props.startTime !== startTime) {
setStartTime(props.startTime);
}
}, [props.startTime]);
we set the state using setState, using useEffect we check for changes to the specified prop, and take the action to update the state on change of the prop.
Solution 5 - Reactjs
There is also componentDidUpdate available.
Function signatur:
componentDidUpdate(prevProps, prevState, snapshot)
Use this as an opportunity to operate on the DOM when the component has been updated. Doesn't get called on initial render
.
See You Probably Don't Need Derived State Article, which describes Anti-Pattern for both componentDidUpdate
and getDerivedStateFromProps
. I found it very useful.
Solution 6 - Reactjs
You Probably Don't Need Derived State
1. Set a key from the parent
> When a key changes, React will create a new component instance rather > than update the current one. Keys are usually used for dynamic lists > but are also useful here.
2. Use getDerivedStateFromProps
/ componentWillReceiveProps
> If key doesn’t work for some reason (perhaps the component is very expensive to initialize)
By using getDerivedStateFromProps
you can reset any part of state but it seems
a little buggy at this time (v16.7)!, see the link above for the usage
Solution 7 - Reactjs
// store the startTime prop in local state
const [startTime, setStartTime] = useState(props.startTime)
//
useEffect(() => {
if (props.startTime !== startTime) {
setStartTime(props.startTime);
}
}, [props.startTime]);
Can this method be migrated to class components?
Solution 8 - Reactjs
From react documentation : https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html
> Erasing state when props change is an Anti Pattern
Since React 16, componentWillReceiveProps is deprecated. From react documentation, the recommended approach in this case is use
- Fully controlled component: the
ParentComponent
of theModalBody
will own thestart_time
state. This is not my prefer approach in this case since i think the modal should own this state. - Fully uncontrolled component with a key: this is my prefer approach. An example from react documentation : https://codesandbox.io/s/6v1znlxyxn . You would fully own the
start_time
state from yourModalBody
and usegetInitialState
just like you have already done. To reset thestart_time
state, you simply change the key from theParentComponent
Solution 9 - Reactjs
It's quite clearly from their docs:
If you used componentWillReceiveProps for re-computing some data only when a prop changes, use a memoization helper instead.
Use: https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#what-about-memoization
Solution 10 - Reactjs
Use Memoize
The op's derivation of state is a direct manipulation of props, with no true derivation needed. In other words, if you have a prop which can be utilized or transformed directly there is no need to store the prop on state.
Given that the state value of start_time
is simply the prop start_time.format("HH:mm")
, the information contained in the prop is already in itself sufficient for updating the component.
However if you did want to only call format on a prop change, the correct way to do this per latest documentation would be via Memoize: https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#what-about-memoization
Solution 11 - Reactjs
The data source of the form must be based on user input, in the case that the user has entered, any situation resulting in the update of the subcomponent, will trigger componentWillReceiveProps or getDerivedStateFromProps operation, at this time, the value after comparison is certainly not equal, after the execution of setState, the value of the user input will be changed, is not this a mistake?
Solution 12 - Reactjs
I came up with the Following solution using Functional Components and the useEffect hook: It works by having the useEffect hook watch the controlling property from props
const { useEffect, useState } = React
const Child = (props) => {
const [bgColor, setBgColor] = useState(props.bgColor);
const { children } = props;
useEffect(() => {
setBgColor(props.bgColor);
}, [props.bgColor]);
return (
<div style={{ height: "100px", width: "100px", backgroundColor: bgColor }}>{children}</div>
)
}
const Parent = (props) => {
const [childControllingProp, setChildControllingProp] = useState(props.childControllingProp);
const { title } = props;
const inputRef = React.createRef();
return (
<>
<input ref={inputRef} type="text" onChange={() => setChildControllingProp(inputRef.current.value)}/>
<Child bgColor={childControllingProp}>{title}</Child>
</>
)
}
$(document).ready(() => {
ReactDOM.render(
<Parent title="Title" childControllingProp="blue"/>,
document.querySelector("#root")
);
})
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<div id="root"></div>
Solution 13 - Reactjs
I think use ref is safe for me, dont need care about some method above.
class Company extends XComponent {
constructor(props) {
super(props);
this.data = {};
}
fetchData(data) {
this.resetState(data);
}
render() {
return (
<Input ref={c => this.data['name'] = c} type="text" className="form-control" />
);
}
}
class XComponent extends Component {
resetState(obj) {
for (var property in obj) {
if (obj.hasOwnProperty(property) && typeof this.data[property] !== 'undefined') {
if ( obj[property] !== this.data[property].state.value )
this.data[property].setState({value: obj[property]});
else continue;
}
continue;
}
}
}