Skip to content

bug(ai-react): useChat calls crypto.randomUUID during Next.js 16.3 App Router SSR #1089

Description

@MioYiSama

TanStack AI version

@tanstack/ai-react@0.19.2 from the current main source tree.

Framework/Library version

  • Next.js 16.3 with the App Router
  • React 19.2.3 in the repository test setup; the issue applies to React SSR generally
  • React Server Components / Client Components rendered through the App Router

This report is specifically about Next.js 16.3 App Router SSR. A component using useChat can be a Client Component, but the App Router still renders it on the server for the initial response, so the hook's render-time work executes in the SSR process.

Describe the bug and the steps to reproduce it

useChat constructs its internal ChatClient during render via useMemo. The React hook passes createChatDevtoolsBridge into the client, and the devtools bridge constructor generates a bridge ID. When globalThis.crypto.randomUUID exists, createBridgeId() calls it during the render phase.

The current call path is:

Next.js 16.3 App Router server render
  -> useChat render
  -> new ChatClient()
  -> createChatDevtoolsBridge()
  -> new ChatDevtoolsBridge()
  -> createBridgeId()
  -> crypto.randomUUID()

Relevant source locations:

A minimal Next.js 16.3 App Router reproduction is a client component rendered by an App Router route:

// app/chat.tsx
'use client'

import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'

export function Chat() {
  useChat({
    connection: fetchServerSentEvents('/api/chat'),
  })
  return null
}
// app/page.tsx
import { Chat } from './chat'

export default function Page() {
  return <Chat />
}

When Next.js 16.3 renders / through the App Router, the Chat Client Component is also evaluated during the server render. The connection is not used by this reproduction. The ChatClient and devtools bridge are constructed before effects or commit.

In an SSR runtime that restricts random-value generation during server/global execution, the render can fail when crypto.randomUUID() is reached. Even when it does not throw, each server render, hydration render, or React-discarded render receives a different bridge ID.

There is a second nondeterministic path when threadId is omitted: ChatClient generates a thread ID with Date.now() and Math.random() in the same constructor.

Expected behavior

Calling useChat from a Next.js 16.3 App Router Client Component during SSR should not perform nondeterministic random ID generation in the server render phase. SSR and hydration should be stable, and constructing a discarded render instance should not mint a new runtime identity as a side effect.

Actual behavior

useChat reaches crypto.randomUUID() while constructing ChatClient during the Next.js 16.3 App Router server render. The current useChat tests cover StrictMode and client recreation, but do not cover Next.js App Router SSR rendering or hydration with restricted/nondeterministic randomness.

Your Minimal, Reproducible Example

The Next.js 16.3 App Router reproduction above is repository-level and does not require an external sandbox.

Related records

  • #667 / #683 fixed module-scope random runtime-ID generation in @tanstack/ai-event-client; this issue is about render-time generation from useChat during Next.js App Router SSR.
  • #864 proposes replacing the useMemo client initializer with a useState lazy initializer. It addresses duplicate ChatClient construction when React discards useMemo, but it still constructs the client during render and does not remove the crypto.randomUUID() call.

PR

No PR attached.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions