Reputation: 3106
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
Upvotes: 252
Views: 387875
Reputation: 544
I know this is an old question, but if anyone wants to achieve this with hooks, here's one.
lib.ts
const useSyncedState = <T extends unknown>(initialValue: T) => {
const [value, setValue] = useState(initialValue);
useEffect(() => {
setValue(initialValue);
console.log("synced state", initialValue);
}, [initialValue]);
return [value, setValue] as const;
};
SomeComponent.ts
const [myThing, setMyThing = useSyncedState(props.myThing);
Upvotes: 1
Reputation: 1533
There is also componentDidUpdate available.
Function signature:
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.
Upvotes: 36
Reputation: 2485
You can add a key
prop to your component with start_time
as value. React will think of the component as a new, separate one each time you pass a different key.
In your example:
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
key: @state.start_time
onChange: @fieldChanged.bind(null, "start_time")
Here's a link to React's (beta) docs where this solution is discussed: Preserving and Resetting State
Upvotes: 0
Reputation: 19
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>
Upvotes: -1
Reputation: 49
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?
Upvotes: 0
Reputation: 71
// 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?
Upvotes: 7
Reputation: 6255
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
Upvotes: 0
Reputation: 11093
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 becausegetInitialState
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 });
}
}
Upvotes: 317
Reputation: 511
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.
Upvotes: 51
Reputation: 1372
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;
}
}
}
Upvotes: -1
Reputation: 1798
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
Upvotes: 12
Reputation: 13571
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.
Upvotes: 66
Reputation: 354
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
Upvotes: 0
Reputation: 401
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
ParentComponent
of the ModalBody
will own the start_time
state. This is not my prefer approach in this case since i think the modal should own this state. start_time
state from your ModalBody
and use getInitialState
just like you have already done. To reset the start_time
state, you simply change the key from the ParentComponent
Upvotes: 4
Reputation: 15659
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
}
}
(above code by danburzo @ github )
Upvotes: 92