Skip to content
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

Closed
wants to merge 2 commits into from

Conversation

catamphetamine
Copy link

@catamphetamine catamphetamine commented Apr 5, 2018

An update from April 21, 2018:

This RFC was aimed to start a discussion on the new static getDerivedStateFromProps() method replacing the old componentWillReceiveProps() method.
A lot of people are using componentWillReceiveProps() to derive this.state values and to do various other things based on whether did the props change or they didn't.
The new static getDerivedStateFromProps() method doesn't provide any access to prevProps so currently we all have to hack around it by storing those "previous props" in this.state:

class Example extends Component {
  state = {
    value: fn(this.props.value),
    props: this.props
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.value !== prevState.props.value) {
      return {
        value: fn(nextProps.value),
        props: nextProps
      }
    }
    return null
  }
}

This does work, but it introduces some unneccessary code which wouldn't need to be if there was a third prevProps argument in the getDerivedStateFromProps() function.

The new lifecycle API feels inconsistent because of this: why does componentDidUpdate(prevProps, prevState) have prevProps argument but at the same time getDerivedStateFromProps() refuses to provide the prevProps argument?
If prevProps argument is removed from getDerivedStateFromProps() then it should also be removed from componentDidUpdate() but it's not and that feels weird.

If the prevProps argument was provided by getDerivedStateFromProps() function then the example at the top would look like this:

class Example extends Component {
  state = {
    value: fn(this.props.value)
  }

  static getDerivedStateFromProps(nextProps, prevState, prevProps) {
    if (nextProps.value !== prevProps.value) {
      return {
        value: fn(nextProps.value)
      }
    }
    return null
  }
}

Which is undoubtedly cleaner.

Still, as the discussion proceeded, the library authors pointed out that adding a third prevProps argument in the next React version (say, 16.4) would not be of much use because the react-lifecycles-compat polyfill only polyfills for React <= 16.2 while for React 16.3 getDerivedStateFromProps is left untouched and that's why it would break on React 16.3 if it relied on the new prevProps argument being present. So, even if the prevProps argument was added developers wouldn't be able to use it in their libraries unless they'd like to break compatibility with React 16.3 and release a new major version of their library with a peerDependency of React >=16.4 and maintain two versions of the same library: one for React <16.4 and the other for React >= 16.4 (which is not a viable solution and which is exactly the opposite of what the polyfill originally aims to provide — backwards compatibility with all versions of React). It's a bit tricky, but seems that it's a valid point. See the library authors' comment. If React had some version number detection (which could perhaps be added to 16.4 but that would be too much hassle I guess) then the polyfill could differentiate between 16.3.0/16.3.1 and all further versions of React (>=16.4) so that it would polyfill on React <=16.3.1 and not polyfill on React >=16.4.

In the end, the library authors decided to close this RFC. I have no hard feelings about that, given that I've already rewritten my libraries with the this.state.props workaround.

See the original RFC
(the original RFC has a couple of errors/typos but I chose not to push the corrections because that would destroy the comments by the reviewers)

@benadamstyles
Copy link

Wanting to proactively keep my code up-to-date with latest best practices, I have been refactoring cWRP out of my components, and working around the lack of prevProps is beginning to feel like a real burden... My use cases for comparing with prevProps are almost all situations where I have an expensive computation to derive state, and I only want to execute that computation when necessary. One solution suggested memoizing that computation and calling it each time, which is a good idea but in practice it means managing caches which, when you're dealing with a function that takes more than one argument, greatly increases your surface area for potential bugs and mistakes. I'm having to resort to a weird multi-depth WeakMap, and making decisions about when to drop different levels of the cache. Again: it works, but from my point of view as a user of react (this is important – I'm not a contributor to react so I'm probably missing lots of important information), it feels unnecessarily burdensome, given it could all be avoided by a single extra argument passed to gDSFP.

