New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
prevProps
in getDerivedStateFromProps()
#40
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
- Start Date: (2018-04-05) | ||
- RFC PR: (leave this empty) | ||
- React Issue: (leave this empty) | ||
|
||
# Summary | ||
|
||
Not having `prevProps` in the new `getDerivedStateFromProps()` function is really inconvenient for a library I'm migrating because in `componentWillReceiveProps()` it compared a property to find out if it has changed, and only if it did then the component changed its own internal state. | ||
|
||
# Basic example | ||
|
||
I added `prevProps` to `getDerivedStateFromProps()` myself this way: | ||
|
||
```js | ||
constructor(props) { | ||
super(props) | ||
|
||
// Needs to store initial `this.props` here | ||
// so that it's not `undefined` on the first | ||
// `getDerivedStateFromProps()` call. | ||
this.state = { props } | ||
} | ||
|
||
// `state.props` is `prevProps`. | ||
static getDerivedStateFromProps(props, state) { | ||
if (props.country !== state.props.country) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In other words- if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If props and prevProps are the same for the initial render, this check won't "won't work". It will work. It just won't enter the
Since your previous statement is falsy, this statement you're conducting from the previous one is also false. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code, as written when I left this comment, would not work- because it would not set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bvaughn ok There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bvaughn I'm not mad at you, by the way. Just to make things clear. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No worries 🙂 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about setting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @j-f1 I think it would break someone's code, because they could have static propTypes = {
value: PropTypes.shape({
nestedValue: PropTypes.object
}).isRequired
} So they could expect There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This part looks reasonable:
But I'm not sure what's the conclusion of this statement? If this statement is valid, could it be the reason to agree suing props as prevProps's first value? |
||
return { | ||
props, | ||
derivedValue: ... | ||
} | ||
} | ||
} | ||
``` | ||
|
||
The whole `constructor()` thing is just for `prevProps` and feels bulky. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This example seems a bit contrived, since you can write this all as:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I think we'll need some real-world examples to motivate this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jquense I can, but essentially this would be the same thing: storing part of previous const new_default_country = this.props.country
const old_default_country = prevProps.country
const country = this.state.country
// If the default country changed.
// (e.g. in case of ajax GeoIP detection after page loaded)
// then select it but only if no phone number has been entered so far.
// Because if the user has already started inputting a phone number
// then he's okay with no country being selected at all ("International")
// and doesn't want to be disturbed, doesn't want his input to be screwed, etc.
if (new_default_country !== old_default_country && !country && !value) {
return {
...,
country : new_default_country
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gaearon CC ^^^^^ |
||
|
||
And I could be comparing more than just country - I'm also comparing `localeMessages` so that if they did change then I do | ||
|
||
```js | ||
// Imagine a user switched their locale, so the UI adapts in real-time. | ||
if (prevProps.localeMessages !== props.localeMessages) { | ||
this.setState({ | ||
selectOptions: generateLocalizedSelectOptions(props.localeMessages) | ||
}) | ||
} | ||
``` | ||
|
||
# Motivation | ||
|
||
In my library `componentWillReceiveProps()` compared a property to find out if it has changed, and if it did then the component changed its own internal state. There's a suggestion to use `componentDidUpdate()` but it feels smelly because "derive state from props" is what my old `componentWillReceiveProps` really does so the name kinda fits in only if it did have `prevProps` argument. | ||
|
||
# Detailed design | ||
|
||
`getDerivedStateFromProps()` could take a third `prevProps` argument, with the first call simply being `getDerivedStateFromProps(this.props, this.state, this.props)` (inside constructor(), or whatever else it's now at). The first call would simply be a no-op. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regarding this proposal ( Code sample from above constructor(props) {
super(props)
// Needs to store initial `this.props` here
// so that it's not `undefined` on the first
// `getDerivedStateFromProps()` call.
this.state = { props }
}
// `state.props` is `prevProps`.
static getDerivedStateFromProps(props, state) {
if (props.country !== state.props.country) {
return {
props,
derivedValue: ...
}
}
}
If React were to pass a
Passing a null initial valueIf we were to pass a null initial value, the pattern for using this method would become something like: static getDerivedStateFromProps(nextProps, prevState, prevProps) {
const nextState = {};
if (
!prevProps ||
prevProps.foo !== nextProps.foo
) {
nextState.derivedFromFoo = derivedFromFoo;
}
if (
!prevProps ||
prevProps.bar !== nextProps.bar
) {
nextState.derivedFromBar = derivedFromBar;
}
return nextState;
} This does not look significantly different (or objectively better) to me than the current pattern: static getDerivedStateFromProps(nextProps, prevState) {
const nextState = {};
if (nextProps.foo !== prevState.prevFoo) {
nextState.prevFoo = nextProps.foo;
nextState.derivedFromFoo = derivedFromFoo;
}
if (nextProps.bar !== prevState.prevBar) {
nextState.prevBar = nextProps.bar;
nextState.derivedFromBar = derivedFromBar;
}
return nextState;
} Passing an empty objectPassing an empty object for the initial call would remove the need for certain types of null checks, but nested values would still need to check, e.g.: if (
prevProps.list != null &&
prevProps.list.length !== nextProps.list.length
) {
// ...
} This would also make it more difficult to differentiate between values that are undefined because they were not passed by the user and values that are undefined because it's the initial render. (This distinction may be important in some cases.) When is
|
||
|
||
This behaviour complies with the official docs [explicitly state](https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops): | ||
|
||
> Note that if a parent component causes your component to re-render, this method will be called even if props have not changed. You may want to compare new and previous values if you only want to handle changes. | ||
|
||
I.e. it says that the new props aren't neccessarily different from the old ones, so doing `getDerivedStateFromProps(this.props, this.state, this.props)` wouldn't be illegal in any way and wouldn't contradict the already cemented behaviour for this new function. | ||
|
||
# Drawbacks | ||
|
||
None I can think of. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is at least one major drawback: This proposed change would break any applications that:
In the above case, React would call To be clear, this drawback is not technically inherent to this proposal. (It exists because we've already released the API.) So it's not the focus of my response, but I think it is a consideration worth mentioning. |
||
|
||
# Alternatives | ||
|
||
I guess this would work but it doesn't feel semantically right given that there already is `getDerivedStateFromProps()`. | ||
|
||
```js | ||
componentDidUpdate(prevProps) { | ||
if (this.props.country !== prevProps.country) { | ||
this.setState({ | ||
derivedValue: ... | ||
}) | ||
} | ||
} | ||
``` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using The React docs show an alternative pattern, which is to store a copy of the props you want to compare (in this case static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.prevCountry !== nextProps.country) {
return {
prevCountry: nextProps.country,
derivedValue: yourValueHere
};
}
} |
||
|
||
# Adoption strategy | ||
|
||
If the third argument is added there's no migration strategy. It could be the first one, but React 16.3 is already released. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This example code wouldn't actually work. If "prev props" are the same as
props
during the initial render- theprops.country !== state.props.country
check below would never be true and the initial render would never have access to the derivedstate
values until the first time the component was updated.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bvaughn Actually, yeah, I should have wrote
this.state = { props, derivedValue: ... }
there instead of justthis.state = { props }
. I think if I push the correction now it will destroy comments so I guess I won't. If the constructor code is corrected tothis.state = { props, derivedValue: ... }
, and then ifprevProps
are implemented as the third argument ofgetDerivedStateFromProps()
, then it will still make sense because developers will be able to dropprops
fromthis.state
resulting in less cluttered state and avoiding the cases when a developer accidentally forgets to returnprops
as part of newstate
in some of the manyif/else
branches ofgetDerivedStateFromProps()
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with catamphetamine. The
derivedValue
could just have an initial value in the constructor. If thederivedValue
issue is solved, couldprevProps
be provided using the value of props then?