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:

loader.ts
export async function loader({ context }: Route.LoaderArgs) {
  const activityPromise = db
    .select()
    .from(activity)
    .orderBy(desc(activity.createdAt))
  return { activityPromise } // not awaited — intentional
}
route.tsx
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 function

That’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.

loader.ts
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
}
route.tsx
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:

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