Skip to content
Support me
Buy Me a Coffee
Contact me
hey@aiktb.dev @aiktb39 t.me/aiktb

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:

vue
<script setup lang="ts">
import { Suspense } from 'vue'
</script>

<template>
  <Suspense>
    <!-- Default slot -->
    <Dashboard />
    <template #fallback> Loading... </template>
  </Suspense>
</template>
vue
<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:

bash
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:

json
{
  "compilerOptions": {
    "types": ["react/canary", "react-dom/canary"]
  }
}

Using the use Hook

Now, let's write some examples similar to Vue Suspense:

tsx
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:

tsx
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.