React 18: Improved data fetching using the new startTransition API


The upcoming release of React 18 will contain a number of improvements, including the much anticipated “concurrent features” (see this announcement). One such feature is the new startTransition API, which makes it possible to assign different priorities to state updates:


  • Urgent updates occur in response to user interactions, such as click or scroll events, and ensure that the application’s UI remains responsive at all times.

  • Non-urgent updates let React render a view and its components “in the background”, and when the rendering completes the UI transitions to an updated view.


In the current version of React, in a scenario where an user performs an action that results in a state update and (potentially expensive) rerender of a view, the rendering process will block other activities on the page (critically any further user interaction) until it completes.

With startTransition, the rerendering of a view is moved to the background, allowing the user to still interact with other UI elements unhindered while the transition to an updated view is being prepared.

This overview provides a detailed rationale for the startTransition feature, and the accompanying demo examines its use to avoid slow rendering and maintain UI responsiveness.

This article focuses on another scenario in which startTransition will improve the user experience, namely data fetching.


About Suspense

React 18 will further introduce the so-called Suspense feature, a mechanism that supports “suspending” a component render as it fetches dependent resources.


To illustrate Suspense, see listings 1 and 2.


All code listings refer to the sample application available on Github


// Listing 1. 
 
// in App.js. 
<Suspense fallback={`Loading repos...`}>   
    <Repos endpoint={`${BASE_URL}/${currentUser}/repos`} /> </Suspense> 

As part of rendering a profile view for a Github user, a component named Repos is responsible for fetching and rendering the user’s repositories data. The component is wrapped in a so-called Suspense boundary with a fallback that will be shown while data is being fetched.


// Listing 2.  

// in Repos.js. 
export const Repos = ({ endpoint }) => {   
    const {     
       data   
    } = useSWR(endpoint, fetchResource, { suspense: true });    
    
    
   return (     
      <>       
         <h2>Repositories</h2>       
         {data.map((repo, i) => <p key={i}>{repo.name}</p>)}           
      </>   
   ); 
} 

Upon commencing the rendering of Repos , a call is made to fetch its resource, i.e. the user’s repositories. The first time the useSWR hook is invoked, it will suspend - not continue rendering - by throwing a Promise object that will be caught by the parent Suspense boundary. When the promise resolves, React will resume rendering the component; useSWR is called anew and this time, the data is available and returned to be used in the resulting markup. This flow is illustrated in figure 1.



The useSWR hook is part of a 3rd party data fetching library called SWR that supports Suspense.


Notice that the Repos component doesn’t manage any loading states; this aspect is fully shouldered by the parent Suspense boundary.


Data fetching with startTransition and Suspense

How does startTransition come into play in this data fetching scenario? See listing 3:


// Listing 3.  

// in App.js. 
const [   
	currentUser,   
	setCurrentUser 
] = useState(null);  

const [   
	showProfile,   
	setShowProfile 
] = useState(false);  

const [isPending, startTransition] = useTransition();  

const selectUser = user => {   
	setCurrentUser(user);    
	
	startTransition(() => {     
		setShowProfile(true);   
	}); 
}; 

In the UI rendered by the main component AppInternal there are two states:

  • currentUser : The currently selected Github user.

  • showProfile : Whether or not the profile view is being shown for the current Github user.

The UI can either display a list of Github users (default view) or a profile view.


When a Github user is selected, the following happens:


  • An urgent state update is made by calling setCurrentUser .

  • A non-urgent state update is performed in a call to startTransition .


By updating the showProfile state in a transition, React will starting rerendering the AppInternal component in the background, which in turn entails rendering the UserProfile , Repos and Followers components, along with the resources they’re fetching respectively. Once React finishes the background update, the default view transitions to the profile view.


Also, as the currentUser state is immediately (“urgently”) updated, the default view is able to display a subtle loading indicator next to the selected Github user in the list while the transition is pending:


// Listing 4.  

// in App.js.  

// the useTransition hook returns an isPending flag that is true while React is performing background work. 
const [isPending, startTransition] = useTransition();  

// ...  

{user} {(isPending && currentUser === user) && '...'}

The following video demonstrates the default view and the transition to a profile view when a Github user is selected; notice that the default view remains visible while the profile view is being rendered in the background.


The UI also allows for selecting another Github user in the midst of preparing the profile view for the one currently selected! As stated in the overview, “React will throw out the stale rendering work that wasn’t finished and render only the latest update”.

Orchestrating loading sequences

As shown in the demo, the profile view loads and renders its components according to the following sequence:


  • The Github user’s information (name and avatar image URL) is loaded first and rendered “together” as a unit.

  • The repositories and followers are displayed as soon as the corresponding data arrives; the rendering order of the Repos and Followers components is indeterminate.


Note that even if repositories and followers data should arrive earlier (i.e. faster) than the user information, the loading sequence ensures that the profile view will not be shown until the latter becomes available.


What if there is a need to alter the sequence, so that repositories and followers now should be rendered simultaneously?


While this can be hard to achieve with the current iteration of React, with React 18 and Suspense the change is trivial, from


// Listing 5.  

// in App.js.  

// separate Suspense boundaries allow for loading repos and followers "lazily", i.e. ensuring that rendering the  
// critical UserProfile component is not delayed by the not-as-vital Repos and Followers components. */}  

<Suspense fallback={`Loading repos...`}>   
    <Repos endpoint={`${BASE_URL}/${currentUser}/repos`} /> </Suspense>  
    
<Suspense fallback={`Loading followers...`}>   
    <Followers endpoint={`${BASE_URL}/${currentUser}/followers`} /> </Suspense>

to

// Listing 6.  

// in App.js.  

// by moving Repos and Followers under a common parent Suspense boundary, React guarantees that it will await 
// _both_ components to finish fetching resources before resuming rendering them.  

<Suspense fallback={`Loading repos and followers...`}>   
    <Repos endpoint={`${BASE_URL}/${currentUser}/repos`} />              
    <Followers endpoint={`${BASE_URL}/${currentUser}/followers`} /> </Suspense> 

Finally, what if React should await all Github user contents to load before transitioning to the profile view? As the AppInternal component itself is wrapped in a Suspense boundary, simply remove any inner Suspense boundaries, such as those for Repos and Followers . The fallback Loading view... will not be shown and disrupt the default view however, as the use of startTransition does not “retrigger” the parent Suspense boundary.


Implementation details

In the example code, the SWR data fetching library is utilized, as it comes with built-in support for working with Suspense.


Certain resources, e.g. static assets such as images, may require a custom fetching approach to be compatible with Suspense. When rendering the user information, the Github data is fetched first followed by a call to explicitly load the avatar image:


// Listing 7. 
// 
// in UserProfile.js.  

// start fetching the user information, which contains the avatar image URL. 
// This call will suspend until the Github data becomes available. 
const { data } = useSWR(endpoint, fetchResource, { suspense: true });  

const {   
    name,   
    avatar_url 
} = data;  

// load (and let the browser cache) the avatar image; this call will also suspend rendering until the image has been  
// loaded.  
// 
// It's necessary to explicitly control the fetching of this static asset, as the Github user's information and avatar are supposed to be rendered jointly. 
const image = useImageFetch(avatar_url); 

See src/util.js for a custom implementation (for images in this instance) of data fetching with support for Suspense.


Conclusion

This article has demonstrated the new React 18 startTransition API for a typical data fetching scenario; it should be noted that the outlined approach is currently not guaranteed to be viable in production, as there remains some work for the React team to fully flesh out Suspense for data fetching.


In the near future, React itself will offer its own libraries for reading and writing data (see e.g. react-fetch), complemented by built-in caching functionality. The Server components initiative will further consolidate best practices for data fetching in React.


Contact

Marc Klefter | marc.klefter@edument.se

0 kommentarer