I was wiring up a dashboard page after a long session of getting the project running on Cloudflare Workers. Then I hit an error that made no sense: TypeError: items.map is not a function. The data was there. I could see it in the console. .map had always worked. I tried searching online but the answers didn’t make any sense. It took me about an hour to figure out the actual cause.
The pattern that looks perfectly fine
React Router’s <Await> component is designed for streaming. Kick off a slow fetch in your loader without blocking the page render, and the result streams in when it’s ready.
The code is clean:
export async function loader({ context }: Route.LoaderArgs) {
const activityPromise = db
.select()
.from(activity)
.orderBy(desc(activity.createdAt))
return { activityPromise } // not awaited — intentional
}export default function Dashboard() {
const { activityPromise } = useLoaderData<typeof loader>()
return (
<Suspense fallback={<Skeleton />}>
<Await resolve={activityPromise}>
{(items) => <Feed items={items} />}
</Await>
</Suspense>
)
}This is straight out of the React Router docs. It works great in Node. It does not work on Cloudflare Workers.
The error that tells you nothing
TypeError: items.map is not a functionThat’s it. No stack trace pointing at <Await>. No mention of Promises. Just a .map failure inside your component, on data you know is an array.
You add a console.log. The array is there. The data is correct. Everything looks fine, until you realize the problem isn’t your data. It’s what <Await> received.
What’s actually happening
Cloudflare Workers doesn’t support defer(). It’s not in the production bundle.
What this means: React Router resolves every Promise in your loader before it sends the response. There is no streaming. By the time useLoaderData() runs on the client, activityPromise is already a plain array, not a Promise.
<Await> does one thing when it receives its resolve prop: it calls .then() on it, expecting a Promise. A plain array doesn’t have .then(). So it falls through to your render function with the raw value, your component tries to call .map() on something it didn’t expect, and you get an error that points nowhere useful.
The Promise resolved. The data arrived. The wrapper was just wrong for the environment.
The fix
Await everything directly in the loader. Return plain values.
export async function loader({ context }: Route.LoaderArgs) {
const items = await db
.select()
.from(activity)
.orderBy(desc(activity.createdAt))
return { items } // plain array, not a Promise
}export default function Dashboard() {
const { items } = useLoaderData<typeof loader>()
return <Feed items={items} />
}Remove <Suspense>, <Await>, and their imports entirely. They serve no purpose here. The data is already resolved before the page loads.
What about loading states?
If you want a skeleton while the page loads, use a route-level HydrateFallback instead:
export function HydrateFallback() {
return <Skeleton />
}This renders during SSR hydration, which is the actual moment of uncertainty on Cloudflare, not mid-stream data arrival.
The rule
On Cloudflare Workers: always await in the loader, always return plain values. <Await> and defer() are Node-only patterns.
The docs don’t say this clearly. Now you don’t have to learn it the same way.