React Libraries Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
- Return toDataStreamResponse: An AI SDK route handler must return
result.toDataStreamResponse()(notresult.textorNextResponse.json); otherwise you ship a full-response JSON blob and theuseChatstreaming UI never updates token-by-token. - Set maxSteps for Tool Calls: Tools defined without
maxStepsdefault to a single step, so the model can call a tool but never see its result and the conversation stalls — raise it to the maximum reasoning depth you actually want. - Use generateObject With Zod: For structured output,
generateObject({ schema })with a Zod schema enforces shape at the SDK layer; prompting for JSON and hand-parsing is brittle and wastes tokens on format instructions. - Encode Lambda Payload as Uint8Array:
InvokeCommandrejects plain objects at runtime — wrap withnew TextEncoder().encode(JSON.stringify(payload))and decode the response symmetrically, respecting the 6 MB sync / 256 KB async payload ceilings. - Check FunctionError, Not Catch: Lambda runtime errors arrive as a
FunctionErrorfield on the response, not as a thrown exception, so inspect the response envelope — async (Event) invocations drop errors silently unless you configure a DLQ. - Mind Polly Character Limits: The neural engine caps at 3,000 characters per request and the standard engine at 6,000, so chunk long text on sentence boundaries and verify neural support with
DescribeVoicesCommandbefore switching voices. - Wrap SSML in speak and Escape: SSML input requires
TextType: "ssml"and must be valid XML inside<speak>...</speak>with&,<,>escaped; also avoidpcmoutput for browsers since it is raw audio — usemp3orogg_vorbis. - Upload to S3 via Presigned PUT: Generate a presigned URL on the server and let the browser
PUTdirectly to S3 so large uploads bypass the Next.js request body limit (1 MB default) and never touch your server bandwidth. - Configure Bucket CORS for PUT: Browser-to-S3 uploads require explicit bucket CORS rules listing your origin, the
PUTmethod, and theContent-Typeheader — and always setContentTypeon the presigned command so the object isn't saved asapplication/octet-stream. - Keep AWS SDKs Server-Only: Never import
@aws-sdk/client-s3,client-lambda, orclient-pollyfrom a"use client"file — credentials end up in the browser bundle and AWS SDK v3 balloons client JS; use Server Actions or Route Handlers. - Use parseISO, Not new Date:
new Date("2026-04-06")parses date-only ISO strings as UTC midnight and shifts visibly in non-UTC zones, so preferparseISO(plusdate-fns-tzfor zone-aware work) to avoid silent off-by-one-day bugs. - Mind MM vs mm Format Tokens: date-fns follows Unicode TR#35, where
MMis month,mmis minutes, andDDis invalid (usedd) — case mistakes compile fine and produce silently wrong strings. - Format Dates on the Client: Server-rendered formatted dates cause hydration mismatches whenever the server timezone differs from the client, so format in a Client Component after mount or pass the raw timestamp and format in the browser.
- Wrap Drag Items in DragOverlay:
DragOverlayrenders the drag preview outside the DOM flow and fixes the "item jumps" problem when the original slot collapses; combine with reduced opacity on the original for a stable visual. - Require Pointer Movement to Start Drag: Add
activationConstraint: { distance: 8 }(or a delay) toPointerSensorso a single click does not start a drag; without it, every click on a sortable item triggers accidental drags. - Globally Unique dnd-kit IDs: dnd-kit identifies items by ID alone across all containers, so duplicate IDs across columns break multi-container drops — use UUIDs or container-prefixed keys rather than per-list indexes.
- Import From lodash-es: Use named imports from
lodash-esso the bundler tree-shakes unused utilities;import _ from "lodash"pulls in the whole ~70 KB CJS build even for a single helper. - Memoize debounce Creation: Creating
debounce(fn, 300)inline in render returns a fresh instance every render and never accumulates calls; wrap creation inuseMemo/useCallbackand call.cancel()in theuseEffectcleanup so pending invocations don't fire after unmount. - Pass a Fresh Target to merge: Lodash's
merge(target, source)mutates its first argument, so always pass a fresh{}(orcloneDeepfirst) as the target; also prefer nativestructuredCloneovercloneDeepunless you need function or class-instance support. - Generate a 32+ Char AUTH_SECRET: Auth.js crashes in production without
AUTH_SECRET, and weak secrets compromise every session — generate one withnpx auth secretand use distinct values per environment so a leaked dev key never compromises prod. - Credentials Provider Is JWT-Only: The Credentials provider cannot use database sessions even with an adapter installed, and extending session types requires both the
jwt/sessioncallback pair and a module augmentation intypes/next-auth.d.tsor TypeScript drifts from runtime. - Use a Singleton Prisma Client: In dev, attach the Prisma client to
globalThisso hot module reload does not open a fresh connection pool on every save — without this the database quickly refuses connections and the dev server appears to hang. - Convert BigInt Before Serialization: Prisma BigInt fields cannot cross the Server→Client boundary via JSON, so map them to
NumberorStringbefore passing to Client Components, and select only the relations you need (withinclude/select) to avoid N+1 queries. - QueryClient Lives in useState: Construct the TanStack Query
QueryClientinsideuseState(() => new QueryClient())within a client provider; a module-level client in a Server Component leaks cache state across user requests and leaks data between users. - Understand staleTime vs gcTime:
staleTimecontrols when data is considered fresh and whether refetches happen;gcTimecontrols how long inactive queries stay in the cache —staleTime: Infinitystill allows garbage collection, and mutations never auto-invalidate related queries without explicitinvalidateQueries.