-
-
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> | |
} |
How to use React Server Components with async/await
- Initialize a new promise (don't await it yet!)
- Define a Suspense boundary (to show a fallback while the promise is pending)
- Pass the promise to child components as a prop (they can be React Server Components or React Client Components)
How to use React Client Components with async/await
- Initialize a new promise (don't await it yet!)
- Define a Suspense boundary (to show a fallback while the promise is pending)
- Pass the promise to child components as a prop (they can't be React Server Components)
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.
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.
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.
What about caching?
I dunno. That's a problem for whatever thing creates the promises. Leave React out of it
Two kinds of components
Where to create promises
Suspense boundaries
Will handle suspense fallback while waiting for a promise to resolve.
Suspendable Components
May trigger suspense while waiting for a promise to resolve.
promise.then(...)
, because that will initialize a new promiseReact.use