-
Notifications
You must be signed in to change notification settings - Fork 558
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
How can we fetch data async in reaction to props changes with getDerivedStateFromProps? #26
Comments
@felixfbecker For side effects you should use |
I'm currently in the process of writing some blog posts to go over this, but I'll answer here (with similar content) in case it's helpful for others who come across this issue in the future. Overview of how React worksI think it would be helpful to start with a quick overview of how React works, and how async rendering will impact class components. Conceptually, React does work in two phases:
The commit phase is usually very fast, but rendering can be slow. For this reason, async mode will break the rendering work into pieces, pausing and resuming the work to avoid blocking the browser. This means that React may invoke render phase lifecycles more than once before committing, or it may invoke them without committing at all (because of an error or a higher priority interruption). Render phase lifecycles include the following class component methods:
Because the above methods might be called more than once, it's important that they do not contain side-effects. Ignoring this rule can lead to a variety of problems. For example, the code snippet you showed above might fetch external data unnecessarily. Suggested patternGiven your first example above, I would suggest an approach like this: class ExampleComponent extends React.Component {
state = {};
static getDerivedStateFromProps(nextProps, prevState) {
// Store prevUserId in state so we can compare when props change.
// Clear out any previously-loaded user data (so we don't render stale stuff).
if (nextProps.userId !== prevState.prevUserId) {
return {
prevUserId: nextProps.userId,
profileOrError: null,
};
}
// No state update necessary
return null;
}
componentDidMount() {
// It's preferable in most cases to wait until after mounting to load data.
// See below for a bit more context...
this._loadUserData();
}
componentDidUpdate(prevProps, prevState) {
if (this.state.profileOrError === null) {
// At this point, we're in the "commit" phase, so it's safe to load the new data.
this._loadUserData();
}
}
render() {
if (this.state.profileOrError === null) {
// Render loading UI
} else {
// Render user data
}
}
_loadUserData() {
// Cancel any in-progress requests
// Load new data and update profileOrError
}
} With regard to the initial data fetching in Fetching data in There is a common misconception that fetching in A couple of other thoughts
Just in case it's unclear, the old lifecycle,
Using
Legacy lifecycles (like |
I was using a similar pattern in lots of my components for ajax requests. The actual change looks hilariously simple on a contrived example, but you get the idea. https://gist.github.com/sorahn/2cdc344cc698f027a948e3fdf6e0e60f/revisions |
Yeah, it may be unnecessary to even use the new static lifecycle. It depends on whether you want to immediately clear out stale data (during the next call to |
@bvaughn did you ever write that article you mentioned you were working on? I would love to read it! |
@kennethbigler Yup! It's on the ReactJS blog: |
Shouldn't the |
Yup! Looks like a typo in |
Why would one ever want to return What I'm curious is about the reasoning behind this. Are there any benefits (i.e., maybe performance benefits) to returning |
Returning |
Gotcha, and I'm assuming this is because React treats the returned object as a state slice so it will always merge it? Also, I guess if the derived state end up being a complex data structure and a new one is created, it might cause some components to re-render lower in the tree if their |
The returned object is treated as a partial state update, just like an update passed to
Yes. This would also be true. |
I don't see why it have to be treated as a partial update if prevState is returned. It would be possible to check the object reference instead of null? Otherwise a warning would be great. |
Someone might modify the |
I would suggest to add a warning about this. My first thought would be to return the I tried myself, but could not find where to add an test. *ReactPartialRenderer.js L:467
-----------------------------------------------------------------------------------------------
if (partialState === inst.state) {
const componentName = getComponentName(Component) || 'Unknown';
warning(
'%s.getDerivedStateFromProps(): Returned the previous state. ' +
'If no changes return null, to prevent unnecessary performance bottlenecks.',
);
} It would also be possible to use |
@bvaughn I didn't follow new react features thoroughly yet, but a question. You say
AFAIK this was not the case with old react right? (that those life cycle methods could be called many times). Question is does what you stated above happen always in new react, or only in case I opt in to use async rendering? Also can you expand a little bit on this:
OP had checks in order to compare userid from previous props to userid from new props, so why would data be fetched unnecessarily? Can you expand on this? |
I’m not on the React team, but it is likely that there won’t be an option, since async rendering is a performance improvement.
What could happen with async rendering is that the app could start to re-render, then cancel the re-render and try it again later. This would cause the function to be called twice with the same previous and new props. I think. |
Am I the only one who finds the new recommended pattern much less attractive than the OP?
I think much of the trouble with state-derived-from-props comes from the fact that React assigns the props after construction. I think I'd rather:
This has some big advantages:
At the very least, it might help to have a component adapter or base class (like |
My first shot excerpted below. Would love critique. Working implementation (and the rest of the details) at [CodeSandbox]. // The abstract base class for developers to extend.
// You get some observable props,
// and your responsibility is to present an observable element.
// You can observe `this.mounted` to react to `componentDidMount()`
// and `componentWillUnmount()`, if you please.
abstract class RxComponent<Props> {
constructor(
protected readonly props: Observable<Props>,
protected readonly mounted: Observable<boolean>,
) {}
element: Observable<JSX.Element>
}
// The adapter between the base class above and `React.Component`.
function rxComponent<Props> (
Component: Constructor<RxComponent<Props>>
): Constructor<React.Component<Props>> {
return class extends React.Component<Props> {
private props$ = new BehaviorSubject<Props>(this.props)
private mounted$ = new BehaviorSubject<boolean>(false)
private component = new Component(this.props$, this.mounted$)
public state = {element: null}
private subscription = this.component.element.subscribe(
element => this.setState({ element })
)
public componentDidMount() {
this.mounted$.next(true)
}
public componentDidUpdate() {
this.props$.next(this.props)
}
public componentWillUnmount() {
this.subscription.unsubscribe()
this.mounted$.next(false)
}
public render() {
// We don't really care how many times React calls `render()`
// because we don't spend any time in this function constructing a new element
// and we return the exact same element since the last state change.
return this.state.element
}
}
}
// OP's example, as an `RxComponent`.
@rxComponent
class RxExample extends RxComponent<{ match: Match<{ userId: number }> }> {
public readonly element = this.props.pipe(
map(props => props.match.params.userId),
// Do not fetch unless the parameters have changed.
distinctUntilChanged(),
// Do not start fetching until after we've mounted.
filterWhen(this.mounted),
loadingSwitchMap(fetchUserProfile),
map(({ value, pastDelay }) => {
// Loaded.
if (value) {
return <p>user = {JSON.stringify(value)}</p>
}
// Taking a long time to load.
if (pastDelay) {
return <p>Loading...</p>
}
// Avoid flash of content.
return null
}),
)
// This doesn't build an element until the component is mounted.
// If you care about what React shows between
// constructing your component and mounting it,
// then you can use `startWith`.
} |
Very similar! I'll start a discussion in your project. |
@bvaughn Maybe I miss something, but the suggested pattern might call _loadUserData multiple times if the component gets re-rendered while the AJAX request is waiting for a response. I think of adding a comparison to prevState to prevent this behaviour.
What do you think? |
@receter that is where |
@receter Seems reasonable. |
I have weird situation with gDSFP: I have this code:
It does return resolved value, but too late, and does not errors out. And state does not change. |
|
I got this, but there is no error, nor StrictMode notifies me about it. |
That's a good point. Maybe we can add a DEV warning for this case. I'll look into it. Currently, what will happen is that React assumes you are returning an object with enumerable properties and it will mix that object into your previous state. Since the |
I mentioned this issue to the rest of the team this morning. We're going to keep our eye out to see if it trips up anyone else. If it looks to be common, we'll add a new DEV warning for it. 😄 Thanks for reaching out! |
https://www.robinwieruch.de/react-fetching-data/#react-fetch-data-async-await This did exactly what I needed... just putting it out there. Simple concept: report state 'isloading:true' while you are loading data, and set to false when you are done. |
@bvaughn I have a similar but slightly different issue... I'm using InfiniteLoader to load additional metadata for the list of visible rows in my Table... loadMoreRows creates an abortable request using fetch and returns the corresponding promise... I save a reference to this request so I can abort it if the component happens to un-mount before the request returns... however, I sometimes also need to abort existing requests if certain props change, indicating that the underlying table data represents something new and all previous in-flight requests are no longer valid... taking these into account, where would you recommend the best place to cancel these existing requests should be? |
Commit phase. In-progress renders can always be thrown out (or error, or be replaced by higher priority other renders). |
I couldn't find this mentioned in the RFC.
Currently almost all of our components implement
compoentWillReceiveProps
to trigger some async operation in reaction to props changes, then callsetState
asynchronously. Simple example:I.e. the new derived state from the Props is not derived synchronously, but asynchronously. Given that
getDerivedStateFromProps
is sync, I don't see how this could work with the new API.Note that the above example is very simple, is prone to race conditions if the responses arrive out of order and does no cancellation on new values or component unmount.
We usually make use of RxJS Observables to solve all those issues:
So now that the methods we were making use of are deprecated, I wonder were we doing this wrong all the time? Is there already a better way to do this with the old API? And what is the recommended way to do this with the new API?
The text was updated successfully, but these errors were encountered: