Comments (32)
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 works
I 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 render phase determines what changes need to be made to e.g. the DOM. During this phase, React calls
render
and then compares the result to the previous render. - The commit phase is when React applies any changes. (In the case of React DOM, this is when React inserts, updates, and removes DOM nodes.) React also calls lifecycles like
componentDidMount
andcomponentDidUpdate
during this phase.
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:
constructor
componentWillMount
componentWillReceiveProps
componentWillUpdate
getDerivedStateFromProps
shouldComponentUpdate
render
setState
updater functions (the first argument)
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 pattern
Given 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 componentDidMount
:
Fetching data in componentWillMount
(or the constructor) is problematic for both server rendering (where the external data won’t be used) and the upcoming async rendering mode (where the request might be initiated multiple times). For these reasons, we suggest moving it to componentDidMount
.
There is a common misconception that fetching in componentWillMount
lets you avoid the first empty rendering state. In practice this was never true because React has always executed render immediately after componentWillMount
. If the data is not available by the time componentWillMount
fires, the first render will still show a loading state regardless of where you initiate the fetch. This is why moving the fetch to componentDidMount
has no perceptible effect in the vast majority of cases.
A couple of other thoughts
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.
Just in case it's unclear, the old lifecycle, componentWillReceiveProps
, was also synchronous. This means that you'd still have a render
in between when the new props were set and when external data finished loading. So the suggested pattern above won't require any additional renders.
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.
Using componentWillReceiveProps
exacerbates this problem, for reasons explained above. Hopefully you're using a library like Axios (or managing cancellation in some other way).
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?
Legacy lifecycles (like componentWillReceiveProps
) will continue to work until React version 17. Even in version 17, it will still be possible to use them, but they will be aliased with an "UNSAFE_" prefix to indicate that they might cause issues. Our preference is that people will migrate to using the new lifecycles though!
from rfcs.
@felixfbecker For side effects you should use componentDidMount
, componentDidUpdate
hooks. I think the RFC is pretty clear about this.
from rfcs.
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
from rfcs.
@kennethbigler Yup! It's on the ReactJS blog:
https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html
from rfcs.
Am I the only one who finds the new recommended pattern much less attractive than the OP?
- Manually comparing
userId
for equality. OP handled this withdistinctUntilChanged
. - Manually cancelling requests. OP handled this with
switchMap
. - Two call sites for
_loadUserData
compared to one forfetchUserProfile
. - Four special React methods (
getDerivedStateFromProps
,componentDidMount
,componentDidUpdate
,componentDidUnmount
) compared to three.
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:
- Have only the special methods
ComponentDidMount
(React seems to insist on this, though I'd rather drop it) andComponentDidUnmount
(effectively a destructor). - Initialize my state only in my constructor.
- Have React reconstruct my component if my props need to change.
This has some big advantages:
- The mental model of a component is much simpler with only two special methods (plus
setState
andrender
) and props that never change from under you. shouldComponentUpdate
gets a default implementation: if and only if the props changed.- I can assume the class invariant that my props never change. Then, for example, I can assume that my props at the time a response comes back are the same as they were when I sent the request.
- I can use tools like MobX and RxJS to, in my constructor, define more derived/computed state without having to worry about how to reinitialize them in the other lifecycle methods.
At the very least, it might help to have a component adapter or base class (like PureComponent
) that offers these guarantees.
from rfcs.
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 render()
) or wait until the new data has come in. This is a case-by-case thing, so I showed the more complex example.
from rfcs.
Yup! Looks like a typo in componentDidUpdate
. Thanks for pointing it out 😄
from rfcs.
It would be possible to check the object reference instead of null?
Someone might modify the state
parameter object. (This isn't the way you're supposed to use the parameter, but...)
from rfcs.
@bvaughn did you ever write that article you mentioned you were working on? I would love to read it!
from rfcs.
Looks like https://github.com/felixfbecker/rx-component :)
from rfcs.
getDerivedStateFromProps
does not support an async (Promise
) return type. So this isn't expected to work.
from rfcs.
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!
from rfcs.
Shouldn't the componentDidUpdate
use current state to determine if the component needs data fetching in the example above? Won't this trigger data fetching twice on initial component rendering - first in componentDidMount
and then, when data was fetched and profileOrError
was set, in componentDidUpdate
method, because it uses prevState
for the profileOrError
check, which is null
(but current state already contains it)?
from rfcs.
Why would one ever want to return null
from getDerivedStateFromProps
instead of just returning the previous state? I understand that internally, returning null
will keep the state the same but returning the previous state will shallowly merge it.
What I'm curious is about the reasoning behind this. Are there any benefits (i.e., maybe performance benefits) to returning null
as opposed to always returning an object from getDerivedStateFromProps
?
from rfcs.
Returning null
is slightly more performant (since React won't have to merge the state with itself). It also more clearly signals the intent of not updating anything.
from rfcs.
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 shouldComponentUpdate
does reference equality.
from rfcs.
Gotcha, and I'm assuming this is because React treats the returned object as a state slice so it will always merge it?
The returned object is treated as a partial state update, just like an update passed to setState
- both get merged into the previous/existing state.
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
shouldComponentUpdate
does reference equality.
Yes. This would also be true.
from rfcs.
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.
from rfcs.
I would suggest to add a warning about this. My first thought would be to return the prevState
if there were no changes.
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 Object.freeze
or a Proxy
on "DEV", to prevent users from mutating the prevState
.
from rfcs.
@bvaughn I didn't follow new react features thoroughly yet, but a question. You say
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.
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:
For example, the code snippet you showed above might fetch external data unnecessarily.
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?
from rfcs.
Question is does what you stated above happen always in new react, or only in case I opt in to use async rendering?
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.
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?
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.
from rfcs.
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`.
}
from rfcs.
Very similar! I'll start a discussion in your project.
from rfcs.
@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.
componentDidUpdate(prevProps, prevState) {
if (prevState.profileOrError !== null && this.state.profileOrError === null) {
// At this point, we're in the "commit" phase, so it's safe to load the new data.
this._loadUserData();
}
}
What do you think?
from rfcs.
@receter that is where RxComponent
and switchMap
come in very handy.
from rfcs.
@receter Seems reasonable.
from rfcs.
I have weird situation with gDSFP:
I have this code:
static getDerivedStateFromProps (props, state) {
if (!state.loaded) {
return new Promise((resolve, reject) => {
const {
googleMapsApiKey,
language,
region,
version
} = props
injectScript({
id: 'googlemaps',
url: `https://maps.googleapis.com/maps/api/js?v=${version}&key=${googleMapsApiKey}&language=${language}®ion=${region}`,
onSuccess: () => {
props.onLoad()
resolve({ loaded: true })
},
onError: () => {
resolve({ loaded: false })
throw new Error(`
There has been an Error with loading Google Maps API script, please check that you provided all required props to <LoadScript />
Props you have provided:
googleMapsApiKey: ${props.googleMapsApiKey}
language: ${props.language}
region: ${props.region}
version: ${props.version}
Otherwise it is a Network issues.
`
)
}
})
}).then(result => {
console.log(result)
return result
})
} else {
this.props.onLoad()
return {
loaded: true
}
}
}
It does return resolved value, but too late, and does not errors out. And state does not change.
from rfcs.
I got this, but there is no error, nor StrictMode notifies me about it.
from rfcs.
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 Promise
you're returning has no enumerable properties, this will just do nothing– leaving your state
unchanged (but without throwing).
from rfcs.
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.
calling setState will signal you need to re-render.
in render, if 'isloading' is true - only render a 'loading...' message
from rfcs.
@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?
from rfcs.
Commit phase. In-progress renders can always be thrown out (or error, or be replaced by higher priority other renders).
from rfcs.
Related Issues (20)
- onScroll event propagation HOT 5
- [Feature Request]: distinguish "what" and "when" dependencies in useEffect HOT 2
- [Feature Request] Return ref rather than forwardRef HOT 1
- the ability to check if something is function or class or an arrow function HOT 5
- npx-create-react-app creating a folder tempnodejsnpm HOT 2
- [Feature Request] Can we do some Static Analysis for diff with babel ? HOT 6
- Improving the RFC workflow process HOT 18
- useIf: Conditional hooks HOT 6
- Is useReducer an overengineering? HOT 10
- Improve profiling react applications HOT 1
- Introduce GUI tooling to speed up web application development HOT 1
- [React Server Components] Idea to simplify overall design HOT 11
- psql: could not connect to server: No such file or directory HOT 1
- [Question] The new JSX transform HOT 1
- [Feature Request]: Add array of updated deps indices to `useEffect` hooks arg HOT 1
- [Feature Request]: React Hooks `Equality` **AKA:** `[isEqual]` Callback HOT 3
- Functional Attribute/Prop Node HOT 2
- [Feature Request][eslint-plugin-react-hooks] no-ref-checks, display error when using useRef's return value as condition HOT 1
- [Feature Request]: useStateRef HOT 5
- Typo: 'exiting' might be 'existing' HOT 2
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from rfcs.