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

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