Skip to content

Instantly share code, notes, and snippets.

@subtleGradient
Created April 10, 2024 20:06
Show Gist options
  • Save subtleGradient/fb3a40f37ccadd1920a19524c5a5d644 to your computer and use it in GitHub Desktop.
Save subtleGradient/fb3a40f37ccadd1920a19524c5a5d644 to your computer and use it in GitHub Desktop.
/* eslint-disable @typescript-eslint/no-unused-vars */
"use client"
import { Suspense, use, useMemo } from "react"
import { Text } from "react-native"
import { waitForTransactionReceipt } from "src/lib/viem"
import { PromisedValue } from "src/utils/type-helpers"
import { Address } from "viem"
type TransactionReceipt = PromisedValue<ReturnType<typeof waitForTransactionReceipt>>
/**
* # Two kinds of components
*
* 1. Suspense boundaries
* 2. Suspendable Client Components
*
* ## Where to create promises
*
* ### Suspense boundaries
* Will handle suspense fallback while waiting for a promise to resolve.
*
* 1. CAN initialize promises
* 2. must NOT await promises
* 3. must pass promises to child components as props
*
* ### Suspendable Components
* May trigger suspense while waiting for a promise to resolve.
*
* 1. must NOT initialize promises
* 1. must NOT initialize promises in useMemo
* 2. must NOT call `promise.then(...)`, because that will initialize a new promise
* 3. CAN await promises using {@link React.use}
*/
////////////////////////// USAGE EXAMPLES //////////////////////////
// These examples demonstrate how to use React Server Components and React Client Components with async/await.
/**
* # How to use React Server Components with async/await
*
* 1. Initialize a new promise (don't await it yet!)
* 2. Define a Suspense boundary (to show a fallback while the promise is pending)
* 3. Pass the promise to child components as a prop (they can be React Server Components or React Client Components)
*/
function TransactionReceiptView_Suspense_Server(props: { hash: Address; chainId: number }) {
"use server"
// useMemo is not necessary on the server
const promisedReceipt = waitForTransactionReceipt(props.hash, props.chainId)
return (
<Suspense fallback={<Text>loading...</Text>}>
{/* @ts-expect-error -- async components are not supported in React Native yet */}
<TransactionReceiptView_Server receipt={promisedReceipt} />
<TransactionReceiptView_Client receipt={promisedReceipt} />
</Suspense>
)
}
/**
* # How to use React Client Components with async/await
*
* 1. Initialize a new promise (don't await it yet!)
* 2. Define a Suspense boundary (to show a fallback while the promise is pending)
* 3. Pass the promise to child components as a prop (they can't be React Server Components)
*/
function TransactionReceiptView_Suspense_Client(p: { hash: Address; chainId: number }) {
// eventually useMemo won't be necessary
const promisedReceipt = useMemo(() => waitForTransactionReceipt(p.hash, p.chainId), [p.hash, p.chainId])
return (
<Suspense fallback={<Text>loading...</Text>}>
<TransactionReceiptView_Client receipt={promisedReceipt} />
</Suspense>
)
}
////////////////////////// GOOD EXAMPLES //////////////////////////
/** React Server Components CAN use async/await directly */
async function TransactionReceiptView_Server({ receipt: promisedReceipt }: { receipt: Promise<TransactionReceipt> }) {
"use server"
const receipt = await promisedReceipt
return <Text>{receipt.status}</Text>
}
/** React Client Components can NOT use async/await directly */
function TransactionReceiptView_Client({ receipt: promisedReceipt }: { receipt: Promise<TransactionReceipt> }) {
const receipt = use(promisedReceipt)
return <Text>{receipt.status}</Text>
}
////////////////////////// GOOD EXAMPLES //////////////////////////
/**
* # How to use React Server Components with multiple promises
* At first glance, you might expect this to be a bad example. But it's actually good.
*
* Typically you would expect to see {@link Promise.all} used to wait for multiple promises.
* But in this example, we are intentionally awaiting each promise separately.
*
* # THIS IS FINE
*
* because the asyncronous work has already been initiated outside of this component.
* {@link Promise.all} is not necessary because the promises are already in flight.
*/
async function TransactionReceiptView_Server2(props: {
receipt1: Promise<TransactionReceipt>
receipt2: Promise<TransactionReceipt>
}) {
"use server"
const receipt1 = await props.receipt1
const receipt2 = await props.receipt2
return (
<>
<Text>{receipt1.status}</Text>
<Text>{receipt2.status}</Text>
</>
)
}
/**
* # How to use React Client Components with multiple promises
*
* Just like the server component, this is fine because the asyncronous work has already been initiated outside of this component.
*/
function TransactionReceiptView_Client2(props: {
receipt1: Promise<TransactionReceipt>
receipt2: Promise<TransactionReceipt>
}) {
const receipt1 = use(props.receipt1)
const receipt2 = use(props.receipt2)
return (
<>
<Text>{receipt1.status}</Text>
<Text>{receipt2.status}</Text>
</>
)
}
////////////////////////// BAD EXAMPLES //////////////////////////
/**
* Technically, React Server Components CAN await promises immediately after initializing them
* but it's better to initiate the promise outside of the component that consumes it.
*/
async function TransactionReceiptView_Server1(props: { hash: Address; chainId: number }) {
"use server"
const receipt = await waitForTransactionReceipt(props.hash, props.chainId)
return <Text>{receipt.status}</Text>
}
////////////////////////// BUG EXAMPLES //////////////////////////
/**
* @deprecated -- this is a bad pattern that triggers an infinite loop
* because the promise is created every time the component is rendered.
* But {@link use} expects a stable reference to the promise.
*/
function TransactionReceiptView_Client__BAD__TRIGGERS_AN_INFINITE_LOOP__(props: { hash: Address; chainId: number }) {
// BUG: can't consume a promise in the same component that creates it
const promisedReceipt = waitForTransactionReceipt(props.hash, props.chainId)
// BUG: triggers an infinite loop because the promise is recreated every time
const receipt = use(promisedReceipt)
return <Text>{receipt.status}</Text>
}
/**
* @deprecated -- this is a bad pattern that triggers an infinite loop
* because the promise is created every time the component is rendered.
* But {@link use} expects a stable reference to the promise.
*
* You may expect {@link useMemo} to make the reference stable, but it does not.
* Because {@link use} triggers suspense, which resets the memoization cache.
*/
function TransactionReceiptView_Client__BAD__TRIGGERS_AN_INFINITE_LOOP2__(props: { hash: Address; chainId: number }) {
// BUG: useMemo can't memoize something that triggers suspense
const promisedReceipt = useMemo(
() => waitForTransactionReceipt(props.hash, props.chainId),
[props.hash, props.chainId],
)
// BUG: triggers an infinite loop
const receipt = use(promisedReceipt)
return <Text>{receipt.status}</Text>
}
@subtleGradient
Copy link
Author

How to use React Server Components with async/await

  1. Initialize a new promise (don't await it yet!)
  2. Define a Suspense boundary (to show a fallback while the promise is pending)
  3. Pass the promise to child components as a prop (they can be React Server Components or React Client Components)

@subtleGradient
Copy link
Author

How to use React Client Components with async/await

  1. Initialize a new promise (don't await it yet!)
  2. Define a Suspense boundary (to show a fallback while the promise is pending)
  3. Pass the promise to child components as a prop (they can't be React Server Components)

@subtleGradient
Copy link
Author

How to use React Server Components with multiple promises

At first glance, you might expect this to be a bad example. But it's actually good.

Typically you would expect to see Promise.all used to wait for multiple promises.
But in this example, we are intentionally awaiting each promise separately.

THIS IS FINE

because the asynchronous work has already been initiated outside of this component.
@link Promise.all is not necessary because the promises are already in flight.

@subtleGradient
Copy link
Author

How to use React Client Components with multiple promises

Just like the server component, this is fine because the asynchronous work has already been initiated outside of this component.

@subtleGradient
Copy link
Author

Technically, React Server Components CAN await promises immediately after initializing them
but it's (usually) better to initiate the promise outside of the component that consumes it.

@subtleGradient
Copy link
Author

What about caching?

I dunno. That's a problem for whatever thing creates the promises. Leave React out of it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment