facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
227.61k stars 46.44k forks source link

The minor release of 16.4 causes BREAKING changes in getDerivedStateFromProps #12898

Closed mririgoyen closed 6 years ago

mririgoyen commented 6 years ago

According to semver, only non-breaking changes are supposed to go into minor and patch version releases. With the release of 16.4, you have made breaking changes to getDerivedStateFromProps. Our entire codebase, running perfectly on 16.3.2 is a dumpster fire as soon as we raise that dependency to 16.4.

The only thing in your CHANGELOG about this breaking change is:

Properly call getDerivedStateFromProps() regardless of the reason for re-rendering. (@acdlite in #12600 and #12802)

Please revert this change and save it for 17.0.0, or provide proper documentation to what this change actually entails so that its use can be adjusted by those who have already implemented getDerivedStateFromProps.

gaearon commented 6 years ago

Thanks for elaborating! I think we don’t disagree that much. This rings true to me:

It warranted more of a heads-up to the community

We’ll take this feedback to the heart.

I stand by saying that this is a poorly communicated bugfix for an (also poorly communicated) feature rather than a semver-major change. But we could definitely have explained it better in retrospect, and it’s our fault.

For what it’s worth we did mention in the blog post that unsupported patterns that worked in some cases (but not others) by accident would break more consistently.

I see why you’re saying it was annoying to debug, but it’s not like we made this change silently. In fact a large part of the text in 16.4.0 blog post is dedicated to this exact change. If you saw breakage after upgrading, it would make sense to search your code for getDerivedStateFromProps (and patterns described in the post) rather than try to find a bug for two hours without that context. I’m not sure whether this is what happened, and I’m definitely not saying it’s your fault, but we did make effort to explain what issues you may run into.

Above all I appreciate everyone’s willingness to engage in this conversation. Again, we’re sorry we let the original version stay out for enough time for you to start depending on the buggy behavior. In fact the biggest reason we scrambled to get 16.4 out sooner was because we realized the mistake a few weeks ago (and we’ve been testing the impact of the fix for these past two weeks, which gave us confidence that the change is scoped enough and the existing bugs it uncovered were important enough to ship the fix now).

mririgoyen commented 6 years ago

@gaearon We didn't knowingly upgrade. I wasn't even aware 16.4 had dropped until someone on my team said, "Hey, look at the React version, it changed."

Our company's policy is ^, so we automatically got the latest version on our next fresh pull, and subsequent yarn, of the codebase, which just happened to be that same night. Under normal circumstances, we may not have discovered the issue for a few days. By that point in time, I'm sure one of us would have saw the release announcement and would have attacked things in a different way.

gaearon commented 6 years ago

Oh, I see. I assumed you’re using lockfiles since you mentioned Yarn (and npm uses lockfiles by default now too). I understand your sentiment better now.

I think in general using lockfiles is desirable for apps. We’re not infallible and there’s always a chance we could introduce a bug or regression in a release.

I totally understand if it’s a policy though and you can’t affect it.

kohlmannj commented 6 years ago

@gaearon Hi! I have a real-world use case (albeit in a nascent library) which breaks in React 16.4 (was on 16.3.2). As I write this, I am investigating a refactor of my code; so here my goal is simply to provide you with an example of something that broke.

In kohlmannj/limina, I've implemented a custom 2D ScrollView component (sighhh don't ask). Here's a working demo of the component; the master branch and that Storybook build uses React 16.3.2 as of this writing.

Try clicking and dragging the scroll bar caps, as shown in this tweet. That interaction corresponds to one or another call to this.setState() within onBeginDrag(), a higher-order method (sic?) whose returned event handler is attached to window.mousemove.

I have also implemented this getDerivedStateFromProps() static method, which serves the following purpose:

Now, I'm sure you can guess where this is going. In theory this can indeed be fixed by lifting state up, which I will certainly consider in my refactor.

* Since I'm actively developing the component / UI library, I am still considering the implications of an "uncontrolled" ScrollView that entirely manages its own internal state vs. a "controlled" version.

Here's how I discovered the breakage in my code with React 16.4: I upgraded both react and react-dom, [re-]started Storybook, navigated back to the "with resizeable kitten" story, and found the component no longer responded to dragging the scroll bar caps. (Here's a reference commit: https://github.com/kohlmannj/limina/commit/c430a909c5a48936aa669cd3b7d16935c474f81d)

It seems as though the call to this.setState() within the window.mousemove event handler now has no effect, since getDerivedStateFromProps() is now being called whenever the component updates, thereby repudiating the update from setState(). Sure enough: when I temporarily commented out getDerivedStateFromProps(), I found that the component reacted to dragging the scroll bar caps once again.

Anyway, as mentioned, I will look into refactoring my code; this is not a major inconvenience. I hope the use case and explanation thereof helps you and the team better understand how getDerivedStateFromProps() might be used out in the wild. Additionally, I had a lockfile in place, so this was a voluntary upgrade. Finally, I personally feel satisfied with the mantra-like, albeit relevant recommendation to "lift state up".

klimashkin commented 6 years ago

Probably getDerivedStateFromProps could be renamed to just getDerivedState to stop confusing it with componentWillRecieveProps and emphasize that it doesn't depend on props change only. Because it literally derives state on each render, not just on props change.

kohlmannj commented 6 years ago

@klimashkin Interesting. It's true that I went into my 16.3 updates thinking that getDerivedStateFromProps() was more or less a drop-in replacement for componentWillRecieveProps(). When I look at your proposed getDerivedState() naming, it does make me wonder if the visual pattern recognition of seeing *Props at the end of the method name threw me off…

Rather than a rename, though, perhaps the React docs could, at minimum, emphasize the need to holistically consider a component's state management strategy when transitioning away from componentWillReceiveProps().

It might also help to point out that getDerivedStateFromProps(), especially by nature of its updated behavior in React 16.4, should be used only when the component exclusively derives its state from props, rather than other state management strategies, such as deriving state updates from event handlers.

(I'm not sure I have the heuristic or the example exactly correct above, but merely asking myself to holistically consider my component's state management has clarified some things.)

Dema commented 6 years ago

@kohlmannj For me it is not a bug in react in any case, it's just these two pieces of code are just not equivalent

public componentWillReceiveProps(nextProps: IScrollViewProps) {
    if (nextProps.scaleX !== this.props.scaleX && typeof nextProps.scaleX === 'number') {
      this.setState({ scaleX: nextProps.scaleX });
}

and

 public static getDerivedStateFromProps(nextProps: IScrollViewProps, prevState: IScrollViewState) {
    if (nextProps.scaleX !== prevState.scaleX && typeof nextProps.scaleX === 'number') {
      return { scaleX: nextProps.scaleX };
}

In cWRP you are comparing old prop value with the new one and in gDSFP you are comparing it with the derived value, that is going to change inside you component. I think now with gDSFP you just have to save the old scaleX value in state, too, to be able to compare new prop against it. Am I correct?

if(nextProps.scaleX !== prevState.oldScaleX && typeof nextProps.scaleX === 'number') {
    return {oldScaleX : nextProps.scaleX, scaleX : nextProps.scaleX}
}
kohlmannj commented 6 years ago

@Dema Ah, I see what you mean! Thanks for your analysis. I implemented this change: https://github.com/kohlmannj/limina/commit/7df272930cc2a9bbf227c2fbc94782433ac6f81f

It's identical to what you've described, except I felt more comfortable naming the state properties this.state.initialScale[XY].

gaearon commented 6 years ago

@catamphetamine

I already asked you to refrain from name-calling about a month ago in another thread. Please stop this.

I am not “blaming” other people for their code being buggy. I’m sorry if I came across this way. I am pointing out that the bug you’re running into already existed in your code and is not a new one. I have analyzed it in detail in this thread, including providing a demo.

This is not a value judgment about you or your code. If we want this discussion to be fruitful, I think it might help to separate the technical details from emotions.

The same is true for your snippet in https://github.com/facebook/react/issues/12912 with React 16.3. Your phone input would completely reset if onChange callback doesn't immediately set the same state, and the parent component re-renders for another reason. If you turn this into a fiddle I’ll be happy to demonstrate that.

Again, I’m sorry that this bug was surfaced by the change (and it frustrated you) but it already exists in the code you wrote with React 16.3. I’m not trying to blame you, or make you feel bad. I’m only stating the fact about the code. And, as I mentioned earlier, we don’t consider making existing bugs in your code occur more often to be a breaking change.

But we definitely could do better at communicating how to use getDerivedStateFromProps without introducing bugs, and the blog post we plan to write will hopefully help that.

Peace.

catamphetamine commented 6 years ago

@gaearon Ok, thx, I removed my comment. I have been heard which means we're not being opressed here. I had to double-check that. It's good that React is having a renaissance and is evolving again instead of just "dying off" like yet another used-to-be-so-hyped framework. Your changes are controversial but I'm fine with that.

gaearon commented 6 years ago

Thanks. Btw I'd be happy to look at the issue in your input component but no earlier than Tuesday (due to holidays).

catamphetamine commented 6 years ago

@gaearon You're being too nice : ) I certainly wouldn't waste your time on something like that. Don't even know how you stay productive taking part in each and every discussion on github. I can handle the fix, seems straightforward actually, I just couldn't simply pass by such an engaging discussion silently, that's about it. Heated discussions are fun.

bvaughn commented 6 years ago

@kohlmannj Regarding the pattern you described above, I wanted to reiterate a concern in case it's helpful. (This isn't exactly related to getDerivedStateFromProps but more about the overall pattern.)

You described your component as:

  • The component exposes this.props.scaleX and this.props.scaleY, which are used to derive initial this.state.scaleX and this.state.scaleY values
  • Incoming number-type props changes to this.props.scaleX or this.props.scaleY should replace the corresponding this.state value
  • This supports the need for an interactively rescaleable scroll view which can either be used in "uncontrolled", standalone form; or, in the future*, as controlled by a parent component

My concern is that mixing controlled and uncontrolled behavior like this leads to a confusing API and/or confusing runtime behavior. At a high level, it's not clear what the source of truth is when props and state values disagree. More practically speaking, there are two variants this pattern usually takes- and each has downsides.


Variant 1: State is always reset to props when the component is re-rendered

The recent getDerivedStateFromProps change highlights one downside of this approach, but the problem also existed for version 16.3 as well as older versions built on top of componentWillReceiveProps.

The problem is this: Unexpected re-renders override your component's state unintentionally. This includes changes to unrelated props like callback functions or inline styles. These are often re-created inline during render, and so bypass PureComponent or shouldComponentUpdate purity checks.


Variant 2: State is only reset when props value changes

This pattern compares incoming props to previous props and only updates state when the value changes externally. (This is done to avoid the unexpected re-render problem I mentioned above.)

However this also has a downside: There's no good way for the parent to reset a property to the previous value.

Here's an example scenario:

  1. A ScrollView component like yours is used to render a lot of form fields and underneath of them, there's a "submit" button.
  2. A user fills out the fields and tries to submit, but there's an error with one field.
  3. The application renders <ScrollView scrollTop={posOfErrorField} {...props} /> to automatically scroll to the field with an error so the user can see it.
  4. The user modifies the field, then scrolls down and submits again.
  5. There's still an error with this field.
  6. The application renders <ScrollView scrollTop={posOfErrorField} {...props} /> to scroll back to the problematic field so the user knows it still has an error.
  7. Nothing happens. (State is not updated, because posOfErrorField hasn't changed.)

This would be confusing for both the user and the developer trying to debug this problem. And the only work arounds are:


We recommend designing your components to be either fully controlled or fully uncontrolled to avoid the above cases.

kohlmannj commented 6 years ago

@bvaughn Excellent notes — thank you very much. As it happens, I tentatively considered a change to fully uncontrolled in https://github.com/kohlmannj/limina/commit/d68e23c7bcb8cbbb872fd4d714848e5826673ed1. Based on this discussion, I will work on entirely separating controlled and uncontrolled behavior as I continue development.

gaearon commented 6 years ago

@bvaughn Thanks for writing this. I love how this thread helped crystallize these problems in my head. I’ve been vaguely aware of them but it was hard to articulate what exactly was broken in such APIs. Looking forward to the blog post!

catamphetamine commented 6 years ago

EDITED

I could provide another example which could potentially be changed to a fully "controlled" one.

So, I have a phone number input component. It has a country select with a flag icon and a phone number input field. The value should be able to be set externally at any point in time, and when it does the country select flag must update accordingly.

So, say, one tries to make such a component a controlled one. In this case value is taken from props. And the country flag is stored in state and must be contstantly re-verified against props.value in order for the (value, country) pair to stay consistent.

static getDerivedStateFromProps(props, state) {
  const { value } = props
  const { country } = state
  const newCountry = getCountryForValue(value)
  if (newCountry && newCountry !== country) {
    return { country: newCountry }
  }
  return null
}

So, I'll be computing getCountryForValue(value) on each keystroke. It's already being called in <input onChange/> handler. It can be viewed as a computationally-heavy function which slows down performance (it has to parse things and compare them to a lot of regular expressions in order to find the suitable country phone numbering plan entry).

One may say that this "controlled" approach makes the component 2x times slower by making it compute getCountryForValue(value) two times (one time inside <input onChange/> handler and the other time inside getDerivedStateFromProps()) but in reality getCountryForValue() could be "memoized" with a limit of 2 arguments and so the call to getCountryForValue() inside getDerivedStateFromProps() will be free.

Looks like memoization really saves the "pure-functional" approach performance in a real-world scenario.


This was a simplified example though. In reality getCountryForValue(value) is too slow to be performed on each keystroke inside <input onChange/> handler, so inside <input onChange/> handler another ("lite", "reduced") function is being called, which only operates within the bounds of the selected country only (which saves performance). So getCountryForValue(value) actually will make an impact inside getDerivedStateFromProps(). Still, it's a very specific case, and it's more of an exclusion than a general case. Maybe I will still keep the component stateful and won't transition it into a purely stateless function to optimize for performance.

bvaughn commented 6 years ago

@catamphetamine The example code you show above looks more like a controlled component to me. (It does not store the current phone number "value" in state; it gets it from props.)

In the example you show above, you don't actually need to use component state at all. The core of what you're doing is deriving something (which flag to show) from props in a way that performs well. All you need to do this sort of thing is a memoization wrapper.

If you think otherwise you can answer. But don't write lengthy comments.

👍

gaearon commented 6 years ago

I want to add a note to https://github.com/facebook/react/issues/12898#issuecomment-392266811 before I forget about it again.

Variant 2: State is only reset when props value changes

This pattern compares incoming props to previous props and only updates state when the value changes externally. (This is done to avoid the unexpected re-render problem I mentioned above.)

However this also has a downside: There's no good way for the parent to reset a property to the previous value.

Here is another scenario where this is a bad solution. Often people want to keep edits to a form in local state like state.value (and “commit” them to some place above, such as a Redux store, on an event like button click).

Let’s say we have a UI like this:

[button: View Previous]
[button: View Next]

Edit value: [input: Value]

[button: Save] 

You can navigate items with Previous and Next buttons (and the text input should reset to the current values for them), and you can click Save to save your edits.

To implement this, people sometimes try to use componentWillReceiveProps or (now) getDerivedStateFromProps. The logic goes like this: we initialize state.value to props.value. And if props.value changes “from above“, we want to throw away local edits (we assume the change from above comes due to Previous or Next click). However, this logic is buggy.

Consider the case where we navigate between two items with Previous and Next buttons. If their values are different, sure, a re-render will reset the field.

But what if two neighbor items have the same value? Then the condition you’re using to determine it (something like props.value !== state.previousValueFromAbove) will be false, and you’ll keep local edits even though the current item has changed, and they’re not valid anymore!

On the other hand, if your condition looks like props.value !== state.value (and compares to current input value), apart from your code breaking in 16.4, in 16.3 you would also get buggy behavior I described in earlier comments (every parent re-render would blow away your state).

So how can you make this pattern work? Really, the best way is to avoid it, and instead explicitly make the component fully controlled (so that the state can be managed by the component above).

But if you must do something like this, you can accept a prop like current item ID, and keep the previous ID in state. Then in getDerivedStateFromProps() you know the current item has changed and you need to reset the state. Just having the current value changing from above doesn’t really tell you enough to make a decision.

sophiebits commented 6 years ago

So how can you make this pattern work? Really, the best way is to avoid it, and instead explicitly make the component fully controlled (so that the state can be managed by the component above).

But if you must do something like this, you can accept a prop like current item ID, and keep the previous ID in state. Then in getDerivedStateFromProps() you know the current item has changed and you need to reset the state. Just having the current value changing from above doesn’t really tell you enough to make a decision.

Another option is to not implement getDerivedStateFromProps at all – meaning your child component won't reset when you move to the next item – but then specify a key on the child that the parent determines using the ID of the item. That will mean that when the key/ID changes, the child is completely remounted. Note that this may be worse for performance in some cases but is quite possibly the semantics you want. (It depends on the use case though.)

Essentially, it boils down to this: When you use getDerivedStateFromProps (or componentWillReceiveProps), your component is guessing at what has happened based on the old and new values. Even if you store the old and new values and compare them properly (as we've been suggesting in this thread), you're making a guess at what the parent wanted you to do. In many cases this won't cause problems – for any parent–child pair, it may be that the child will always guess correctly because the parent may only change its state in certain ways. But for a fully reusable child, it's not a good idea to use these methods. Because when you pull them into a new parent that behaves differently, they might do the wrong thing.

gaearon commented 6 years ago

Here is a few smaller examples (with a counter) if someone prefers to see the code.

Before (using componentWillReceiveProps): https://codepen.io/ismail-codar/pen/gzVZqm?editors=1011

Demonstration of why this code already has a bug: https://codepen.io/anon/pen/jxgXgK?editors=1011. Click “demonstrate bug” and you’ll see that completely unrelated setState in parent blows away the child state.

A correct solution that needs neither componentWillReceiveProps nor getDerivedStateFromProps, and instead lifts state up to the parent so it can decide what to do in either case: https://codepen.io/anon/pen/erqXpz?editors=1111. Note how I am now forced to deal with the complexity of this UI. For example, I had to explicitly decide what needs to happen if I edit the “initial value” field after interacting with the counter. In my implementation I ignore these edits, but you can do something else. The important part is that this is now explicit.

klimashkin commented 6 years ago

I don't think there are some huge problems with controlled/uncontrolled handling in getDerivedStateFromProps, it's all solvable.

But on the other side I don't understand purpose of getDerivedStateFromProps if you say 'just lift your state up' (I assume it doesn't matter if that upper state holder is another component or redux selector). If you follow 'just lift your state up', you don't need local component state anywhere at all. And you don't need getDerivedStateFromProps ever as well. Then react team should make components stateless and thisless (get rid of classes) to force that approach. If last is an option that react team is considering now (and getDerivedStateFromProps is an early bird), just let us know so we can change mindset in advance.

Also the quote from docs 'getDerivedStateFromProps is invoked right before calling the render method' not quite correct, because there is shouldComponentUpdate in between, it will make people think they can move derive logic directly into render method (where they have access to this again).

gaearon commented 6 years ago

@klimashkin

Then react team should make components stateless and thisless (get rid of classes) to force that approach. If last is an option that react team is considering now (and getDerivedStateFromProps is an early bird), just let us know so we can change mindset in advance.

While we’re always considering alternatives to classes, as we have done for the past four years, local component state is an essential feature of React (pretty much the most important one), and it’s not going away anywhere. I think you might be taking my comment to an extreme I didn’t intend.

But on the other side I don't understand purpose of getDerivedStateFromProps if you say 'just lift your state up' (I assume it doesn't matter if that upper state holder is another component or redux selector). If you follow 'just lift your state up', you don't need local component state anywhere at all.

I didn’t mean you should always lift it up as high as possible. I meant that it’s bad to have duplicate sources of truth that disagree with each other about any particular value. Lifting state up can solve that problem: basically, it forces you to choose one source of truth, and get the data flowing down from there. I didn’t say “lift all the way up“, just enough to solve the practical issue.

Even if you lift it up to their shared ancestor, it’s still local state in the component that owns it. But if you have two local states in two components that really are supposed to represent the same conceptual value, and “syncing” them is a pain, this is a sign that lifting state up to their common ancestor (but no higher than necessary) can resolve the ambiguity and the bugs it causes.

Lifting state up isn’t a panacea either. I’m only suggesting it as a solution when you’re literally trying to sync two values. I think that if a prop and state field are named the same way, it’s a good rule of thumb that getDerivedStateFromProps might not be an ideal solution because it’s just a symptom of a duplicate source of truth.

But there are legit cases for getDerivedStateFromProps that are not solved by lifting state up.

Sometimes state really is both local and derivative. For example, you may want to reset displayed suggestion list when the user changes a field. The suggestions themselves might be an implementation detail of a component, and you might not want to lift them up (thus causing every parent to manage them). I think this would be a legitimate case for using getDerivedStateFromProps: you reset some local state in response to a prop change, and the parent doesn’t need to be aware the derived value even exists.

If I were to come up with a checklist for when using getDerivedStateFromProps makes sense, it would be something like:

EyalPerry commented 6 years ago

@gaearon

Here is another scenario where this is a bad solution. Often people want to keep edits to a form in local state like state.value (and “commit” them to some place above, such as a Redux store, on an event like button click).

I happen to be working on something very similar, and do have some additional points to list. I would love your input as I believe there's something you did not address in your example of the single input form in the comments above.

Envision an app, which, upon navigation to some route, loads a json representation of some Form, a model, if you will- and then renders this model using React. Along with the model, data is fetched from the API so that the generated UI is populated with it. The app enables the user to edit this form and to send it back to the server. Dead Simple.

While the model is fetched only upon navigation, the data can change at any given point due to server side pushes.

Both data and model come in as props from the Redux store, and since I do not want to dispatch an action to the store on each key stroke, changes are cached locally in the form component's state, then validated and integrated with the data upon submission.

So, with your suggestion- that indicates that child components of said form should be controlled, The render method of the said form would be invoked whenever a child changes. This operation, albeit light thanks to react, is quite redundant, since it was not the model that changed. It was just a value on a single input element. Adding to that, this is no task to repeat unnecessarily on mobile devices, or worse- IoT devices with a UI.

So, in order to try and avoid this, as long as sCU is treated as more than just a hint, the form component only re-renders when either the model or the data have changed- an event which could only originate from a change to store state.

This forces each element to handle it's own state changes, and notify the form of these changes. This enables single elements to re-render, without affecting their siblings.

This, in turn, creates a tiny redundancy since changed element state is duplicated, but that is something I am willing to live with.

Each element's gDSFP method is implemented such that their initial value is stored in their local state, So the when the next call to this method comes, the local state is reset only if the value differs from the initially passed value, which only happens if the server decided that it should change.

Is there something I missed, that makes each element susceptible to the bug you described? Is this implementation considered "problematic" in regard to compatibility with future react versions? Is there another way to implement said form without causing the form to re-render (albeit virtually) on each element change?

Sincerely Eyal Perry

gaearon commented 6 years ago

Why are you not putting sCU/PureComponents on inputs themselves? If most of them bail out I think you might be overestimating the cost of updating “the whole form”.

gaearon commented 6 years ago

Is there something I missed, that makes each element susceptible to the bug you described?

Yes, any change that would make Form re-render more often (e.g. if it subscribes to some Redux piece of state that updates independently of the input, or has its own local state) will reset the inputs. You may not have this bug right now but it is very fragile to rely on this not ever happening.

EyalPerry commented 6 years ago

Thanks for taking the time to reply.

I have indeed put sCU on the stateful form elements. guess I was not clear about that. The cost I am trying to avoid stems from 'parsing' the same json model into the final result returned by the Form.render call, despite the fact that in all cases, except for the initial render, all but one bail.

About your second comment.. well, what you described is the desired and expected behavior of said Form. Store wins. Thing is, the only way this state ever gets updated is when the user receives notification of an incoming change, and takes explicit action to apply changes to the "local state".

Either way, I understand from your words that the React way, as of version 16.4 is that gDSFP should be implemented to be idempotent and that the only risk free practice is to lift state up.

The thing is, there's no "official" way to tell whether or not a state change is in order due to props change, which lead to the length of this thread and it's kin.

Perhaps some sort of "recommended pattern" should appear in the gDSFP documentation under the Component reference section.

Again, thanks a lot.

gaearon commented 6 years ago

The cost stems from 'parsing' the same json model into the final result returned by the Form.render call, despite the fact that in all cases, except for the initial render, all but one bail.

You can memoize it and then there's no extra cost. For example using https://github.com/alexreardon/memoize-one.

About your second comment.. well, what you described is the desired and expected behavior of said Form.

I understand your point that form state should win. But imagine you add a different kind of state to global store that form cares about (but that's not necessarily this form's state). You wouldn't want that other state to reset the form state.

In other words, it's generally expected that adding something to mapStateToProps result doesn't break the application.

the only risk free practice is to lift state up

Not necessarily, but it's the one that gives you the most flexibility. We'll publish a blog post soon with an overview of different approaches and their pros and const.

EyalPerry commented 6 years ago

Thanks, I will have a look. That blog post will be most welcome. I also understand your point, really healthy outlook. Best regards, and thanks for taking the time.

evoyy commented 6 years ago

@gaearon

While we’re always considering alternatives to classes, as we have done for the past four years, local component state is an essential feature of React (pretty much the most important one), and it’s not going away anywhere.

Thanks for stating this, it reassures me a lot. I'm not a functional purist - I use local state a lot, combined with MobX observables. Just know that there are people like me out there :-)

gaearon commented 6 years ago

FWIW we don't think local state is at odds with being functional. But that's a topic for another day.

onethreeseven commented 6 years ago

@gaearon

For context, I am professionally a backend developer but have recently been learning Javascript and React for a personal project. So if the verdict is that I don't know what I'm talking about, it's probably because I don't know what I'm talking about. :smile:

I have a relatively simple case which works in 16.3 and is failing in 16.4, and in addition to hoping you can shed some light on what I'm doing wrong I hope my newbie experience can inform your upcoming post.

The component consists of a radio button and text inputs:

() I would like to donate financial aid: $[    ]
() I would like to request financial aid: $[    ]
() I decline to participate in financial aid

The state that is useful externally is a single value, which may be positive, negative, zero, or "invalid" (i.e. they selected one of the top two lines but didn't input a number) which I represent as NaN. I get the impression that controlled components are preferred, and I do occasionally have to reset the value from above, so I implemented this as a controlled component.

But the state of the component is not fully determined by the controlled value: if the user selects one of the top two radio options, before entering a number the value is NaN in either case. The parent only needs to know that the input is in an invalid state, so to make it cleaner I hid this detail inside the component.

In 16.3 I deal with this using getDerivedStateFromProps(), approximately:

static getDerivedStateFromProps({value}, {radioState, textState}) {
  // (this actually uses _.isEqual() which treats NaN appropriately)
  if (value !== computeExportedValue(radioState, textState)) {
    return defaultState(value);
  }
}

On user input I update the state and call the onChange prop if the exported value has changed. When getDerivedStateFromProps() is called it sees that the two match and nothing happens. (Likewise, upon some unrelated re-render the two continue to match and nothing happens.)

In 16.4 I'm seeing a call to getDerivedStateFromProps() with the old value and new state, so it returns the default state for the old value, meaning the component can never change.

If I've accidentally elided anything important, the actual code is here.

Cheers!

bvaughn commented 6 years ago

Hi @onethreeseven

I've been working on a blog post this week (that I hope to publish today) that covers this topic in more detail, along with examples of what to do and not what to do (and explanations about why). Hopefully you'll read that blog post once it's published. (I'll comment here with a link as well once it's live.)

The code you've shown above would be expected to break in 16.4 because of the changes made to getDerivedStateFromProps. Those changes were made to help expose existing (but inconsistently reproducible) bugs. The blog post will explain this in much greater detail.

In your case, you could quick-fix your component by explicitly tracking the input props to see when they change- and only updating state when that happens:

static getDerivedStateFromProps(props, state) {
  // Only reset state when the external prop changes.
  // Don't reset it anytime props and state disagree though,
  // Or you will override your own setState() updates.
  if (props.value !== state.prevPropsValue) {
    return {
      prevPropsValue: props.value,
      ... computeExportedValue(state.radioState, state.textState)
    };
  }
  return null;
}
onethreeseven commented 6 years ago

Okay, thanks! I look forward to the post.

For posterity, I managed to fix my problem much later last night by changing from (roughly)

handleInput(update) {
  setState(update, () => this.props.onChange(computeExportedValue(this.state)));
}

to

handleInput(update) {
  this.props.onChange(computeExportedValue(_.assign(_.clone(this.state), update)));
  setState(update);
}

I had been doing the former so as to avoid manually computing the updated state, but the latter seems to work just as well and only triggers the one expected call to getDerivedStateFromProps(). I hope this turns out to be kosher in the end!

bvaughn commented 6 years ago

Here's the blog post!

https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html

tankgod commented 6 years ago

image

tankgod commented 6 years ago

This writing will lead to an infinite loop, always asynchronous request, how to solve this problem?

gaearon commented 6 years ago

As explained both in API reference for componentDidUpdate and blog post, you should always make a comparison in componentDidUpdate to decide whether to do something.

componentDidUpdate(prevProps, prevState) {
  if (prevProps.something !== this.props.something) {
    // do something
  }
  if (prevState.somethingElse !== this.state.somethingElse) {
    // do something else
  }
}
tankgod commented 6 years ago

I have a piece of code, as follows:

addFiled(d: any) { const {factor} = this.state; const item = [$utils.generateMixed(6), d]; factor.push(item); this.setState({factor}); } componentWillReceiveProps(nextProps) { if (this.props.mode !== nextProps.mode) { if (nextProps.mode.type === 'edit') { this.setState({factor: []}, () => { // This is a loop mu.each(mu.prop(nextProps, 'mode.data.anys'), res => { this.addFiled(res); }); }); if(mu.prop(nextProps,'mode.data.source.length')){ this.setState({ origin: 'source' }) }else{ this.setState({ origin: 'platforms' }) } }else{ this.setState({ factor: [[$utils.generateMixed(6), []]], origin: 'platforms' }); } this.setState({ visible: true }); } } How do I change it with getDerivedStateFromProps?(I tried using ‘getDerivedStateFromProps’ and ‘componentDidUpdate’, but it went into an infinite loop.I am very annoyed)

bvaughn commented 6 years ago

I tried using ‘getDerivedStateFromProps’ and ‘componentDidUpdate’, but it went into an infinite loop.

Please show what you tried.

tankgod commented 6 years ago

I have solved this problem, thank you anyway.

lvl99 commented 6 years ago

I'm still a bit perplexed on this myself.

While I understand the idea of "controlled" and "uncontrolled" data, I've got a few places where I want it to be both.

I have two examples (both have the same code, one using React 16.3 and the other React 16.4) with two intentions:

React 16.3: https://codesandbox.io/s/9lopjp809o React 16.4: https://codesandbox.io/s/rrxjpn7p6m

In both intentions I want to make a copy of the external prop value and save it internally. Equally, I want any external changes to overwrite the internal state. The ButtonSelectable is a silly example — in most cases this should always be controlled — however in the EditObject one, I want to store a copy of the object in the component's state and manipulate that so the component's view is correctly updated, then publish my changes to the parent component. If the external prop object changes (let's say another component updates the object), I want those changes to overwrite what's in the internal state.

I don't doubt that there's probably an anti-pattern to the React 16.3 getDerivedStateFromProps working like componentWillReceiveProps, but I feel like that lifecycle method is still a good thing to include in future updates. I'm amiable, but I'm still just trying to understand the implications of the anti-pattern and how this new behaviour is better.

gaearon commented 6 years ago

@lvl99 Did you get a chance to read the blog post?

lvl99 commented 6 years ago

@gaearon I did, but still wanting that sweetness of the componentWillReceiveProps behaviour. I attempted something like (warning -- mildly untested code ahead, think of it more as pseudo-code):

// ...
import memoize from "memoize-one";

export default class EditObject {
  // ...

  getObject = memoize((propsObject, stateObject) => {
    if (propsObject && propsObject !== stateObject) {
      return propsObject
    }
    return stateObject
  })

  // ...

  render() {
    let useObject = getObject(this.props.object, this.state.object);

    // ..
  }
}

But the problem that I'm finding is that I just don't want to compare both objects on each render (because when this.state.object changes it'll always then revert to this.props.object, but that's prob more a fault of my logic), just only when this.props.object changes. Would I perhaps use something like memoization to locally store the propsObject itself and then compare/setState when/if it changes? I wonder, is it "legal" to use setState in the memoised function too?:

getObject = memoize(propsObject => {
  if (propsObject !== this.state.object) {
    this.setState({
      object: propsObject
    });
    return propsObject
  }
  return this.state.object
})

I realise I've written a comment where I'm working stuff out at the same time. I'll modify my Codesandboxes to play around and get back to you...

gaearon commented 6 years ago

@lvl99

I don't think your use case is memoization?

What you're trying to do seems to be covered here:

https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#anti-pattern-erasing-state-when-props-change

Including why it's fragile and probably not a good idea.

lvl99 commented 6 years ago

@gaearon Yeah, my approach is definitely not the right idea. However, just trying to figure out what the alternative is when wanting to sandbox some information that then may change due to other external side-effects. Thought it was still relevant to have something like componentWillReceiveProp, but perhaps that's just not the right idea as well. Just need to figure out the right path.

gaearon commented 6 years ago

Here's an updated example with 16.4 that works: https://codesandbox.io/s/m4km18q38j

My changes (compared to the broken 16.4 version you posted are):

Hope this is helpful!

lvl99 commented 6 years ago

@gaearon very helpful! Thanks for the update and the clear breakdown. Much appreciated.

The use of the overrides and the id is very cool. Thanks for taking the time 🙏

drcmda commented 6 years ago

@gaearon is there anything the react team can do to warn users when they install 16.3.x? The newer gDSFP behaviour changes component fundamentals, for instance in the transition component in react-spring. Hence i get issues posted for it every now and then, for instance this one: https://github.com/drcmda/react-spring/issues/198

gDSFP is assuming it's going to be called by setState (which is used by transition when exit-out transitions are completed). As a direct result old transitions are stuck in limbo in 16.3.x until a new render pass begins.

gaearon commented 6 years ago

I don't think this is a serious enough bug to deprecate 16.3.x (which is the only way we can warn them). We try to use npm deprecations very sparingly — either for security vulnerabilities or for guaranteed crashes. Otherwise people won't treat them seriously.

I'm sorry that a React bug is affecting your users. One thing you could to is to cut a major release and raise the minimal required React version to 16.4. Then at least it'll be clear to new users.

Hypnosphi commented 6 years ago

Maybe react-polyfill should override 16.3 behavior with current one?