Enhancing React DX with the use
Hook and Suspense Component
Introduction
In React, working with asynchronous logic using async/await
can lead to a less-than-ideal experience due to the synchronous nature of component logic. This often results in the use of IIFE, diminishing the effectiveness of the await
keyword in simplifying JavaScript asynchronous logic.
For example, if you're familiar with Vue Suspense, you might have encountered code similar to the following:
<script setup lang="ts">
import { Suspense } from 'vue'
</script>
<template>
<Suspense>
<!-- Default slot -->
<Dashboard />
<template #fallback> Loading... </template>
</Suspense>
</template>
<script setup>
const res = await fetch('...')
const posts = await res.json()
</script>
<template>
{{ posts }}
</template>
However, in React 18.2, using await
at the top level of a functional component results in errors without the comprehensive support of React Suspense. Unless using Next.js, opportunities to utilize Suspense are limited.
Fortunately, the React team acknowledges these limitations. While direct support for top-level await
is not yet available (refer to this RFC), they are making efforts to enhance the DX for writing asynchronous logic in React. One significant improvement is the introduction of the use
hook in React 18.3, which bears similarities to the await
keyword and provides an experience akin to Vue Suspense.
Obtaining react@canary
As of this writing, react@18.3
has not been officially released. To access the use
hook in React, you'll need to install dependencies from react@canary
(an alias for react@next
). Use the following command:
pnpm add -D react@canary react-dom@canary @types/react @types/react-dom
If you're using TypeScript with TSX, include the following configuration in your tsconfig.json
for type hints:
{
"compilerOptions": {
"types": ["react/canary", "react-dom/canary"]
}
}
Using the use
Hook
Now, let's write some examples similar to Vue Suspense:
import { Suspense, use } from 'react'
export default function Page() {
return (
<Suspense fallback={<>Loading...</>}>
<Dashboard />
</Suspense>
)
}
function Dashboard() {
const res = use(fetch('...'))
const posts = res.json()
return <>{posts}</>
}
However, there's a catch in this code. When introducing reactive logic in the Dashboard
component, you'll notice that the code doesn't behave as expected. Every time reactive logic is triggered, React re-renders the entire Dashboard
component, causing the use
hook to trigger again. This results in the display of the fallback content until the promise is resolved. In essence, the page keeps toggling between the fallback content and the asynchronously fetched data, which is not the desired behavior.
The correct approach is to let the Dashboard
component receive a promise prop and use this prop with the use
hook internally. Once the promise is resolved, even if the component re-renders and the use
hook is called again, an already resolved promise won't trigger the Suspense fallback.
Let's take a look at the corrected code:
import { Suspense, use } from 'react'
export default function Page() {
return (
<Suspense fallback={<>Loading...</>}>
<Dashboard promise={fetch('...')} />
</Suspense>
)
}
function Dashboard({ promise }: { promise: Promise<Response> }) {
const res = use(promise)
const posts = res.json()
return <>{posts}</>
}
Limitations
While React use
hook and Suspense
seem promising, comparing them with Vue Suspense reveals significant limitations.
Firstly, any component using the use
hook needs to be wrapped in Suspense
. Additionally, any use(promise)
within a component triggers a fallback in the presence of an unresolved promise. This restricts their widespread use, providing only occasional opportunities. In contrast, Vue allows unrestricted use of await/async
without the need for special wrappers, resulting in a significantly lower cognitive load compared to React.
Secondly, React Suspense
introduces noticeable CLS(Cumulative Layout Shift) during the initial render. This occurs even if your promises resolve quickly (e.g., fetching resources from local storage). In comparison, Vue Suspense almost immediately renders content when promises resolve quickly, minimizing any noticeable CLS.
Conclusion
This is expected to be one of the most common use cases for the use
hook and the Suspense
component in the future. The use
hook has some additional important features, such as use(content)
and if (...) { use(...)}
, but those are not our focus here. It's promising to see how these additions from the React team have successfully improved the asynchronous DX in React.
However, the impact of the use
hook on enhancing the writing of asynchronous logic in React remains somewhat limited. At least compared to Vue, DX still has a clear gap.