Pitfalls and surprises in data fetching #35
Replies: 5 comments 6 replies
-
Additionally, I think one historically canonical example of using Suspense was to fetch data seemingly granularly on the client (by colocating a fetch for data inside the component that wants it), but it's easy to see that without also using something like GraphQL or the upcoming React Server Components that can batch requests for an entire tree, it's easy to end up in a situation with multiple request waterfalls and potentially subpar performance. I think it would be useful to clarify if this is still a suggested/recommended pattern, and/or what people should do instead. |
Beta Was this translation helpful? Give feedback.
-
We're very happy to share lessons learned and thoughts on our experiences using Suspense for data-fetching - but I think that answer could go on for a while. Is there a specific area that you're most interested in? Are you more interested in what React 18 means as a product engineer or more about the lessons we've learned that are relevant to library authors? |
Beta Was this translation helpful? Give feedback.
-
There are a few main concerns that come to mind in terms of data-management and concurrent rendering:
I'm going to create separate responses for these (will add one at a time as time permits). |
Beta Was this translation helpful? Give feedback.
-
Data Changes During RenderingThis scenario primarily applies to libraries like Relay or Apollo that maintain a "store" of data that is accessed by — and kept consistent across — multiple components or roots. In synchronous rendering it was generally not possible for data in the store to change during the course of a single render: render functions should be "pure", so if rendering occurs synchronously no other code can have run to modify the store. Note that libraries are generally expected to maintain this invariant too - APIs meant to be called during render should also be pure-ish: for example Relay's However, concurrent rendering changes this: concurrent rendering does not complete synchronously so it's possible for other code to run in between rendering of components. This can cause "tearing": when different parts of the screen show inconsistent data. An example of how this can occur:
The issue is step 3, which modifies the store concurrently with rendering in a way that couldn't happen before, when rendering was synchronous. Again, note that this can only happen when concurrent rendering is enabled via new features. There are a few main solutions:
Relay's ApproachRelay currently uses strategy #3: we maintain a "version" counter in the store that increments every time data is published, and we remember the version of the store that each component read from during render. On commit ( We also have a good idea of what it would look like to support #2 in the data layer, but it would require some additional hooks in React. Basically, the data layer would need to understand how long a snapshot should be kept around, know when to reconcile different branches of the store together, etc. This is something we expect to continue exploring going forward. Recommended PatternWe recommend that existing data libraries start by adopting |
Beta Was this translation helpful? Give feedback.
-
Lazy-Fetching and Concurrent RenderingFirst, note that fetching data during render isn't ideal from a performance perspective. As we discussed in our recent blog post, lazy fetching delays the point at which an application can start fetching data, which then blocks further rendering. See the post for more details about alternative approaches that we're using internally. However, in some situations it's just easier to lazily fetch data during rendering. As a library developer trying to support this pattern in React 18, though, we have to handle the possibility that a component may render multiple times before it commits - or it may not commit! So libraries should ensure that each render doesn't trigger a duplicate fetch, while also cancelling fetches that are no longer needed. Libraries should not require developers to handle these cases. More on how to achieve this below. As an example, consider an application with two components: a parent that fetches data (the user's profile photo URL), and then a child that shows the profile photo. The parent suspends while fetching the data, the child suspends while the photo is loading. With concurrent rendering, this would occur as follows:
Note that the parent component renders 3 times in this scenario, the child renders 2 times, and each commits once. But it's also possible that the user navigated to different content before this sequence completed, in which case the parent/child may have rendered some number of times without ever committing! Relay's ApproachIn Relay we handle these cases with a custom cache implementation that de-dupes fetches over a short period. The Recommended PatternOur experience using Suspense for data-fetching in Relay and elsewhere informed the creation of the new In terms of a recommendation for library authors, |
Beta Was this translation helpful? Give feedback.
-
I'd love to hear from the Relay / React Data team on some of the pitfalls / surprises / lessons learned from working with Concurrent APIs. Outside of being a maintainer of a data fetching / data caching library, as a frontend engineer, I think a first concern would be what React 18+ means for data fetching.
Beta Was this translation helpful? Give feedback.
All reactions