It’s the hottest topic these days in the React community, and everybody gets either super excited or completely confused when the word “Suspense” is thrown around. This post was originally published on dev.to . In this article, I’m not going to cover all the details of , as this has been discussed and explained numerous times, and the are very explicit about the topic. Instead, I will show you how you can start using it today in your React projects. what the heck is up with this Suspense thing official docs TLDR? yarn add use-async-resource so you can: fetchUser = (id: number) => fetch(`.../ /user/by/${id}`).then(res => res.json()); { [userReader, getNewUser] = useAsyncResource(fetchUser, ); ( <> <ErrorBoundary> <React.Suspense fallback= > <User userReader={userReader} /> </React.Suspense> </ErrorBoundary> <button onClick={() => getNewUser( )}>Get user id </button> { } </> ); } { userData = userReader(); <div>{userData.name}</div>; } { useAsyncResource } from 'use-async-resource'; import // a simple api function that fetches a user const get function App () // --> initialize the data reader and start fetching the user immediately const 1 return "user is loading..." /* <-- pass it to a suspendable child component */ 2 with 2 /* --> clicking the button will start fetching a new user */ function User ({ userReader }) const // <-- just call the data reader function to get the user object return Of course there’s more to it, so read on to find out. “But I thought it’s experimental and we shouldn’t use it yet” is experimental! Suspense for lazy-loaded components, and even simple data fetching, works today. The component has been shipped since React 16.6, even before hooks! Concurrent mode React.Suspense All the other fancy things, like , , , priority-based rendering etc are not officially out. But we’re not covering them here. We’re just trying to get started with the simple data fetching patterns, so when all these new things will be released, we can just improve our apps with them, building on top of the solutions that do work today. SuspenseList useTransition useDeferredValue So what is Suspense again? In short, it’s a pattern that allows React to suspend the rendering of a component until some condition is met. In most cases, until some data is fetched from the server. The component is “suspended” if, instead of returning some JSX like it’s supposed to, . This allows React to render other parts of your app without your component being “ready”. it throws a promise Fetching data from a server is always an asynchronous action. At the same time, the data a component needs in order to render should be available as a simple synchronous read. Of course, the whole Suspense thing is much more than that, but this is enough to get you started. In code, it’s a move from this: { [user, setUser] = useState(); [loading, setLoading] = useState( ); [error, setError] = useState(); useEffect( { setLoading( ); fetchUser(props.id) .then( { setUser(userResponse); setLoading( ); ) .catch( { setError(e); setLoading( ); ); }, [props.id]); (loading) ; (error) ; ; } { ( ) function User props const const true const => () true ( ) => userResponse false ( ) => e false if return loading... < > div </ > div if return something happened :( < > div </ > div return {user.name} < > div </ > div ( ) function App return ; } < = /> User id {someIdFromSomewhere} to this: { user = props.userReader(); ; } { userReader = initializeUserReader(someIdFromSomewhere); ( <React.Suspense fallback="loading..."> <User userReader={userReader} /> </React.Suspense> </ErrorBoundary> ); } ( ) function User props const return {user.name} < > div </ > div ( ) function App const return < = > ErrorBoundary error "something went wrong with the user :(" Note: some details were omitted for simplicity. If you haven’t figured it out yet, is just a synchronous function that, when called, returns the user object. userReader What is not immediately clear is that it also if the data is not ready. The boundary will catch this and will render the fallback until the component can be safely rendered. At the same time, will kick off the async call immediately. throws a promise React.Suspense Calling userReader can also throw an error if the async request failed, which is handled by the ErrorBoundary wrapper. initializeUserReader This is the most basic example, and the docs get into way more detail about the concepts behind this approach, its benefits, and further examples about managing the flow of data in your app. Ok, so how do we turn async calls into sync data reads? First of all, the simplest way to get some async data is to have a function that returns a Promise, which ultimately resolves with your data; for simplicity, let’s call such functions “api functions”: fetchUser = fetch( ); const => id `path/to/user/get/ ` ${id} Here, we’re using , but the Promise can be anything you like. We can even mock it with a random timeout: fetch fetchUser = ( { setTimeout( resolve({ id, : }), .random() * ); }); const => id new Promise ( ) => resolve => () name 'John' Math 2000 Meanwhile, our component wants a function that just returns synchronous data; : for consistency, let’s call this a “data reader function” getUser = ({ : , : }); const => () id 1 name 'John' But in a Suspense world, we need a bit more than that: we also need to start fetching that data from somewhere, as well as throw the Promise if it’s not resolved yet, or throw the error if the request failed. We will need to generate the data reader function, and we’ll encapsulate the fetching and throwing logic. The simplest (and most naive) implementation would look something like this: initializeUserReader = { data; status = ; error; fetchingUser = fetchUser(id) .then( { data = user; status = ; }) .catch( { error = e; status = ; }); { (status === ) { fetchingUser; } (status === ) { error; } data; } }; const ( ) => id // keep data in a local variable so we can synchronously request it later let // keep track of progress and errors let 'init' let // call the api function immediately, starting fetching const ( ) => user 'done' ( ) => e 'error' // this is the data reader function that will return the data, // or throw if it's not ready or has errored return => () if 'init' throw else if 'error' throw return If you’ve been reading other articles or even the official docs, you probably are familiar with this “special” pattern. It’s nothing special about it, really: you immediately start fetching the data, then you return a function that, when called, will give you the data if the async call is ready, or throw the promise if it’s not (or an error if it failed). That’s exactly what we’ve been using in our previous example: userReader = initializeUserReader(someIdFromSomewhere); ( <React.Suspense fallback= > <div>{user.name}</div> // in AppComponent const return // ... "loading..." ); // in UserComponent const user = props.userReader(); return < = /> User userReader {userReader} </ > React.Suspense ; In the parent, we initialize the data reader, meaning we’re triggering the api call immediately. We get back that “special” function which the child component can call to access the data, throwing if not ready. “But this is not practical enough…” Yes, and if you’ve been reading anything about Suspense, this is also not new. It’s just an example to illustrate a pattern. So how do we turn it into something we can actually use? First of all, it’s not correct. You probably spotted by now that, if the component updates for any other reason, the data reader gets re-initialized. So even if an api call is already in progress, if the component re-renders, it will trigger another api call. We can solve this by keeping our generated data reader function in a local state: App App [userReader] = useState( initializeUserReader(someId)); // in AppComponent const => () Next, we will probably need to fetch new data based on a new user id. Again, the setter function from can help us: useState [userReader, updateReader] = useState( initializeUserReader(someId)); btnClickCallback = useCallback( { updateReader( initializeUserReader(newUserId)); }, []); ( <button onClick={() => btnClickCallback( )}> get user id < const => () const ( ) => newUserId => () return // ... 1 with 1 /button> ); It looks better, but we’re starting to see a lot of repetitions. Plus, it’s hardcoded for our api function. We need something more generic. fetchUser Let’s change the initializer to accept an api function, any. We will also need to pass all the parameters the api function might need, if any. initializeDataReader = { fetcingPromise = apiFn(...parameters) .then( ) }; const ( ) => apiFn, ...parameters // ... const /* ... */ // ... // ... Our initializer now works with ANY api function which accepts ANY number of parameters (or even none). Everything else remains unchanged. [userReader, updateUserReader] = useState( initializeDataReader(fetchUser, userId)); [postsReader, updatePostsReader] = useState( initializeDataReader(fetchPostByTags, , , , )); getNewUser = useCallback( { updateUserReader( initializeDataReader(fetchUser, newUserId)); }, []); getNewPosts = useCallback( { updatePostsReader( initializeDataReader(fetchPostByTags, ...tags)); }, []); const => () const => () 'react' 'suspense' 'data' 'fetching' const ( ) => newUserId => () const ( ) => ...tags => () But we’re still facing the repetition problem when we need to fetch new data because we always need to pass the api function to the initializer. Time for a custom hook! useAsyncResource = { [dataReader, updateDataReader] = useState( initializeDataReader(apiFunction, ...parameters)); updater = useCallback( { updateDataReader( initializeDataReader(apiFunction, ...newParameters)); }, [apiFunction]); [dataReader, updater]; }; const ( ) => apiFunction, ...parameters const => () const ( ) => ...newParameters => () return Here, we’ve encapsulated the logic of initializing both the data reader and the updater function. Now, when we need to fetch new data, we will never have to specify the api function again. We also return them as a tuple (a pair), so we can name them whatever we want when we use them: [userReader, refreshUserReader] = useAsyncResource(fetchUser, userId); onBtnClick = useCallback( { refreshUserReader(newId); }, []); const const ( ) => newId Again, everything else remains unchanged: we still pass the generated data reader function to the “suspendable” component that will call it in order to access the data, and we wrap that component in a Suspense boundary. Taking it further Our custom hook is simple enough, yet it works for most use cases. But it also needs other features that have proven useful in practice. So let’s try to implement them next. useAsyncResource Lazy initialization In some cases, we don’t want to start fetching the data immediately, but rather we need to wait for a user’s action. We might want to initialize the data reader. lazily Let’s modify our custom hook so that when it gets the api function as the only argument, we won’t start fetching the data, and the data reader function will return (just like an unassigned variable). We can then use the updater function to start fetching data on demand, just like before. undefined [userReader, refreshUserReader] = useAsyncResource(fetchUser); btnClick = useCallback( { refreshUserReader(userId); }, []); const const ( ) => userId // calling userReader() now would return `undefined`, unless a button is clicked This might work for api functions that take arguments, but now how do we eagerly initialize a data reader for an api function that take any arguments? Well, as a convention, let’s specify that in order to eagerly initialize such functions, the custom hook will expect an empty array as a second argument (just like React hooks!). doesn’t fetchLatestPosts = fetch( ); [latestPosts, refreshLatestPosts] = useAsyncResource(fetchLatestPosts, []); [latestPosts, getLatestPosts] = useAsyncResource(fetchLatestPosts); startFetchingLatestsPosts = useCallback( { getLatestPosts(); }, []); ( ); // this api function doesn't take any arguments const => () 'path/to/latest/posts' // eagerly initialized data reader, will start fetching immediately const // lazily initialized, won't start fetching until the button is clicked const const => () // this will kick off the api call return get latest posts < = > button onClick {startFetchingLatestsPosts} </ > button In short, passing the api function params to the hook will kick off the api call immediately; otherwise, it won’t. All cases would work on the same principle: [userReader, refreshUserReader] = useAsyncResource(fetchUser); [latestPosts, getLatestPosts] = useAsyncResource(fetchLatestPosts); [userReader, refreshUserReader] = useAsyncResource(fetchUser, userId); [latestPosts, refreshLatestPosts] = useAsyncResource(fetchLatestPosts, []); // lazily initialized data readers const const // eagerly initialized data readers const const Implementing this will require some changes to our custom hook: useAsyncResource = { [dataReader, updateDataReader] = useState( { (!parameters.length) { ; } ( !apiFunction.length && parameters.length === && .isArray(parameters[ ]) && parameters[ ].length === ) { initializeDataReader(apiFunction); } initializeDataReader(apiFunction, ...parameters); }); updater = useCallback( { updateDataReader( initializeDataReader(apiFunction, ...newParameters)); }, [apiFunction]); [dataReader, updater]; }; const ( ) => apiFunction, ...parameters // initially defined data reader const => () // lazy initialization, when no parameters are passed if // we return an empty data reader function return => () undefined // eager initialization for api functions that don't accept arguments if // check that the api function doesn't take any arguments // but the user passed an empty array as the only parameter 1 Array 0 0 0 return // eager initialization for all other cases // (i.e. what we previously had) return // the updater function remains unchaged const ( ) => ...newParameters => () return Transforming the data upon reading In other cases, the data you get back might be a full response from the server, or a deeply nested object, but your component only needs a small portion from that, or even a completely transformed version of your original data. Wouldn’t it be nice if, when reading the data, we can easily transform it somehow? { userObject.friendsList.length; } { friendsCount = props.userReader(friendsCounter); ; } // transform function ( ) function friendsCounter userObject return ( ) function UserComponent props const return Friends: {friendsCount} < > div </ > div We will need to add this functionality to our data reader initializer: initializeDataReader = { { (status === ) modifier === ? modifier(data) : data; } }; const ( ) => apiFn, ...parameters // ... return ( ) => modifier if 'init' // ... // ... throwing like before return typeof 'function' // apply a transformation if it exists // otherwise, return the unchanged data What about TypeScript? If you use TypeScript in your project, you might want to have this custom hook fully typed. You’d expect the data reader function to return the correct type of the data your original api function was returning as a Promise. Well, this is where things can get complicated. But let’s try… First, we know we are working with many types, so let’s define them in advance to make everything more readable. ApiFn<R, A [] = []> = <R>; UpdaterFn<A [] = []> = ; DataFn<R> = R; LazyDataFn<R> = (R | ); ModifierFn<R, M = > = M; ModifiedDataFn<R> = <M> M; LazyModifiedDataFn<R> = <M> (M | ); DataOrModifiedFn<R> = DataFn<R> & ModifiedDataFn<R>; LazyDataOrModifiedFn<R> = LazyDataFn<R> & LazyModifiedDataFn<R>; // a typical api function: takes an arbitrary number of arguments of type A // and returns a Promise which resolves with a specific response type of R type extends any ( ) => ...args: A Promise // an updater function: has a similar signature with the original api function, // but doesn't return anything because it only triggers new api calls type extends any ( ) => ...args: A void // a simple data reader function: just returns the response type R type => () // a lazy data reader function: might also return `undefined` type => () undefined // we know we can also transform the data with a modifier function // which takes as only argument the response type R and returns a different type M type any ( ) => response: R // therefore, our data reader functions might behave differently // when we pass a modifier function, returning the modified type M type ( ) => modifier: ModifierFn<R, M> type ( ) => modifier: ModifierFn<R, M> undefined // finally, our actual eager and lazy implementations will use // both versions (with and without a modifier function), // so we need overloaded types that will satisfy them simultaneously type type That was a lot, but we covered all the types that we’re going to use: we start from a simple api function and we’ll want to end up with a simple data reader function ; ApiFn<R, A ...> DataFn<R> this data reader function my return if it’s lazily initialized, so we’ll also use ; undefined LazyDataFn<R> our custom hook will correctly return one or the other based on how we initialize it, so we’ll need to keep them separate; the data reader function can accept an optional modifier function as a parameter, in which case it will return a modified type instead of the original data type (therefore or ); without it, it should just return the data type; ModifiedDataFn<R> LazyModifiedDataFn<R> to satisfy both these conditions (with or without the modifier function), we’ll actually use and respectively; DataOrModifiedFn<R> LazyDataOrModifiedFn<R> we also get back an updater function , with a similar definition as the original api function. UpdaterFn<R, A ...> Let’s start with the initializer. We know we’re going to have two types of api functions: with arguments, and without arguments. We also know that the initializer will always kick off the api call, meaning the data reader is always eagerly generated. We also know that the returned data reader can have an optional modifier function passed to it. ; ; { AsyncStatus = | | ; data: ResponseType; status: AsyncStatus = ; error: ; fetcingPromise = apiFn(...parameters) .then( { data = response; status = ; }) .catch( { error = e; status = ; }); ; ; { (status === ) { fetcingPromise; } (status === ) { error; } modifier === ? modifier(data) M : data ResponseType; } dataReaderFn; } // overload for wrapping an apiFunction without params: // it only takes the api function as an argument // it returns a data reader with an optional modifier function < >( ): < > function initializeDataReader ResponseType apiFn: ApiFn<ResponseType>, DataOrModifiedFn ResponseType // overload for wrapping an apiFunction with params: // it takes the api function and all its expected arguments // also returns a data reader with an optional modifier function < , []>( ): < > function initializeDataReader ResponseType ArgTypes extends any apiFn: ApiFn<ResponseType, ArgTypes>, ...parameters: ArgTypes DataOrModifiedFn ResponseType // implementation that covers the above overloads < , [] = []>( ) function initializeDataReader ResponseType ArgTypes extends any apiFn: ApiFn<ResponseType, ArgTypes>, ...parameters: ArgTypes type 'init' 'done' 'error' let let 'init' let any const ( ) => response 'done' ( ) => e 'error' // overload for a simple data reader that just returns the data ( ): function dataReaderFn ResponseType // overload for a data reader with a modifier function < >( ): function dataReaderFn M modifier: ModifierFn<ResponseType, M> M // implementation to satisfy both overloads < >( ) function dataReaderFn M modifier?: ModifierFn<ResponseType, M> if 'init' throw else if 'error' throw return typeof "function" as as return Pretty complex, but it will get the job done. Now let’s continue the custom hook. We know there are 3 use cases, so we’ll need 3 overloads: lazy initializing, eager initializing for api functions without arguments, and eager initializing for api functions with arguments. typing ; ; ; // overload for a lazy initializer: // the only param passed is the api function that will be wrapped // the returned data reader LazyDataOrModifiedFn<ResponseType> is "lazy", // meaning it can return `undefined` if the api call hasn't started // the returned updater function UpdaterFn<ArgTypes> // can take any number of arguments, just like the wrapped api function < , []>( ): [ < >, < >] function useAsyncResource ResponseType ArgTypes extends any apiFunction: ApiFn<ResponseType, ArgTypes>, LazyDataOrModifiedFn ResponseType UpdaterFn ArgTypes // overload for an eager initializer for an api function without params: // the second param must be `[]` to indicate we want to start the api call immediately // the returned data reader DataOrModifiedFn<ResponseType> is "eager", // meaning it will always return the ResponseType // (or a modified version of it, if requested) // the returned updater function doesn't take any arguments, // just like the wrapped api function < >( ): [ < >, ] function useAsyncResource ResponseType apiFunction: ApiFn<ResponseType>, eagerLoading: never[], // the type of an empty array `[]` is `never[]` DataOrModifiedFn ResponseType UpdaterFn // overload for an eager initializer for an api function with params // the returned data reader is "eager", meaning it will return the ResponseType // (or a modified version of it, if requested) // the returned updater function can take any number of arguments, // just like the wrapped api function < , []>( ): [ < >, < >] function useAsyncResource ResponseType ArgTypes extends any apiFunction: ApiFn<ResponseType, ArgTypes>, ...parameters: ArgTypes DataOrModifiedFn ResponseType UpdaterFn ArgTypes And the implementation that satisfies all 3 overloads: { [dataReader, updateDataReader] = useState( { (!parameters.length) { < , []>( ) function useAsyncResource ResponseType ArgTypes extends any apiFunction: ApiFn<ResponseType> | ApiFn<ResponseType, ArgTypes>, ...parameters: ArgTypes // initially defined data reader const => () // lazy initialization, when no parameters are passed if // we return an empty data reader function return ( ) < >; } // ' ( ) { ( ); } // ( ); }); // = ( ); [ , ]; }; ( ) => undefined as LazyDataOrModifiedFn ResponseType eager initialization for api functions that don t accept arguments if // ... check for empty array param return initializeDataReader apiFunction ApiFn<ResponseType> as eager initialization for all other cases return initializeDataReader apiFunction ApiFn<ResponseType, ArgTypes >, ...parameters as the updater function const updater useCallback ( ) => { updateDataReader( ); }, [apiFunction] ...newParameters: ArgTypes ( ) => initializeDataReader( ) apiFunction ApiFn<ResponseType, ArgTypes >, ...newParameters as return dataReader updater Now our custom hook should be fully typed and we can take advantage of all the benefits TypeScript gives us: User { id: ; name: ; email: ; } fetchUser = (id: ): <User> => fetch( ); { [userReader, updateUserReader] = useAsyncResource(fetchUser, someIdFromSomewhere); ( <React.Suspense fallback= > <UserComponent userReader={userReader} /> < div>; } interface number string string const number Promise `path/to/user/ ` ${id} ( ) function AppComponent const // `userReader` is automatically a function that returns an object of type `User` // `updateUserReader` is automatically a function that takes a single argument of type number return // ... "loading..." /React.Suspense> ); } function UserComponent(props) { / / `user` is automatically an object of type User const user = props.userReader(); / / your IDE will happily provide full autocomplete for this object return <div>{user.name}</ Note how all types are inferred: we don’t need to manually specify them all over the place, as long as the api function has its types defined. Trying to call with other parameter types will trigger a type error. TS will also complain if we pass the wrong parameters to . updateUserReader useAsyncResource [userReader, updateUserReader] = useAsyncResource(fetchUser, , , ); updateUserReader( , ); // TS will complain about this const 'some' true 'params' // and this 'wrong' 'params' However, if we don’t pass any arguments to the hook other than the api function, the data reader will be lazily initialized: { [userReader, updateUserReader] = useAsyncResource(fetchUser); getNewUser = useCallback( { updateUserReader(newUserId); }, []); ( <button onClick={ getNewUser( )}> load user id < > < div>; } ( ) function AppComponent const // `userReader` is a function that returns `undefined` or an object of type `User` // `updateUserReader` is still a function that takes a single argument of type number const ( ) => newUserId: number return // ... => () 1 with 1 /button> <React.Suspense fallback="loading..."> <UserComponent userReader={userReader} / /React.Suspense> ); } function UserComponent(props) { / / here, `user` is `undefined` unless the button is clicked const user = props.userReader(); / / we need to add a type guard to get autocomplete further down if (!user) { return null; } / / now autocomplete works again for the User type object return <div>{user.name}</ Using the data reader with a modifier function also works as expected: { userObj.firstName + + userObj.lastName; } { userName = props.userReader(getUserDisplayName); <div>Name: {userName}< // a pure function that transforms the data of type User ( ) function getUserDisplayName userObj: User return ' ' ( ) function UserComponent props // `userName` is automatically typed as string const return /div>; } Resource caching There’s one more thing our custom hook is missing: resource caching. Subsequent calls with the same parameters for the same api function should return the same resource, and not trigger new, identical api calls. But we’d also like the power to clear cached results if we really wanted to re-fetch a resource. In a very simple implementation, we would use a with a hash function for the api function and the params as the key, and the data reader function as the value. We can go a bit further and create separate lists for each api function, so it’s easier to control the caches. Map Map caches = Map(); { (!caches.has(apiFn)) { caches.set(apiFn, Map()); } apiCache: Map< , DataOrModifiedFn<R>> = caches.get(apiFn); pKey = .stringify(params); { () { apiCache.get(pKey); }, (data: DataOrModifiedFn<R>) { apiCache.set(pKey, data); }, () { apiCache.delete(pKey); }, clear() { apiCache.clear(); } }; } const new export < , []>( ) function resourceCache R A extends any apiFn: ApiFn<R, A>, ...params: A | never[] // if there is no Map list defined for our api function, create one if new // get the Map list of caches for this api function only const string // "hash" the parameters into a unique key* const JSON // return some methods that let us control our cache return get return set return delete return return *Note: we’re using a naive “hashing” method here by converting the parameters to a simple JSON string. In a real scenario, you would want something more sophisticated, like object-hash . Now we can just use this in our data reader initializer: { cache = resourceCache(apiFn, ...parameters); cachedResource = cache.get(); (cachedResource) { cachedResource; } AsyncStatus = | | ; { } cache.set(dataReaderFn); dataReaderFn; } ( ) function initializeDataReader apiFn, ...parameters // check if we have a cached data reader and return it instead const const if return // otherwise continue creating it type 'init' 'done' 'error' // ... ( ) function dataReaderFn modifier // ... // cache the newly generated data reader function return That’s it! Now our resource is cached, so if we request it multiple times, we’ll get the same data reader function. If we want to clear a cache so we can re-fetch a specific piece of data, we can manually do so using the helper function we just created: [latestPosts, getPosts] = useAsyncResource(fetchLatestPosts, []); refreshLatestPosts = useCallback( { resourceCache(fetchLatestPosts).clear(); getPosts(); }, []); ( <button onClick={refreshLatestPosts}> fresh posts< const const => () // clear the cache so we force a new api call // refresh the data reader return // ... get /button> / / ... ); In this case, we’re clearing the entire cache for the api function. But you can also pass parameters to the helper function, so you only delete the cache for those specific ones: fetchLatestPosts [user, getUser] = useAsyncResource(fetchUser, id); refreshUserProfile = useCallback( { resourceCache(fetchUser, id).delete(); getUser(id); }, [id]); const const => () // only clear cache for user data reader for that id // get new user data Future-proofing We said in the beginning that the shiny new stuff is still in the works, but we’d like to take advantage of them once they’re officially released. So is our implementation compatible with what’s coming next? Well, yes. Let’s quickly look at some. Enabling Concurrent Mode First, we need to opt into making (the experimental version of) React work in concurrent mode: rootElement = .getElementById( ); ReactDOM.createRoot(rootElement).render( <App /> const document "root" ); // instead of the traditional ReactDOM.render( < /> App , rootElement) SuspenseList This helps us coordinate many components that can suspend by orchestrating the order in which these components are revealed to the user. <React.SuspenseList revealOrder= > <div>...user</div> <User userReader={userReader} /> </React.Suspense> <React.Suspense fallback={<div>...posts</div>}> <LatestPosts postsReader={postsReader} /> </React.Suspense> </React.SuspenseList> "forwards" < = React.Suspense fallback { }> In this example, if the posts load faster, React still waits for the user data to be fetched before rendering anything. useTransition This delays the rendering of a child component being suspended, rendering with old data until the new data is fetched. In other words, it prevents the Suspense boundary from rendering the loading indicator while the suspendable component is waiting for the new data. [user, getUser] = useAsyncResource(fetchUser, ); [startLoadingUser, isUserLoading] = useTransition({ : }); getRandomUser = useCallback( { startLoadingUser( { getUser( .ceil( .random() * )); }); }, []); ( <button onClick={getRandomUser} disabled={isUserLoading}>get random user< div>}> const 1 const timeoutMs 1000 const => () => () Math Math 1000 return // ... /button> <React.Suspense fallback={<div>...loading user</ ); < = /> User userReader {userReader} </ > React.Suspense Here, the message is not displayed while a new random user is being fetched, but the button is disabled. If fetching the new user data takes longer than 1 second, then the loading indicator is shown again. ...loading user Conclusion With a little bit of work, we managed to make ourselves a nice wrapper for api functions that works in a Suspense world. More importantly, we can start using this today! In fact, we already use it in production at OpenTable, in our Restaurant product. We started playing around with this at the beginning of 2020, and we now have refactored a small part of our application to use this technique. Compared to previous patterns we were using (like Redux-Observables), this one brings some key advantages that I’d like to point out. It’s simpler to write, read and understand Treating data like it’s available synchronously makes the biggest difference in the world, because your UI can fully be declarative. And that’s what React is all about! Not to mention the engineering time saved by shaving off the entire boilerplate that Redux and Redux-Observables were requiring. We can now write code much faster and more confident, bringing projects to life in record time. It’s “cancellable” Although not technically (you can’t prevent a fetch or a Promise to fulfill), as soon as you instantiate a new data reader, the old one is discarded. So stale or out of order updates just don’t happen anymore! This used to bring a lot of headaches to the team with traditional approaches. Then, after adopting Redux-Observables, we had to write A LOT of boilerplate: registering epics, listening for incoming actions, switch-mapping and calling the api (thus canceling any previously triggered one), finally dispatching another action that would update our redux store. It’s nothing new All the Redux + Observables code was living in external files too, so it would make understanding the logic of a single component way harder. Not to mention the learning curve associated with all this. Junior engineers would waste precious time reading cryptic code and intricate logic, instead of focusing on building product features. Instead, now we just update the data reader by calling the updater function! And that’s just plain old JavaScript. In closing, I’d like to leave you with so much. Ultimately, I think the beauty of the entire thing is in its simplicity. this thread about “Why Suspense matters”