I don't want to fuel old discussions which have been had elsewhere already, but the solution here of passing this.props the first time and then prevProps for all subsequent calls doesn't appear to have been suggested before (please correct me if I'm wrong!), and it does appear to solve the issue with prevProps being nullable.

The other reason given for not passing prevProps, about freeing up react to save memory by dropping prevProps from memory in the future, suggests prevProps shouldn't be available in cDU either, but it is. Also, if we're keeping prevProps in state instead, then we're not solving the memory problem anyway. I could be completely missing the point about this though!

}
```

The whole `constructor()` thing is just for `prevProps` and feels bulky.
Copy link

Choose a reason for hiding this comment

The 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:

state = {}

static getDerivedStateFromProps({ country }, state) {
  if (country !== state.country) {
    return { country, derivedValue ... }
  }
  return null
}

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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 props inside state - that's what you're basically suggesting. In my case I don't even need that country in my state - it's just the fact that it changed, nothing more. Consider the externally set default country, which can or can not change the currently selected internal state country depending on circumstances. Or consider localizedMessages: i don't need to store them in state, I just re-generate <select/> options based on those messages if they changed, and that's it.

The real-world example:

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
  }
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gaearon CC ^^^^^

@jamesreggio
Copy link

jamesreggio commented Apr 5, 2018

Here's a real-world example I encountered a couple weeks ago while reviewing a PR to upgrade react-apollo to React 16.3.0. (Pertinent comments are here, though unfortunately a force-push to the branch has caused the errant code to disappear.)

As you noted, it's not particularly burdensome to do as @jquense suggested and store the previous prop values within the component state for comparison. However, it does feel a bit error-prone, since every return path from getDerivedStateFromProps needs to ensure that the prior props are properly included in the nextState being returned.

Here's an approximate recreation of the buggy getDerivedStateFromProps I found in react-apollo:

static getDerivedStateFromProps(nextProps, prevState) {
  const { client, query } = prevState;

  // If we're being bypassed, return early.
  if (nextProps.skip) {
    return;
  }

  // Reset all component state if the Apollo client changes.
  if (client !== nextProps.client) {
    return {
      ...initialState,
      client: nextProps.client,
    };
  }

  // Dump our prior results if the query changes.
  if (query !== nextProps.query) {
    return {
      lastResult: null,
      query: nextProps.query,
    }
  }
}

In case it's not clear, the problem with the above code is that state.client and state.query are not being updated in every return path from the function, which means that future comparisons may be evaluated against far-outdated values of those props.

There are two straightforward ways to fix this function, but both have drawbacks (in my opinion).

The first approach is to adopt a builder-pattern for nextState.

static getDerivedStateFromProps(nextProps, prevState) {
  const { client, query } = prevState;

  const nextState = {
    client: nextProps.client,
    query: nextProps.query,
  };

  // If we're being bypassed, return early.
  if (nextProps.skip) {
    return nextState;
  }

  // Reset all component state if the Apollo client changes.
  if (client !== nextProps.client) {
    return {
      ...nextState,
      ...initialState,
    };
  }

  // Dump our prior results if the query changes.
  if (query !== nextProps.query) {
    return {
      ...nextState,
      lastResult: null,
    }
  }
}

I could see a world in which linters enforce that nextState is always composed with its prior value, so perhaps this is a fully acceptable solution to the problem.

The alternative approach I've seen in the wild (and was indeed suggested by @catamphetamine in this RFC) is to just include the entirety of nextProps in the nextState. Something akin to this:

static getDerivedStateFromProps(nextProps, prevState) {
  const { prevProps } = prevState;
  const nextState = { prevProps: nextProps };

  // If we're being bypassed, return early.
  if (nextProps.skip) {
    return nextState;
  }

  // Reset all component state if the Apollo client changes.
  if (prevProps.client !== nextProps.client) {
    return {
      ...nextState,
      ...initialState,
    };
  }

  // Dump our prior results if the query changes.
  if (prevProps.query !== nextProps.query) {
    return {
      ...nextState,
      lastResult: null,
    }
  }
}

This approach is a bit simpler, but still requires the nextState builder pattern to ensure that nextState.prevProps is set in all return paths.

The (admittedly minor) problem I see with this approach concerns PureComponents. Depending on React internals, the nextProps object may not strictly equal prevProps, even if a shallow key/value comparison of the individual props yields no inequalities. As a result, with this approach, you're basically guaranteeing that the component will update every time getDerivedStateFromProps is invoked, because you're always affixing a new reference to nextState.prevProps.


I think the coding conventions I proposed above are adequate and workable; however, I don't think this API sets developers up to fall into the 'pit of success', so to speak.

In fact, even after fixing and simplifying the codepaths through getDerivedStateFromProps in that PR, another bug popped up where nextState.props was failing to be set under some conditions. (I'm only noticing this now.) I have a ton of respect for @jbaxleyiii (who agreed to be referenced here — thanks!), so I do worry when I see these mistakes being made.


// Needs to store initial `this.props` here
// so that it's not `undefined` on the first
// `getDerivedStateFromProps()` call.
Copy link
Collaborator

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- the props.country !== state.props.country check below would never be true and the initial render would never have access to the derived state values until the first time the component was updated.

Copy link
Author

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 just this.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 to this.state = { props, derivedValue: ... }, and then if prevProps are implemented as the third argument of getDerivedStateFromProps(), then it will still make sense because developers will be able to drop props from this.state resulting in less cluttered state and avoiding the cases when a developer accidentally forgets to return props as part of new state in some of the many if/else branches of getDerivedStateFromProps().

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 the derivedValue issue is solved, could prevProps be provided using the value of props then?


// `state.props` is `prevProps`.
static getDerivedStateFromProps(props, state) {
if (props.country !== state.props.country) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other words- if props and prevProps are the same for the initial render, this check won't work. That's why prevProps would need to be null or undefined the first time (if that parameter were to be provided).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bvaughn

In other words- if props and prevProps are the same for the initial render, this check won't work.

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 if condition. Ain't nothing illegal in that.

That's why prevProps would need to be null or undefined the first time (if that parameter were to be provided).

Since your previous statement is falsy, this statement you're conducting from the previous one is also false.
prevProps would not need to be null or undefined the first time.
prevProps will simply be equal to this.props the first time.
And that's it.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 derivedValue anywhere for the initial render. That is all I was pointing out.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries 🙂

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about setting prevProps to {} on the first render?

Copy link
Author

Choose a reason for hiding this comment

The 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 props.value to always exist, and a developer could query props.value.nestedValue without any if/elses, so empty props would throw Cannot read "nestedValue" of undefined.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@j-f1 This was already discussed here and in my opinion good reasons were given why this shouldn't happen.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part looks reasonable:

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 if condition. Ain't nothing illegal in that.

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?

@bvaughn
Copy link
Collaborator

bvaughn commented Apr 6, 2018

Thanks for the detailed comment, @jamesreggio! ❤️

I think you raise a good point about there being potential for a subtle "memoization" type bug with this pattern.

I want to point out that props values only need to be mirrored in state if one of the following conditions are true:

  1. A changing props value needs to erase or reset a component state value, or...
  2. It's expensive to derive state for a certain prop.1

In the above example, lastResult seems to fall under the first category. I don't think anything falls under the second category. So I believe you could write a more succinct version as:

  static getDerivedStateFromProps(nextProps, prevState) {
    const nextState = {
      client: nextProps.client,
      query: nextProps.query,
      lastResult:
        nextProps.query !== prevState.query ? null : prevState.lastResult
    };

    // Reset all component state if the Apollo client changes.
    if (nextProps.client !== prevState.client) {
      Object.assign(nextState, initialState);
    }

    return nextState;
  }

1: If it isn't expensive to derive "state" for a certain prop, then you can just do it in render and skip the getDerivedStateFromProps lifecycle entirely.

Edit: My initial suggested re-write didn't account for state reset when client changed because I overlooked it.

@wood1986
Copy link

wood1986 commented Apr 6, 2018

This is my subjective opinion and you can ignore if you want.

I think the problem with getDerivedStateFromProps is we have to create a duplicated reference pointing the props in state. And I do not like the duplication this.state.client and this.props.client in any methods of React.Component. Before 16.3, it guaranteed people would use this.props.client only. However getDerivedStateFromProps gives us a chance that people will use this.state.client. We may not be able to identify which one is the most reliable if the Component is getting complicated.

@catamphetamine
Copy link
Author

catamphetamine commented Apr 6, 2018

@bvaughn

I want to point out that props values only need to be mirrored in state if one of the following conditions are true:

There is a condition you didn't mention:

  1. componentWillReceiveProps examines several props values, without storing them in state because it doesn't need those values in state for any rendering. The component doesn't neccessarily apply the changed value from props to its own state - it depends on its own internal state at the moment and some other factors: it could be applied, it could be not applied. I already provided such an example ("default country" property) at the top of the discussion in comments to code: where the value from props is not stored in state because it doesn't need to. But it needs to be checked for changes everytime, resulting in this.state.props hack due to the lack of prevProps argument.

@bvaughn
Copy link
Collaborator

bvaughn commented Apr 6, 2018

I think the problem with getDerivedStateFromProps is we have to create a duplicated reference pointing the props in state. And I do not like the duplication

This is only necessary if one of the two conditions I mentioned above is true. getDerivedStateFromProps is not a commonly used lifecycle to begin with, and those conditions only apply to some of its uses.

There is a condition you didn't mention

I don't see how what you mention above is a new condition. (I think the two conditions I mentioned are sufficient, but please correct me if I'm misunderstanding.)

@catamphetamine
Copy link
Author

catamphetamine commented Apr 6, 2018

@bvaughn
My example was about a "default country" property for a phone number input widget.
#40 (comment)
The widget has state.country selected country and state.value phone number being input.
If "default country" property changes: if no phone number state.value has been input yet then the widget selects the "default country" as state.country, otherwise it doesn't react to the "default country" property change.
Maybe it's just my example being too exotic.
I could see that.
I'll let others roll into this thread with their examples, if there are any.

@gaearon
Copy link
Member

gaearon commented Apr 6, 2018

I think the problem with getDerivedStateFromProps is we have to create a duplicated reference pointing the props in state

I don’t understand this comment. You’re not supposed to add getDerivedStateFromProps to a component that doesn’t need it. That’s a migration strategy for componentWillReceiveProps for people who already needed to sync props to state for some reason.

We’re not encouraging you to add getDerivedStateFromProps to any components that didn’t already duplicate props in state. Could you clarify your concern?

@bvaughn
Copy link
Collaborator

bvaughn commented Apr 6, 2018

What you're describing sounds like it would fall under the first category I mentioned above:

  1. A changing props value needs to erase or reset a component state value, or...

The phone number you're storing in state isn't really relevant to mirroring a prop, because you would (presumably) have to store it in state either way- unless I'm misunderstanding your example.

@Malgalad
Copy link

Malgalad commented Apr 6, 2018

@gaearon my take on main concern of not having prevProps is @bvaughn's first point: computationally expensive operations.

Lets say I can derive B from A, but it's not trivial and I don't want to recalculate it unless A changed. But in order to know if A changed, I need to store it in state, and that means that getDerivedStateFromProps returns not only derived B, but also reference to A.

(And that might also means prevProps will not be garbage collected anyway, because of a reference in state - I think you mentioned it somewhere as a goal)

@bvaughn
Copy link
Collaborator

bvaughn commented Apr 6, 2018

(And that might also means prevProps will not be garbage collected anyway, because of a reference in state - I think you mentioned it somewhere as a goal)

I think it's important to point out two things:

  • Storing a single prop (prop A in your example above) in state is less impactful than holding on to all props
  • A single component holding onto a single prop (or even all props) is less impactful than React holding onto previous props for all components.

So that's still a worthwhile goal. 😄

@nwoltman
Copy link

nwoltman commented Apr 7, 2018

I'd like to add another example where this RFC would help. I have a custom RangeInput component (it renders both a range slider and a number text input) that needs to do an expensive computation (computeTextInputWidth) if any of three props change: min, max, step.

Before (using componentWillReceiveProps)

constructor(props) {
  super(props);

  this.state = {
    textWidth: computeTextInputWidth(props.min, props.max, props.step),
    textValue: '' + props.value,
  };
}

componentWillReceiveProps(nextProps) {
  const nextState = {
    textValue: '' + nextProps.value,
  };

  if (
    nextProps.min !== this.props.min ||
    nextProps.max !== this.props.max ||
    nextProps.step !== this.props.step
  ) {
    nextState.textWidth = computeTextInputWidth(nextProps.min, nextProps.max, nextProps.step);
  }

  this.setState(nextState);
}

After (using getDerivedStateFromProps)

constructor(props) {
  super(props);

  this.state = {
    min: props.min,
    max: props.max,
    step: props.step,
    textWidth: computeTextInputWidth(props.min, props.max, props.step),
    textValue: '' + props.value,
  };
}

static getDerivedStateFromProps(nextProps, prevState) {
  const state = {
    min: nextProps.min,
    max: nextProps.max,
    step: nextProps.step,
    textValue: '' + nextProps.value,
  };

  if (
    state.min !== prevState.min ||
    state.max !== prevState.max ||
    state.step !== prevState.step
  ) {
    state.textWidth = computeTextInputWidth(state.min, state.max, state.step);
  }

  return state;
}

If this RFC were implemented

constructor(props) {
  super(props);

  this.state = {
    textWidth: computeTextInputWidth(props.min, props.max, props.step),
    textValue: '' + props.value,
  };
}

static getDerivedStateFromProps(nextProps, prevState, prevProps) {
  const state = {
    textValue: '' + nextProps.value,
  };

  if (
    nextProps.min !== prevProps.min ||
    nextProps.max !== prevProps.max ||
    nextProps.step !== prevProps.step
  ) {
    state.textWidth = computeTextInputWidth(nextProps.min, nextProps.max, nextProps.step);
  }

  return state;
}

Having access to prevProps keeps the component simple because the min, max, and step props don't need to be copied into the state. This also ensures that there is a single "source of truth" for these values (in the component's props, instead of exactly duplicated in both props and state).

@wood1986
Copy link

wood1986 commented Apr 7, 2018

I don’t understand this comment. You’re not supposed to add getDerivedStateFromProps to a
component that doesn’t need it. That’s a migration strategy for componentWillReceiveProps for people who already needed to sync props to state for some reason.

We’re not encouraging you to add getDerivedStateFromProps to any components that didn’t already duplicate props in state. Could you clarify your concern?

I have not said we should implement the getDerivedStateFromProps no matter what. I am saying the migration you guys suggest is not perfect especially from the symmetry perspective. And this is very subjective.

As @gaearon say, we will only need this.state.client = this.props.client if it is necessary. And this is the asymmetry because some need or do not need the duplicated reference in this.state. I strongly believe most people are talking about the symmetry instead of the migration/solution/problem of componentWillReceiveProps. That's why I asked in #27 ,

If this.state will have props information via the implementation of getDerivedStateFromProps, what is the point of having nextProps and prevProps in shouldComponentUpdate and componentDidUpdate respectively?

Unless you are going to tell us the next version,

  • this.props no longer exists and is not accessible in any methods
  • shouldComponentUpdate will not have nextProps
  • componentDidUpdate will not have prevProps
  • Implementing getDerivedStateFromProps is a must in order to read the props

then I believe we(at least me) should not have the problem when looking at the example/suggestion of getDerivedStateFromProps. Because it meets the "source of truth" and the symmetry with these four breaking change. However, this is not going to happen.

In short, it's mainly about the symmetry and the habit of traditional/classical thinking of this.state and this.props

@TrySound
Copy link
Contributor

TrySound commented Apr 7, 2018

@nwoltman You don't need to duplicate logic in state initialization. This can be achieved with the first gDSFP call. Use something like default values.

  this.state = {
    min: 0,
    max: 0,
    step: 0,
    textWidth: 0,
    textValue: '',
  };

@bvaughn
Copy link
Collaborator

bvaughn commented Apr 7, 2018

I think the central theme of the most recent example is convenience? The difference between what's current and what's proposed is fairly trivial. Personally, I don't think this is a convincing enough reason to reverse the decision that was made on the RFC. (The convenience aspect has been discussed on the RFC and on some GitHub issues (e.g. facebook/react/issues/12188, /issues/27).)

@catamphetamine
Copy link
Author

catamphetamine commented Apr 7, 2018

@bvaughn
It is a purely convenience thing and it can be worked around.
If there are real reasons to remove prevProps from the method arguments - so be it.
So far no reasons were provided, aside from freeing prevProps from RAM.
Still, prevProps stay in componentDidUpdate so it feels weird and halfway-done/inconsistent.
And don't link some RFCs, no one's gonna read them.
If you can't explain something in simple words then you don't know/understand it.
That's not me, that's a well-known wisdom of explaining something to a child/grandma.
So far, no reasons have been given aside from the RAM footprint thing which is still invalid given that prevProps are still being stored and used at least in componentDidUpdate - if you removed them here then why did you leave them for componentDidUpdate. It's a clear contradiction.
So, to summarize, so far there are no valid reasons not to provide prevProps as an argument, and it feels like you're just being stubborn about not pushing this change through.

@nwoltman
Copy link

nwoltman commented Apr 7, 2018

@TrySound True, I could do that when initializing state, but my main issue isn't really code duplication, it's more the fact that the properties are duplicated in both props and state.

Also, there is actually a bug with that solution. If the component receives min={0} max={0} step={0} for the initial props, then the textWidth prop won't be calculated in gDSFP because the min/max/step values will be equal. So either textWidth will need to be calculated in the constructor, or the initial values for min/max/step will need to be carefully chosen so that they will never be equal to the initial props.

Initializing state with essentially "dummy values" feels like an anti-pattern to me. It seems useless to set values that are just going to be replaced when gDSFP is called and this also makes the code less clear (Person A: "What are all these empty values in state for?" Person B: "It doesn't matter. They're just going to get replaced in getDerivedStateFromProps"). Initializing state in this way also creates the bug I mentioned above where the initial state and initial props that are shared in the state are equal.

I think the developer experience around getDerivedStateFromProps could be improved by:

  1. Calling it only when new props are received (not during initialization)
  2. Passing prevProps to the function (this RFC)

2 becomes trivial once 1 is implemented since nextProps would be the newly-received props and prevProps would be the value of this.props. This API would also make it much easier to migrate to gDSFP since it should be mostly straightforward to refactor componentWillReceiveProps into gDSFP. As @catamphetamine mentioned, "saving memory" by not passing prevProps won't always be possible because prevProps needs to stick around for componentDidUpdate, and the trade-off between memory consumption and developer experience doesn't seem worth it to me.

@bvaughn In addition to convenience, it also saves on the amount of code that needs to be written (so less bytes need to be sent to the client).

@bvaughn
Copy link
Collaborator

bvaughn commented Apr 7, 2018

And don't link some RFCs, no one's gonna read them.
If you can't explain something in simple words then you don't know/understand it.

RFCs exist so the community can share feedback about new APIs. I wrote this particular RFC, so I do understand it, and I've read and responded to every piece of feedback on it (including ancillary issues like this), so it's quite reasonable for me to link to previous, similar discussions rather than repeat answers already provided. You don't have to read the previous discussion of course. That's your choice, although you may miss out on some context if you don't. But please, let's keep this discussion friendly.

As @catamphetamine mentioned, "saving memory" by not passing prevProps won't always be possible because prevProps needs to stick around for componentDidUpdate, and the trade-off between memory consumption and developer experience doesn't seem worth it to me.

As for the concern that saving memory is a bad argument because React passes prevProps to other lifecycles- please keep in mind that the React API evolves slowly so as to avoid introducing a lot of painful churn and fragmentation. It's true that this parameter is passed to other component lifecycles, but that doesn't mean that it will always be the case- or that it will be the case for all components. Perhaps React may one day support a component type with a limited set of lifecycles that don't require prevProps- or perhaps React may be able to stop retaining previous props if it knows that a component only defines lifecycles that don't use them. This particular justification (saving memory) was championed by Sebastian. He is someone who tends to think strategically, looking ahead a couple of major versions.

@bvaughn In addition to convenience, it also saves on the amount of code that needs to be written (so less bytes need to be sent to the client).

True, although this difference is also trivial in the context of an entire application.

@catamphetamine
Copy link
Author

catamphetamine commented Apr 7, 2018

@bvaughn So it's just keeping the API minimal then.
But I think you're simplifying things too much here.
If a person resorts to getDerivedStateFromProps(), then what's their scenario?
Why aren't they just using this.props?
Why do they need this.state?
If it was a simple calculation like:

getDerivedStateFromProps(props) {
  return {
    derivedValue: derive(props.value)
  }
}

Then there's really no need for getDerivedStateFromProps and the developer would get away with simply:

render() {
  const derivedValue = derive(this.props.value)
  ...
}

So there's no real usefullness in using getDerivedStateFromProps in this simple scenario.

Sidenote: Well, maybe there is usefullness in a sense that getDerivedStateFromProps usually gets called when props do change, though it's not clear from the docs due to them being vague about when exactly is this function called. Perhaps getDerivedStateFromProps is mostly being called on props change, therefore it's more efficient than just recalculating the derived props every time (on every component re-render). And a developer isn't required to use extra "memoization" code because of that.

Furthermore, dropping this.state paradigm and resorting to just deriving the value from this.props in real-time would enable "stateless components" which are more efficient and more simple, etc.
So by introducing getDerivedStateFromProps you're actually advocating bad practice and this is where you contradict yourself being a simplification/efficiency evangelist.

Now, my point is that when deciding on the getDerivedStateFromProps API you didn't see the real patterns and use-cases where the need for it arises.
The real need for using getDerivedStateFromProps arises only when there's some complex prevProps/nextProps comparison logic happening inside componentWillReceiveProps (all other cases can totally get away without using getDerivedStateFromProps at all).
So there's the need for prevProps/nextProps comparison and you still don't provide prevProps.
It's like supplying a soldier with a machine gun, but, you know, you have to craft your own bullets for that, because it's considered unsafe (bullets are a dangerous cargo, can explode, etc).

Perhaps getDerivedStateFromProps should be an advanced "opt-in" feature, and if a developer "opts-in" it means that he understands that prevProps will stay in memory, etc.
In other words, if you want to comply with the "minimal" React API for efficiency reason — then don't use getDerivedStateFromProps.
Otherwise, if you define getDerivedStateFromProps, then you're "opting-in".

@wood1986
Copy link

wood1986 commented Apr 7, 2018

Calm down first. I think we should have a poll in somewhere and voters have to provide an example. I would recommend React Core team should do it. Otherwise we do not have enough voices/examples to drive this.

@thysultan
Copy link

Unless i'm missing more context the argument against prevProps specifically related to RAM/memory sounds remanences of the FUD that surrounded inline functions. React is working at a high enough abstraction that the fear of a reference to a object that is expect to be GC'd taking more significant RAM sounds more worrisome than any other problem.

@catamphetamine
Copy link
Author

catamphetamine commented Apr 7, 2018

Actually, yeah, I'd vote for a poll.
I kinda feel that I'm pushing this too far sometimes.
At the same time, I kinda feel that if I abandon my position I'm betraying all other devs who're in the same boat.
Perhaps we should ask people on twitter.

@catamphetamine
Copy link
Author

catamphetamine commented Apr 21, 2018

@bvaughn Aha, I see, thx.
Well, yes, if a component is rewritten to account for the prevProps argument in getDerivedStateFromProps then it does break backwards compatibility with older versions of React (in this example, React < 16.4) because on React 16.3.0/16.3.1 the polyfill simply passes-through the getDerivedStateFromProps function as-is without polyfilling it and React 16.3.0/16.3.1 wouldn't pass the third prevProps argument therefore breaking such getDerivedStateFromProps which relies on that third argument to be present.
For you it was clear since you wrote the polyfill but for others it was unclear, so I guess it's clear now.

If React had some version number detection (which could perhaps be added to 16.4 but that would be too much hassle I guess) then the polyfill could differentiate between 16.3.0/16.3.1 and all further versions (>=16.4) so that it would still polyfill on 16.3.0/16.3.1 (React doesn't seem to emit warnings for cWRP/cWM in 16.3, though <StrictMode/> does but maybe that could be changed).

I have updated the first post in this thread with the new text clarifying the backwards compatibility issues.

Still, as the discussion proceeded, the library authors pointed out that adding a third prevProps argument in the next React version (say, 16.4) would not be of much use because the react-lifecycles-compat polyfill only polyfills for React <= 16.2 while for React 16.3 getDerivedStateFromProps is left untouched and that's why it would break on React 16.3 if it relied on the new prevProps argument being present. So, even if the prevProps argument was added developers wouldn't be able to use it in their libraries unless they'd like to break compatibility with React 16.3 and release a new major version of their library with a peerDependency of React >=16.4 and maintain two versions of the same library: one for React <16.4 and the other for React >= 16.4 (which is not a viable solution and which is exactly the opposite of what the polyfill originally aims to provide — backwards compatibility with all versions of React). It's a bit tricky, but seems that it's a valid point. See the library authors' comment. If React had some version number detection then the polyfill could differentiate between 16.3.0/16.3.1 and all other versions of React, so that it would polyfill on React <16.4 and not polyfill on React >=16.4.

If you have anything to add to / change in the first post in this thread then leave a comment.

@theKashey
Copy link

I am not sure, but what it example with memoization inside getDerivedStateFromProps, not render, could change some mind?

import memoize from "lodash.memoize";

// "one for all" memoization
const memoizedFilter memoize(allItems => allItems.filter((thing) => !thing.hide));

class ListSomeThingsV1 extends Component {
  state = {
     // "per instance" memoization
     filter: memoize(allItems => allItems.filter((thing) => !thing.hide));
  }
  static getDerivedStateFromProps(props, state) {
     return {
        filteredItems: this.state.filter(props.allItems)
      }
      // ps: you can compare oldState.filteredItems and newState.filteredItems to "detect" a change.
  }

  // ...
}

@bvaughn
Copy link
Collaborator

bvaughn commented Apr 21, 2018

@theKashey You could update the state value every time, using a memoize approach, yes- but there really isn't a point to using component state at all in that case. You should just do calculations in render.

Maybe you would not even need to use a class component and could just use a functional one- which would be faster anyway, e.g.:

import memoize from "lodash.memoize";

const memoizedFilter = memoize(allItems =>
  allItems.filter(thing => !thing.hide)
);

const ListSomeThingsV1 = ({ list }) => {
  const filteredItems = memoizedFilter(list);

  // ...
};

PS. I probably wouldn't suggest putting a filter function in state like that example shows. It would lend itself too easily to accessing this in a way that getDerivedStateFromProps was specifically written to discourage. 😄

@theKashey
Copy link

It is better to leave an important note about react and _memoization.

  • lodash.memoize has endless cache by default. Using it with SFCs will lead to memory leaks. But there are ways to limit it.
  • reselect or memoize-one has a single cache line. Using it with SFCs will make no sense, as long you will always "wash away" that single cache line.
  • per instance cache or per class cache. Or both. Chose wisely.

@bvaughn - what is actually the "proper" place to store memoized function - "this" or "this.state"? I personally prefer "state", as long you can access function from dev tools and inspect it. Or provide some useless information from another point of view.

@bvaughn
Copy link
Collaborator

bvaughn commented Apr 21, 2018

Yeah, Dan filed a follow-up issue for calling out the caching behavior 😄

Seems like lodash.memoize + a WeakMap custom cache might be a nice solution, or a different single-caching package like the ones you reference.

I personally wouldn't recommend putting functions (memoized or otherwise) on state since it could encourage unsafe side effects (e.g. calling prevState.someFunction from the static getDerivedStateFromProps lifecycle). I'd prefer putting it on the instance.

@dantman
Copy link

dantman commented Apr 21, 2018

Seems like lodash.memoize + a WeakMap custom cache might be a nice solution, or a different single-caching package like the ones you reference.

For when I want WeakMap based memoization or even endless memoization I've been using memoize-immutable recently. I have a distaste for the lodash style memoize that uses serialization.

There used to be a library that gave reselect a WeakMap based selector implementation, but the GitHub repo appears to have disappeared and the npm package corrupted.

Though there may be a different library worth looking into: https://heygrady.github.io/redux-selectors/

@gaearon
Copy link
Member

gaearon commented Apr 21, 2018

If you want something very simple and memoizing based on the last value is all you need, https://github.com/alexreardon/memoize-one looks like a good candidate (per instance).

@dantman
Copy link

dantman commented Apr 21, 2018

All of the above memoizers also work well with core-decorators' decorate. If you need to pass options, lodash's partialRight helps out.

import {decorate} from 'core-decorators';
import {partialRight} from 'lodash';
import memoizeOne from 'memoize-one';
import memoize from 'memoize-immutable';

class MyComponent extends Component {
	// Memoized with a single cache per-instance
	@decorate(memoizeOne)
	computeSomethingForInstance(foo, bar) {
		// expensive computation
		return result;
	}

	// Memoized globally with a WeakMap for a single argument
	@decorate(partialRight(memoize, {cache: new WeakMap()}))
	static computeSomethingNonPrimitive(foo) {
		// expensive computation
		return result;
	}

	// Memoized globally with an unlimited cache for a single argument
	@decorate(partialRight(memoize, {cache: new Map()}))
	static computeSomethingPermanently(foo) {
		// expensive computation
		return result;
	}

	// See https://github.com/memoize-immutable/memoize-immutable#choosing-a-cache-store
	// for other cache options involving multiple arguments, LRU maps, etc
}

@sullivan-sean
Copy link

Whats to stop somebody from doing the following as a workaround to access previous props? Just tested as a thought experiment and it seemed to work for me.

class MyComponent extends Component {
  static getDerivedStateFromProps(nextProps, prevState) {
    const prevProps = prevState.getOldProps();
    console.log(prevProps, nextProps);
    return null;
  }

  constructor(props) {
    super(props);
    this.state = { 
      getOldProps: () => this.props
    };
  }

  render() {
    return null;
  }
}

@catamphetamine
Copy link
Author

@sullivan-sean It isn't stopping anyone from anything.
Since this RFC didn't pass through everyone can implement their own workaround as they wish.

@dantman
Copy link

dantman commented Apr 26, 2018

@sullivan-sean I don't believe React guarantees that this.props accessed that way through the static getDerivedStateFromProps will be safe. i.e. At that point you might as well just keep using UNSAFE_componentWillReceiveProps. It's not going away, it's just unsafe to use in async render mode.

@bvaughn
Copy link
Collaborator

bvaughn commented Apr 26, 2018

@sullivan-sean That's a reasonable question 😄

The new lifecycle was made static to strongly discourage access to the instance (this) because of many potential problems that can happen as a result. It's possible to work around this (e.g. state = { instance: this };) but this wouldn't be doing yourself any favors and as Daniel said above, you should just stick with UNSAFE_componentWillReceiveProps if you prefer to go that route.

As Sebastian recently said, there are rules to React. At times they may feel limiting, but following them allows React to do many complex things for you (even more so with upcoming async features).
You can break these rules, but if you do- React will likely not work like it's supposed to, and you'll potentially lose a lot of time debugging and investigating why.

@catamphetamine
Copy link
Author

catamphetamine commented May 26, 2018

A warning though: the new React 16.4 might break your getDerivedStateFromProps() (in some cases).
An example: facebook/react#12912

@bvaughn
Copy link
Collaborator

bvaughn commented May 26, 2018

That is a mischaracterization of the changes made in 16.4, as the thread you linked to shows.

Let's keep the conversation about that issue on that issue and not expand it into unrelated threads (like this one).

@istarkov
Copy link

@bvaughn can you explain issues you have told a little bit more. For example why shouldComponentUpdate had not been moved into static method too (having that it has aceess to this and could be used in improper way?)
(I see the main idea of moving, btw scu causes some questions)

@istarkov
Copy link

Still cant get idea why eslint rule anecdotelly better

@bvaughn
Copy link
Collaborator

bvaughn commented May 27, 2018

For example why shouldComponentUpdate had not been moved into static method too

Our decision was a pragmatic one. We looked at a lot of React components (within Facebook and on GitHub) and we saw that componentWillReceiveProps was being used in a lot of places in ways that would break with async rendering. (This is covered in detail in the RFC.)

We didn't see this kind of misuse with shouldComponentUpdate though. I think this might be because the method was named better. Its purpose was more clear. So there was no reason to cause people pain to migrate away from when it wasn't likely to cause any problems.

We have a pretty high bar for changes like this because they cause a lot of churn (within the open source community and within Facebook).

Still cant get idea why eslint rule anecdotelly better

Don't really understand what this means.

@istarkov
Copy link

Don't really understand what this means.
https://github.com/reactjs/rfcs/blob/master/text/0006-static-lifecycle-methods.md#unresolved-questions

got you, Thank you.
looks like compromise decision, having scu static, almost all render phase cases (except instance access) could be implemented as function component

Copy link

@WOLVIE97 WOLVIE97 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, i guess that is a little better, lolol....😉, But 4real though nice pull on the decision of changes.

@marsonmao
Copy link

I just found the thread today, completely read it, and have several questions:

  1. If prevProps is the same as nextProps for the initial render. Then the "if nextProp.propToDerive !== prevProp.propToDerive then derivedState must be computed" rule is broken. The derivedState has not been computed and needs to be initialized because this is the first render. But prevProps is incorrectly telling gDSFP that the prop to derive has not been updated and does not need to be computed.

If someone is writing if nextProp.propToDerive !== prevProp.propToDerive then compute derivedState, the computation is only intended to happen when there is a !==, which means, if the condition is ===, it doesn't matter. I mean, this piece of code is very self-explaining, it wants to do something when the condition is !==, so we dont have to worry about it not working in the first render. If derivedState need to be initialized, I think it could happen in the constructor? And the default value could be anything, not needed to be related to props.

The question is: am I correct about the above point?

  1. So this example:
state = {
  derivedValue: undefined
}

static getDerivedStateFromProps(nextProps, state, prevProps) {
  if (nextProps.value !== prevProps.value) {
    return {
      derivedValue: fn(nextProps.value)
    }
  }
  return null
}

could be, and might possibly be:

state = {
  derivedValue: -1, // or some 'default-string' or anything
}

static getDerivedStateFromProps(nextProps, state, prevProps) {
  if (nextProps.value !== prevProps.value) {
    return {
      derivedValue: fn(prevProps.value, nextProps.value)
    }
  }
  return null
}

Because,
2.1, Like I said in 1., derivedValue could just have any default value, no need to be undefined.
2.2, if the computation happens inside !==, it is very likely the the computation depends on both prevProps and nextProps, so both parameters should go into fn(). A very simple example is that derivedValue is the difference of prevProps.value and nextProps.value.

The question is: is my example code making sense?

  1. The thing that getDerivedStateFromProps (or componentWillReceiveProps) provides is the ability to react to changes in props.

According to this, that's definitely why many people (including me) are eager for prevProps. And based on some example provided in this thread, we needs the prevProps so that we can compute the difference with nextProps (or this.props, anyway). And, since we only care about difference, it's no harm to let prev === next happen inside getDerivedStateFromProps , no matter it's initial render or upcoming renders.

The quesiton is: is the above making sense?

And the final question: if the above are correct and making sense, could prevProps be provided and equals to nextProps in the initial render?


As for my own use case, I'm building a chat app, and I'd like to know the difference of previous and current chat log length before rendering. What I'm trying to do in getDerivedStateFromProps is:

const lengthDiff = props.logs.length - prevProps.logs.length;
return {
  scrollToIndex: lengthDiff,
}

So that I can render the log list with latest logs, and scroll to my desired log index. However, at the moment, I can only do this in componentDidUpdate because that's the only life cycle with prevProps. I know I can add prevProps or prevProps.logs or whatever I need into state, but like the others I also doenst feel like it's better than just passing prevProps as the parameter of getDerivedStateFromProps.

@bvaughn
Copy link
Collaborator

bvaughn commented Jun 13, 2018

Hey @marsonmao,

What you're saying make sense. I understand that explicitly tracking e.g. state.prevLogs adds a few more lines of code to your component, and I understand why you wouldn't like that.

I don't think the points you've raised are new though. (Similar issues have already been discussed above.) And I don't see much value in repeating covered topics. It just makes a long thread longer. 😄

In most cases, there are alternatives to getDerivedStateFromProps that end up working just as well and requiring less code. Our recent blog post covers that, so here's a link if you haven't seen it yet:
https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html

@marsonmao
Copy link

marsonmao commented Jun 14, 2018

@bvaughn Thanks for the reply! Yeah I think I basically agree with you, the post is long enough, but I think my use case is new, and it should be good to provide more real examples like that. And I've read the post, it's very helpful, truly clarify several misconceptions, but I think it did not cover types like my use case, which requires to know the difference of props changes. And, the 4 proposals are discussed, but only the "nextProps as prevProps" one is not ansered/discussed very well, so that I posted the questions. Hope it's okay to do so.

My proposal is not new, that's true, and I think I asked wrong questions. Oh and, before asking the new question, I should state that I totally agree with NOT ADDING prevProps into getDerivedStateFromProps for now, no matter what the reasons are; so that's right, I'm not asking anyone to change the API. But, the new (and final, I guess) question is: If, hypothetically, prevProps is going to be added, then, will the nextProps as prevProps for the initial render be the chosen solution?

P.S. I've seen the comment but it's the same topic so I only post here 😁

@bvaughn
Copy link
Collaborator

bvaughn commented Jun 14, 2018

Thanks for being understanding.

If, hypothetically, prevProps is going to be added, then, will the nextProps as prevProps for the initial render be the chosen solution?

Honestly, I don't believe this change will ever be made. It would cause too much confusion and churn in the ecosystem (as explained in this comment). My hope going forward is that fewer components use derived state, and so the awkwardness of using it becomes less important.

And the final question: if the above are correct and making sense, could prevProps be provided and equals to nextProps in the initial render?

But if, hypothetically, we did add this parameter in the future– the downside of the approach you mention would be that it relies on you to initialize the entire state object in the constructor, or your component's state will be invalid for the initial render.

Even your example above looks problematic to me:

state = {
  derivedValue: -1, // or some 'default-string' or anything
}

static getDerivedStateFromProps(nextProps, state, prevProps) {
  if (nextProps.value !== prevProps.value) {
    return {
      derivedValue: fn(prevProps.value, nextProps.value)
    }
  }
  return null
}

Presumably "-1" is not a valid derivedValue since it isn't in any way based on, or derived from, the initial props.value. So if nextProps and prevProps were equal for the first call to getDerivedStateFromProps, then the example component above would essentially ignore the initial props value and have the wrong derived state.

But I don't think there's a lot of value in discussing hypotheticals here. 😄 Have a nice day!

@marsonmao
Copy link

marsonmao commented Jun 15, 2018

Honestly, I don't believe this change will ever be made.

That's totally fine.

the downside of the approach you mention would be that it relies on you to initialize the entire state object in the constructor, or your component's state will be invalid for the initial render.

I think this is okay, assign member variables (state object is one) initial values in the constructor is very intuitive I guess?

Maybe the problem is that getDerivedStateFromProps is responsible for too many tasks? In the questions I posted I'm only focusing the "props changes" part of it. Because it's the only life cycle that fires before render, so I'd like to know how the props changes in this render then I can derive something. And the initial render did not make any "props changes", so it's no matter in this situation.

My hope going forward is that fewer components use derived state, and so the awkwardness of using it becomes less important.

I can live with that, if it's the library authors' suggestion. But I think the people calling for prevProps all needs to "detect the props changes before render", and this is the only life cycle could do that. Unless React got some new life cycle or other re-design or something, I'm not sure how do we avoid it?

P.S. I'm not discussing how to DO it, the way to do it is like dicussion bofore, like copying props into state or writing wrapper function or whatever.

Even your example above looks problematic to me:

Maybe or maybe not I think, the value of -1 can be checked inside render() so the final values could be further decided. I mean, since the real value is derived from changes, whenever there is no changes or changes not happened yet, the derived value is invalid or a safe value are all fine.


I'll give my real example here to leave some record. I'm building a chat log list, it behaves like Slack, when you scroll upwards and is at scroll top 0, it fetches more logs, after fetch complete the scroll top should be adjusted to a proper location so that the view stays at the log that trigger the fetch (Anyway it's like Slack, if I'm not describing it good enough). So I assign the scroll index to the difference of previous chat log length and the current length. And actually one of my attemp is to use react-virtualized.

state = {
  scrollToIndex: 0,
};

componentDidUpdate(prevProps, prevState, snapshot) {
  if (prevProps.chatObjects.length !== this.props.chatObjects.length &&) {
    this.setState({ scrollToIndex: this.props.chatObjects.length - prevProps.chatObjects.length });
  }
}
  
render() {
  return (
    <VirtualList
      width={width}
      height={height}
      itemCount={this.props.chatObjects.length}
      itemSize={this.getItemSize}
      renderItem={this.renderItem}
      onItemsRendered={this.onItemsRendered}
      onScroll={this.onScroll}
      scrollToIndex={this.state.scrollToIndex}
      overscanCount={20}
    />
  );
}

I know it always render twice for each fetch, but somehow I need that to happen for item sizes to be correctly calculated, so it's...fine (not so fine though). But if I could do the same logic before render then it's not bad, at least the scroll index could be calculated in a better place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet