UUID generation in React and Next.js is straightforward until it is not. The two main pitfalls are hydration mismatches (generating different values on server and client) and unstable keys (generating a new UUID on every render). This article covers both problems, the correct patterns for each framework, and the App Router server action approach.
The hydration mismatch problem
In a Next.js application with server-side rendering, React renders the component tree on the server and sends HTML to the browser. When the JavaScript loads, React renders again on the client and compares the result to the server HTML. If they differ, React throws a hydration error.
UUID generation breaks this because crypto.randomUUID() produces a different value each time:
// ❌ Hydration error — server and client produce different UUIDs
function Form() {
const formId = crypto.randomUUID(); // different on every render
return <form id={formId}>...</form>;
}
// ❌ Also wrong — useState initialiser runs on both server and client
function Form() {
const [id] = useState(() => crypto.randomUUID()); // mismatches on hydration
return <form id={id}>...</form>;
}
Fix 1: generate server-side only
In Next.js App Router, generate the UUID in a Server Component where there is no hydration:
// app/form/page.tsx — Server Component (no 'use client')
import { v7 as uuidv7 } from 'uuid';
export default function FormPage() {
const formId = uuidv7(); // runs once on the server, never on client
return <Form id={formId} />;
}
The formId is stable because it is generated once during server rendering and sent as a prop.
Fix 2: generate after mount with useEffect
For Client Components that need a UUID for purely client-side concerns:
'use client';
import { useState, useEffect } from 'react';
import { v7 as uuidv7 } from 'uuid';
function Form() {
const [formId, setFormId] = useState<string>('');
useEffect(() => {
setFormId(uuidv7()); // runs only on client, after hydration
}, []);
if (!formId) return null; // or a skeleton
return <form id={formId}>...</form>;
}
useEffect runs after hydration, so the server renders with an empty formId and the client populates it after mount. Avoid this pattern unless you genuinely need a client-generated ID — the flash of empty state is a UX cost.
Fix 3: suppress hydration warning (last resort)
For non-critical attributes where the mismatch is harmless:
<div id={crypto.randomUUID()} suppressHydrationWarning>
Only use this when the differing value does not affect functionality. Never suppress warnings for IDs used in form labels, ARIA attributes, or JavaScript logic.
UUID as React keys
React keys must be stable across renders. Using crypto.randomUUID() as a key directly is a common mistake:
// ❌ Wrong — generates new UUID on every render, causes remount
{items.map((item) => (
<Item key={crypto.randomUUID()} data={item} />
))}
// ✓ Correct — UUID assigned when item is created, stable thereafter
{items.map((item) => (
<Item key={item.id} data={item} /> // item.id is a UUID from the server or state
))}
If items come from a local state array and have no server-assigned ID, generate the UUID when the item is added to state — not during render:
'use client';
import { useState } from 'react';
import { v7 as uuidv7 } from 'uuid';
interface TodoItem {
id: string;
text: string;
}
function TodoList() {
const [todos, setTodos] = useState<TodoItem[]>([]);
function addTodo(text: string) {
setTodos((prev) => [...prev, { id: uuidv7(), text }]); // UUID assigned once
}
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li> // stable key
))}
</ul>
);
}
Next.js App Router: Server Actions
Server Actions are the cleanest pattern for UUID generation in App Router — the UUID is created on the server, never touches the client:
// app/actions.ts
'use server';
import { v7 as uuidv7 } from 'uuid';
import { db } from '@/lib/db';
export async function createOrder(formData: FormData) {
const orderId = uuidv7(); // server-only, no hydration concern
await db.order.create({
data: {
id: orderId,
userId: formData.get('userId') as string,
total: Number(formData.get('total')),
},
});
return { orderId };
}
// app/checkout/page.tsx
'use client';
import { createOrder } from '../actions';
function CheckoutForm() {
async function handleSubmit(formData: FormData) {
const { orderId } = await createOrder(formData);
// orderId is a UUID v7 generated on the server
window.location.href = `/orders/${orderId}`;
}
return <form action={handleSubmit}>...</form>;
}
useId() for accessibility — not UUIDs
React 18 added useId() for generating stable IDs for accessibility attributes (form labels, ARIA). It produces hydration-safe IDs but they are not UUIDs — they look like :r0:, :r1:, etc.
Use useId() when you need a stable HTML element ID for htmlFor/aria-labelledby. Use UUID v7 when you need a globally unique identifier for a data record.
// ✓ useId() for form accessibility
function EmailInput() {
const id = useId();
return (
<>
<label htmlFor={id}>Email</label>
<input id={id} type="email" />
</>
);
}
// ✓ UUID v7 for record identity
async function createUser(email: string) {
const userId = uuidv7();
await db.user.create({ data: { id: userId, email } });
return userId;
}
Summary
| Scenario | Correct approach |
|---|---|
| UUID for a data record | Generate in Server Component, Server Action, or event handler |
| UUID as a React key | Generate when item is created (in state update), not during render |
| UUID for an HTML element ID | Use useId() instead |
| UUID in a Client Component before data fetch | Generate in useEffect, initialise state as empty |
UUID in useState initialiser | Only safe in pure client apps with no SSR |
Further reading
- UUID in JavaScript — crypto.randomUUID(), uuid npm package, Prisma and Drizzle patterns
- TypeScript UUID Types — branded types for compile-time ID safety
- UUID v7 ORM Support in 2026 — Prisma and Drizzle UUID v7 configuration
Frequently asked questions
Why does generating a UUID in a React component cause errors?
React hydration renders the component on both server and client and compares results. crypto.randomUUID() produces a different value each time, so the server and client outputs differ — React throws a hydration mismatch error. Fix: generate UUIDs in Server Components, Server Actions, event handlers, or useEffect (after hydration) — never in the component body or useState initialiser.
Does JavaScript have a built-in UUID generator?
Yes for v4: crypto.randomUUID() is available in all modern browsers and Node.js 19+. For v7 there is no built-in — use a library like the uuid npm package or a short RFC 9562-compliant implementation.
Are these UUIDs cryptographically secure?
The randomness is, yes — it comes from the Web Crypto API. That said, a UUID is an identifier, not a secret; don't use one as a password or an unguessable capability token on its own.
Why does generating a UUID in a React component cause a hydration error?
React hydration works by running the component on the server, sending the HTML to the browser, then running the same component again on the client to attach event handlers. If you generate a UUID with crypto.randomUUID() inside the component body or a useState initialiser, the server and client produce different values — React detects the mismatch and throws a hydration error. The fix is to generate UUIDs outside the render cycle, in event handlers, useEffect, server actions, or before the component mounts.
Can I use a UUID as a React key?
Yes, but only if the UUID is stable across renders. Using crypto.randomUUID() directly in the key prop (key={crypto.randomUUID()}) generates a new UUID on every render, forcing React to unmount and remount the component every time — which is almost always wrong. Generate the UUID once when the item is created and store it in state or derive it from stable data.