From 4b00d3a3814ab4cfc9fa3e938b7c9518b1e5047f Mon Sep 17 00:00:00 2001 From: 1shCha Date: Sat, 21 Mar 2026 02:46:15 -0400 Subject: [PATCH 1/4] created folders only fetch endpoint for chrome extension --- .gitignore | 3 +++ src/app/api/workspaces/[id]/folders/route.ts | 26 ++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/app/api/workspaces/[id]/folders/route.ts diff --git a/.gitignore b/.gitignore index 81dc4e73..25f74a47 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# Claude Code +CLAUDE.md + # dependencies /node_modules .pnpm-store diff --git a/src/app/api/workspaces/[id]/folders/route.ts b/src/app/api/workspaces/[id]/folders/route.ts new file mode 100644 index 00000000..3e43fe83 --- /dev/null +++ b/src/app/api/workspaces/[id]/folders/route.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse } from "next/server"; +import { loadWorkspaceState } from "@/lib/workspace/state-loader"; +import { requireAuth, verifyWorkspaceAccess, withErrorHandling } from "@/lib/api/workspace-helpers"; + +/** + * GET /api/workspaces/[id]/folders + * Returns only folder items from the workspace state. + * Lightweight alternative to GET /api/workspaces/[id] for clients (e.g. Chrome extension) + * that only need folder data without the full workspace payload. + */ +async function handleGET( + _request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params; + const userId = await requireAuth(); + + await verifyWorkspaceAccess(id, userId, "viewer"); + + const state = await loadWorkspaceState(id); + const folders = state.items.filter((item) => item.type === "folder"); + + return NextResponse.json({ folders }); +} + +export const GET = withErrorHandling(handleGET, "GET /api/workspaces/[id]/folders"); From b13ca2321bd7bd640f69f9ed30be7604a63a4e19 Mon Sep 17 00:00:00 2001 From: 1shCha Date: Thu, 26 Mar 2026 17:18:55 -0400 Subject: [PATCH 2/4] folder enpoint branch for chrome extension merged --- .../better-auth-best-practices/SKILL.md | 166 ++ .agents/skills/create-auth-skill/SKILL.md | 214 ++ .agents/skills/git-guidelines/SKILL.md | 104 + .agents/skills/update/SKILL.md | 94 + .agents/skills/update/references/ai-sdk-v6.md | 1928 +++++++++++++ .../skills/update/references/assistant-ui.md | 261 ++ .../update/references/breaking-changes.md | 108 + .../vercel-react-best-practices/AGENTS.md | 2410 +++++++++++++++++ .../vercel-react-best-practices/SKILL.md | 125 + .../rules/advanced-event-handler-refs.md | 55 + .../rules/advanced-use-latest.md | 49 + .../rules/async-api-routes.md | 38 + .../rules/async-defer-await.md | 80 + .../rules/async-dependencies.md | 36 + .../rules/async-parallel.md | 28 + .../rules/async-suspense-boundaries.md | 99 + .../rules/bundle-barrel-imports.md | 59 + .../rules/bundle-conditional.md | 31 + .../rules/bundle-defer-third-party.md | 49 + .../rules/bundle-dynamic-imports.md | 35 + .../rules/bundle-preload.md | 50 + .../rules/client-event-listeners.md | 74 + .../rules/client-localstorage-schema.md | 71 + .../rules/client-passive-event-listeners.md | 48 + .../rules/client-swr-dedup.md | 56 + .../rules/js-batch-dom-css.md | 82 + .../rules/js-cache-function-results.md | 80 + .../rules/js-cache-property-access.md | 28 + .../rules/js-cache-storage.md | 70 + .../rules/js-combine-iterations.md | 32 + .../rules/js-early-exit.md | 50 + .../rules/js-hoist-regexp.md | 45 + .../rules/js-index-maps.md | 37 + .../rules/js-length-check-first.md | 49 + .../rules/js-min-max-loop.md | 82 + .../rules/js-set-map-lookups.md | 24 + .../rules/js-tosorted-immutable.md | 57 + .../rules/rendering-activity.md | 26 + .../rules/rendering-animate-svg-wrapper.md | 47 + .../rules/rendering-conditional-render.md | 40 + .../rules/rendering-content-visibility.md | 38 + .../rules/rendering-hoist-jsx.md | 46 + .../rules/rendering-hydration-no-flicker.md | 82 + .../rules/rendering-svg-precision.md | 28 + .../rules/rerender-defer-reads.md | 39 + .../rules/rerender-dependencies.md | 45 + .../rules/rerender-derived-state.md | 29 + .../rules/rerender-functional-setstate.md | 74 + .../rules/rerender-lazy-state-init.md | 58 + .../rules/rerender-memo.md | 44 + .../rules/rerender-transitions.md | 40 + .../rules/server-after-nonblocking.md | 73 + .../rules/server-cache-lru.md | 41 + .../rules/server-cache-react.md | 76 + .../rules/server-parallel-fetching.md | 83 + .../rules/server-serialization.md | 38 + .agents/skills/web-design-guidelines/SKILL.md | 39 + AGENTS.md | 97 + 58 files changed, 7987 insertions(+) create mode 100644 .agents/skills/better-auth-best-practices/SKILL.md create mode 100644 .agents/skills/create-auth-skill/SKILL.md create mode 100644 .agents/skills/git-guidelines/SKILL.md create mode 100644 .agents/skills/update/SKILL.md create mode 100644 .agents/skills/update/references/ai-sdk-v6.md create mode 100644 .agents/skills/update/references/assistant-ui.md create mode 100644 .agents/skills/update/references/breaking-changes.md create mode 100644 .agents/skills/vercel-react-best-practices/AGENTS.md create mode 100644 .agents/skills/vercel-react-best-practices/SKILL.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-api-routes.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-defer-await.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-dependencies.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-parallel.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-conditional.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-preload.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-event-listeners.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-cache-storage.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-early-exit.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-index-maps.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-length-check-first.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-activity.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-memo.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-transitions.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-cache-lru.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-cache-react.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-serialization.md create mode 100644 .agents/skills/web-design-guidelines/SKILL.md create mode 100644 AGENTS.md diff --git a/.agents/skills/better-auth-best-practices/SKILL.md b/.agents/skills/better-auth-best-practices/SKILL.md new file mode 100644 index 00000000..3458e073 --- /dev/null +++ b/.agents/skills/better-auth-best-practices/SKILL.md @@ -0,0 +1,166 @@ +--- +name: better-auth-best-practices +description: Skill for integrating Better Auth - the comprehensive TypeScript authentication framework. +--- + +# Better Auth Integration Guide + +**Always consult [better-auth.com/docs](https://better-auth.com/docs) for code examples and latest API.** + +Better Auth is a TypeScript-first, framework-agnostic auth framework supporting email/password, OAuth, magic links, passkeys, and more via plugins. + +--- + +## Quick Reference + +### Environment Variables +- `BETTER_AUTH_SECRET` - Encryption secret (min 32 chars). Generate: `openssl rand -base64 32` +- `BETTER_AUTH_URL` - Base URL (e.g., `https://example.com`) + +Only define `baseURL`/`secret` in config if env vars are NOT set. + +### File Location +CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--config` for custom path. + +### CLI Commands +- `npx @better-auth/cli@latest migrate` - Apply schema (built-in adapter) +- `npx @better-auth/cli@latest generate` - Generate schema for Prisma/Drizzle +- `npx @better-auth/cli mcp --cursor` - Add MCP to AI tools + +**Re-run after adding/changing plugins.** + +--- + +## Core Config Options + +| Option | Notes | +|--------|-------| +| `appName` | Optional display name | +| `baseURL` | Only if `BETTER_AUTH_URL` not set | +| `basePath` | Default `/api/auth`. Set `/` for root. | +| `secret` | Only if `BETTER_AUTH_SECRET` not set | +| `database` | Required for most features. See adapters docs. | +| `secondaryStorage` | Redis/KV for sessions & rate limits | +| `emailAndPassword` | `{ enabled: true }` to activate | +| `socialProviders` | `{ google: { clientId, clientSecret }, ... }` | +| `plugins` | Array of plugins | +| `trustedOrigins` | CSRF whitelist | + +--- + +## Database + +**Direct connections:** Pass `pg.Pool`, `mysql2` pool, `better-sqlite3`, or `bun:sqlite` instance. + +**ORM adapters:** Import from `better-auth/adapters/drizzle`, `better-auth/adapters/prisma`, `better-auth/adapters/mongodb`. + +**Critical:** Better Auth uses adapter model names, NOT underlying table names. If Prisma model is `User` mapping to table `users`, use `modelName: "user"` (Prisma reference), not `"users"`. + +--- + +## Session Management + +**Storage priority:** +1. If `secondaryStorage` defined → sessions go there (not DB) +2. Set `session.storeSessionInDatabase: true` to also persist to DB +3. No database + `cookieCache` → fully stateless mode + +**Cookie cache strategies:** +- `compact` (default) - Base64url + HMAC. Smallest. +- `jwt` - Standard JWT. Readable but signed. +- `jwe` - Encrypted. Maximum security. + +**Key options:** `session.expiresIn` (default 7 days), `session.updateAge` (refresh interval), `session.cookieCache.maxAge`, `session.cookieCache.version` (change to invalidate all sessions). + +--- + +## User & Account Config + +**User:** `user.modelName`, `user.fields` (column mapping), `user.additionalFields`, `user.changeEmail.enabled` (disabled by default), `user.deleteUser.enabled` (disabled by default). + +**Account:** `account.modelName`, `account.accountLinking.enabled`, `account.storeAccountCookie` (for stateless OAuth). + +**Required for registration:** `email` and `name` fields. + +--- + +## Email Flows + +- `emailVerification.sendVerificationEmail` - Must be defined for verification to work +- `emailVerification.sendOnSignUp` / `sendOnSignIn` - Auto-send triggers +- `emailAndPassword.sendResetPassword` - Password reset email handler + +--- + +## Security + +**In `advanced`:** +- `useSecureCookies` - Force HTTPS cookies +- `disableCSRFCheck` - ⚠️ Security risk +- `disableOriginCheck` - ⚠️ Security risk +- `crossSubDomainCookies.enabled` - Share cookies across subdomains +- `ipAddress.ipAddressHeaders` - Custom IP headers for proxies +- `database.generateId` - Custom ID generation or `"serial"`/`"uuid"`/`false` + +**Rate limiting:** `rateLimit.enabled`, `rateLimit.window`, `rateLimit.max`, `rateLimit.storage` ("memory" | "database" | "secondary-storage"). + +--- + +## Hooks + +**Endpoint hooks:** `hooks.before` / `hooks.after` - Array of `{ matcher, handler }`. Use `createAuthMiddleware`. Access `ctx.path`, `ctx.context.returned` (after), `ctx.context.session`. + +**Database hooks:** `databaseHooks.user.create.before/after`, same for `session`, `account`. Useful for adding default values or post-creation actions. + +**Hook context (`ctx.context`):** `session`, `secret`, `authCookies`, `password.hash()`/`verify()`, `adapter`, `internalAdapter`, `generateId()`, `tables`, `baseURL`. + +--- + +## Plugins + +**Import from dedicated paths for tree-shaking:** +``` +import { twoFactor } from "better-auth/plugins/two-factor" +``` +NOT `from "better-auth/plugins"`. + +**Popular plugins:** `twoFactor`, `organization`, `passkey`, `magicLink`, `emailOtp`, `username`, `phoneNumber`, `admin`, `apiKey`, `bearer`, `jwt`, `multiSession`, `sso`, `oauthProvider`, `oidcProvider`, `openAPI`, `genericOAuth`. + +Client plugins go in `createAuthClient({ plugins: [...] })`. + +--- + +## Client + +Import from: `better-auth/client` (vanilla), `better-auth/react`, `better-auth/vue`, `better-auth/svelte`, `better-auth/solid`. + +Key methods: `signUp.email()`, `signIn.email()`, `signIn.social()`, `signOut()`, `useSession()`, `getSession()`, `revokeSession()`, `revokeSessions()`. + +--- + +## Type Safety + +Infer types: `typeof auth.$Infer.Session`, `typeof auth.$Infer.Session.user`. + +For separate client/server projects: `createAuthClient()`. + +--- + +## Common Gotchas + +1. **Model vs table name** - Config uses ORM model name, not DB table name +2. **Plugin schema** - Re-run CLI after adding plugins +3. **Secondary storage** - Sessions go there by default, not DB +4. **Cookie cache** - Custom session fields NOT cached, always re-fetched +5. **Stateless mode** - No DB = session in cookie only, logout on cache expiry +6. **Change email flow** - Sends to current email first, then new email + +--- + +## Resources + +- [Docs](https://better-auth.com/docs) +- [Options Reference](https://better-auth.com/docs/reference/options) +- [LLMs.txt](https://better-auth.com/llms.txt) +- [GitHub](https://github.com/better-auth/better-auth) +- [Init Options Source](https://github.com/better-auth/better-auth/blob/main/packages/core/src/types/init-options.ts) \ No newline at end of file diff --git a/.agents/skills/create-auth-skill/SKILL.md b/.agents/skills/create-auth-skill/SKILL.md new file mode 100644 index 00000000..b18c85f2 --- /dev/null +++ b/.agents/skills/create-auth-skill/SKILL.md @@ -0,0 +1,214 @@ +--- +name: create-auth-skill +description: Skill for creating auth layers in TypeScript/JavaScript apps using Better Auth. +--- + +# Create Auth Skill + +Guide for adding authentication to TypeScript/JavaScript applications using Better Auth. + +**For code examples and syntax, see [better-auth.com/docs](https://better-auth.com/docs).** + +--- + +## Decision Tree + +``` +Is this a new/empty project? +├─ YES → New project setup +│ 1. Identify framework +│ 2. Choose database +│ 3. Install better-auth +│ 4. Create auth.ts + auth-client.ts +│ 5. Set up route handler +│ 6. Run CLI migrate/generate +│ 7. Add features via plugins +│ +└─ NO → Does project have existing auth? + ├─ YES → Migration/enhancement + │ • Audit current auth for gaps + │ • Plan incremental migration + │ • See migration guides in docs + │ + └─ NO → Add auth to existing project + 1. Analyze project structure + 2. Install better-auth + 3. Create auth config + 4. Add route handler + 5. Run schema migrations + 6. Integrate into existing pages +``` + +--- + +## Installation + +**Core:** `npm install better-auth` + +**Scoped packages (as needed):** +| Package | Use case | +|---------|----------| +| `@better-auth/passkey` | WebAuthn/Passkey auth | +| `@better-auth/sso` | SAML/OIDC enterprise SSO | +| `@better-auth/stripe` | Stripe payments | +| `@better-auth/scim` | SCIM user provisioning | +| `@better-auth/expo` | React Native/Expo | + +--- + +## Environment Variables + +```env +BETTER_AUTH_SECRET=<32+ chars, generate with: openssl rand -base64 32> +BETTER_AUTH_URL=http://localhost:3000 +DATABASE_URL= +``` + +Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE_CLIENT_ID`, etc. + +--- + +## Server Config (auth.ts) + +**Location:** `lib/auth.ts` or `src/lib/auth.ts` + +**Minimal config needs:** +- `database` - Connection or adapter +- `emailAndPassword: { enabled: true }` - For email/password auth + +**Standard config adds:** +- `socialProviders` - OAuth providers (google, github, etc.) +- `emailVerification.sendVerificationEmail` - Email verification handler +- `emailAndPassword.sendResetPassword` - Password reset handler + +**Full config adds:** +- `plugins` - Array of feature plugins +- `session` - Expiry, cookie cache settings +- `account.accountLinking` - Multi-provider linking +- `rateLimit` - Rate limiting config + +**Export types:** `export type Session = typeof auth.$Infer.Session` + +--- + +## Client Config (auth-client.ts) + +**Import by framework:** +| Framework | Import | +|-----------|--------| +| React/Next.js | `better-auth/react` | +| Vue | `better-auth/vue` | +| Svelte | `better-auth/svelte` | +| Solid | `better-auth/solid` | +| Vanilla JS | `better-auth/client` | + +**Client plugins** go in `createAuthClient({ plugins: [...] })`. + +**Common exports:** `signIn`, `signUp`, `signOut`, `useSession`, `getSession` + +--- + +## Route Handler Setup + +| Framework | File | Handler | +|-----------|------|---------| +| Next.js App Router | `app/api/auth/[...all]/route.ts` | `toNextJsHandler(auth)` → export `{ GET, POST }` | +| Next.js Pages | `pages/api/auth/[...all].ts` | `toNextJsHandler(auth)` → default export | +| Express | Any file | `app.all("/api/auth/*", toNodeHandler(auth))` | +| SvelteKit | `src/hooks.server.ts` | `svelteKitHandler(auth)` | +| SolidStart | Route file | `solidStartHandler(auth)` | +| Hono | Route file | `auth.handler(c.req.raw)` | + +**Next.js Server Components:** Add `nextCookies()` plugin to auth config. + +--- + +## Database Migrations + +| Adapter | Command | +|---------|---------| +| Built-in Kysely | `npx @better-auth/cli@latest migrate` (applies directly) | +| Prisma | `npx @better-auth/cli@latest generate --output prisma/schema.prisma` then `npx prisma migrate dev` | +| Drizzle | `npx @better-auth/cli@latest generate --output src/db/auth-schema.ts` then `npx drizzle-kit push` | + +**Re-run after adding plugins.** + +--- + +## Database Adapters + +| Database | Setup | +|----------|-------| +| SQLite | Pass `better-sqlite3` or `bun:sqlite` instance directly | +| PostgreSQL | Pass `pg.Pool` instance directly | +| MySQL | Pass `mysql2` pool directly | +| Prisma | `prismaAdapter(prisma, { provider: "postgresql" })` from `better-auth/adapters/prisma` | +| Drizzle | `drizzleAdapter(db, { provider: "pg" })` from `better-auth/adapters/drizzle` | +| MongoDB | `mongodbAdapter(db)` from `better-auth/adapters/mongodb` | + +--- + +## Common Plugins + +| Plugin | Server Import | Client Import | Purpose | +|--------|---------------|---------------|---------| +| `twoFactor` | `better-auth/plugins` | `twoFactorClient` | 2FA with TOTP/OTP | +| `organization` | `better-auth/plugins` | `organizationClient` | Teams/orgs | +| `admin` | `better-auth/plugins` | `adminClient` | User management | +| `bearer` | `better-auth/plugins` | - | API token auth | +| `openAPI` | `better-auth/plugins` | - | API docs | +| `passkey` | `@better-auth/passkey` | `passkeyClient` | WebAuthn | +| `sso` | `@better-auth/sso` | - | Enterprise SSO | + +**Plugin pattern:** Server plugin + client plugin + run migrations. + +--- + +## Auth UI Implementation + +**Sign in flow:** +1. `signIn.email({ email, password })` or `signIn.social({ provider, callbackURL })` +2. Handle `error` in response +3. Redirect on success + +**Session check (client):** `useSession()` hook returns `{ data: session, isPending }` + +**Session check (server):** `auth.api.getSession({ headers: await headers() })` + +**Protected routes:** Check session, redirect to `/sign-in` if null. + +--- + +## Security Checklist + +- [ ] `BETTER_AUTH_SECRET` set (32+ chars) +- [ ] `advanced.useSecureCookies: true` in production +- [ ] `trustedOrigins` configured +- [ ] Rate limits enabled +- [ ] Email verification enabled +- [ ] Password reset implemented +- [ ] 2FA for sensitive apps +- [ ] CSRF protection NOT disabled +- [ ] `account.accountLinking` reviewed + +--- + +## Troubleshooting + +| Issue | Fix | +|-------|-----| +| "Secret not set" | Add `BETTER_AUTH_SECRET` env var | +| "Invalid Origin" | Add domain to `trustedOrigins` | +| Cookies not setting | Check `baseURL` matches domain; enable secure cookies in prod | +| OAuth callback errors | Verify redirect URIs in provider dashboard | +| Type errors after adding plugin | Re-run CLI generate/migrate | + +--- + +## Resources + +- [Docs](https://better-auth.com/docs) +- [Examples](https://github.com/better-auth/examples) +- [Plugins](https://better-auth.com/docs/concepts/plugins) +- [CLI](https://better-auth.com/docs/concepts/cli) +- [Migration Guides](https://better-auth.com/docs/guides) \ No newline at end of file diff --git a/.agents/skills/git-guidelines/SKILL.md b/.agents/skills/git-guidelines/SKILL.md new file mode 100644 index 00000000..fa763be5 --- /dev/null +++ b/.agents/skills/git-guidelines/SKILL.md @@ -0,0 +1,104 @@ +--- +name: git-guidelines +description: Follow Git branch and commit naming conventions. Use when creating branches, making commits, or when asked about Git conventions. +metadata: + version: "1.0.0" +--- + +# Git Guidelines + +Follow these conventions when naming branches and writing commit messages. + +## Branch Naming Convention + +### Category + +A Git branch should start with a **category**. Pick one of these: + +* `feature` — adding, refactoring, or removing a feature +* `bugfix` — fixing a bug +* `hotfix` — changing code with a temporary solution and/or without following the usual process (usually because of an emergency) +* `test` — experimenting outside of an issue/ticket + +### Reference + +After the category, add a `/` followed by the reference of the issue/ticket you are working on. + +If there's no reference, use `no-ref`. + +### Description + +After the reference, add another `/` followed by a short description that summarizes the purpose of the branch. + +Guidelines: + +* Use **kebab-case** +* Keep it short +* You can reuse the issue/ticket title +* Replace special characters with `-` + +### Pattern + +```bash +git branch +``` + +### Examples + +```bash +# Add, refactor, or remove a feature +git branch feature/issue-42/create-new-button-component + +# Fix a bug +git branch bugfix/issue-342/button-overlap-form-on-mobile + +# Emergency fix (possibly temporary) +git branch hotfix/no-ref/registration-form-not-working + +# Experiment outside of an issue/ticket +git branch test/no-ref/refactor-components-with-atomic-design +``` + +--- + +## Commit Naming Convention + +For commits, combine and simplify the **Angular Commit Message Guideline** and the **Conventional Commits** guideline. + +### Category + +A commit message should start with a **category of change**. These four are usually enough: + +* `feat` — adding a new feature +* `fix` — fixing a bug +* `refactor` — changing code for performance or convenience (e.g. readability) +* `chore` — everything else (documentation, formatting, tests, cleanup, etc.) + +After the category, add a `:` to announce the commit description. + +### Statement(s) + +After the colon: + +* Write short statements describing the changes +* Start each statement with an **imperative verb** +* Separate multiple statements with a `;` + +### Pattern + +```bash +git commit -m '' +``` + +### Examples + +```bash +git commit -m 'feat: add new button component; add new button components to templates' +git commit -m 'fix: add stop directive to button component to prevent propagation' +git commit -m 'refactor: rewrite button component in TypeScript' +git commit -m 'chore: write button documentation' +``` + +## Credits + +Based on: https://dev.to/varbsan/a-simplified-convention-for-naming-branches-and-commits-in-git-il4 diff --git a/.agents/skills/update/SKILL.md b/.agents/skills/update/SKILL.md new file mode 100644 index 00000000..c7c51bf5 --- /dev/null +++ b/.agents/skills/update/SKILL.md @@ -0,0 +1,94 @@ +--- +name: update +description: Update assistant-ui and AI SDK to latest versions. Detects current versions, identifies breaking changes, and executes migrations. +version: 0.0.1 +license: MIT +--- + +# assistant-ui Update + +**Always verifies against npm ground truth and GitHub commits.** + +## References + +- [./references/ai-sdk-v6.md](./references/ai-sdk-v6.md) -- AI SDK v4/v5 → v6 migration (complete guide) +- [./references/assistant-ui.md](./references/assistant-ui.md) -- assistant-ui version migrations +- [./references/breaking-changes.md](./references/breaking-changes.md) -- Quick reference table + +## Phase 1: Detect Versions + +### Get Ground Truth + +```bash +# Installed versions +npm ls @assistant-ui/react @assistant-ui/react-ai-sdk ai @ai-sdk/react 2>/dev/null + +# Latest from npm +npm view @assistant-ui/react version +npm view @assistant-ui/react-ai-sdk version +npm view ai version +``` + +### Version Analysis + +| Package | Check For | +|---------|-----------| +| `ai` | < 6.0.0 → needs AI SDK v6 migration | +| `@assistant-ui/react` | < 0.11.0 → needs runtime migration | +| `@assistant-ui/react` | < 0.10.0 → needs ESM migration | +| `@assistant-ui/react` | < 0.8.0 → needs UI split migration | +| `@assistant-ui/react-ai-sdk` | < 1.0.0 → needs AI SDK v6 first | + +## Phase 2: Route to Migration + +``` +AI SDK < 6.0.0? +├─ Yes → See ./references/ai-sdk-v6.md +└─ No + └─ assistant-ui outdated? + ├─ Yes → See ./references/assistant-ui.md + └─ No → Already up to date +``` + +### Migration Order + +1. **AI SDK first** (if < 6.0.0) - Required for @assistant-ui/react-ai-sdk >= 1.0 +2. **assistant-ui second** - Apply breaking changes for version jump +3. **Verify** - Type check, build, test + +## Phase 3: Execute + +### Update Packages + +```bash +# pnpm +pnpm add @assistant-ui/react@latest @assistant-ui/react-ai-sdk@latest ai@latest @ai-sdk/react@latest + +# npm +npm install @assistant-ui/react@latest @assistant-ui/react-ai-sdk@latest ai@latest @ai-sdk/react@latest +``` + +### Apply Migrations + +Based on version jump, apply relevant migrations from references. + +### Verify + +```bash +npx tsc --noEmit # Type check +pnpm build # Build check +``` + +## Troubleshooting + +**"Peer dependency conflict"** +- Update all packages together +- Check version compatibility in [./references/breaking-changes.md](./references/breaking-changes.md) + +**Type errors after upgrade** +- Consult breaking changes reference +- Check specific migration guide + +**Runtime errors** +- Verify API patterns match new version +- Check for renamed/moved APIs diff --git a/.agents/skills/update/references/ai-sdk-v6.md b/.agents/skills/update/references/ai-sdk-v6.md new file mode 100644 index 00000000..46dc32d8 --- /dev/null +++ b/.agents/skills/update/references/ai-sdk-v6.md @@ -0,0 +1,1928 @@ +# AI SDK v6 Migration + +Migrate a codebase from AI SDK v4 or v5 to v6. This is a methodical, careful process using agents. Do not rush. Verify everything. + +**Covers:** v4.x → v6.x, v5.x → v6.x + +**Official docs:** https://ai-sdk.dev/docs/migration-guides/migration-guide-6-0 + +--- + +## Critical Rules + +1. **NEVER make changes without reading files first** - Always read the full file before editing +2. **NEVER guess** - If unsure, search the codebase or ask the user +3. **ALWAYS verify after changes** - Run type check after each file modification +4. **USE AGENTS for research** - Spawn Explore agents for thorough codebase analysis +5. **TRACK EVERYTHING** - Use TodoWrite to track every file and change +6. **ONE CHANGE AT A TIME** - Make atomic changes, verify, then proceed +7. **REFERENCE THE GUIDE** - The complete migration guide is embedded below - consult it for every change + +--- + +## Phase 1: Deep Research (Use Agents) + +**STOP. Do not skip this phase. Thorough research prevents mistakes.** + +### 1.1 Spawn Research Agent for AI SDK Patterns + +Use Task tool with `subagent_type: "Explore"` and `model: "opus"`: + +``` +Thoroughly search this codebase for ALL AI SDK usage. Find EVERY instance of: + +IMPORTS TO FIND: +- import from "ai" +- import from "@ai-sdk/*" +- import from "@assistant-ui/react-ai-sdk" + +PATTERNS TO FIND: +- useChat hook usage +- streamText / generateText calls +- generateObject / streamObject calls +- convertToCoreMessages calls +- CoreMessage / Message types +- maxSteps configuration +- tool definitions (look for parameters:, execute:) +- addToolResult calls +- textEmbedding / textEmbeddingModel +- Experimental_Agent +- toDataStreamResponse + +For EACH finding, report: +- Exact file path +- Line numbers +- The actual code snippet +- What v6 change applies to it + +Be exhaustive. Missing something causes migration failures. +``` + +### 1.2 Spawn Research Agent for Package Analysis + +Use Task tool with `subagent_type: "Explore"`: + +``` +Find and analyze all package.json files in this repository. + +For each package.json, extract: +1. Current versions of: ai, @ai-sdk/*, zod, @assistant-ui/* +2. The package manager (look for pnpm-lock.yaml, yarn.lock, package-lock.json) +3. Test scripts (test, test:watch, etc.) +4. Build scripts + +Report the exact current versions vs required v6 versions. +``` + +### 1.3 Spawn Research Agent for Test Infrastructure + +Use Task tool with `subagent_type: "Explore"`: + +``` +Find the test infrastructure in this codebase: + +1. Test framework (vitest, jest, etc.) +2. Test file locations and patterns +3. Any AI SDK test mocks (MockLanguageModelV2, etc.) +4. The exact command to run tests +5. Any test configuration files +``` + +### 1.4 Compile Research Results + +After ALL agents complete, create a comprehensive findings document: + +- Total files requiring changes +- Categorized list of all patterns found +- Package versions needing update +- Test command to use +- Any unusual patterns or edge cases + +**CHECKPOINT: Present findings to user. Ask if anything was missed. Do not proceed until confirmed.** + +--- + +## Phase 2: Create Detailed Migration Plan + +Based on Phase 1 findings and the migration guide below, create a file-by-file plan. + +### 2.1 Categorize Changes + +Group findings into categories: + +**Category A: Codemod-handled** (automatic) +- CoreMessage → ModelMessage +- convertToCoreMessages → convertToModelMessages +- textEmbedding/textEmbeddingModel → embedding/embeddingModel (on providers) +- ToolCallOptions → ToolExecutionOptions + +**Category B: Manual - Simple renames** +- Message → UIMessage +- maxSteps → stopWhen: stepCountIs(n) + +**Category C: Manual - Structural changes** +- Adding await to convertToModelMessages +- toDataStreamResponse → toUIMessageStreamResponse +- generateObject → generateText + Output.object +- Tool definition restructuring +- useChat hook changes + +**Category D: Manual - Complex logic** +- Message parts array handling +- Custom stream implementations +- Tool result handling changes + +### 2.2 Create File-by-File Plan + +For EACH file that needs changes, document: + +``` +FILE: path/to/file.ts +CHANGES NEEDED: + 1. Line X: [old] → [new] (Category: X) + 2. Line Y: [old] → [new] (Category: X) +IMPORTS TO ADD: [list] +IMPORTS TO REMOVE: [list] +VERIFICATION: What to check after editing +``` + +### 2.3 Determine Execution Order + +Order matters. Follow this sequence: + +1. Package updates (package.json) +2. Run codemods +3. Type definition files +4. Utility/helper files +5. API routes +6. React components +7. Test files + +**CHECKPOINT: Present full plan to user for approval. Do not proceed without explicit approval.** + +--- + +## Phase 3: Execute Migration + +**Only proceed after user approves the plan.** + +### 3.1 Update Packages + +Detect package manager and run appropriate command: + +```bash +# For pnpm +pnpm add ai@latest @ai-sdk/react@latest @ai-sdk/openai@latest zod@latest @assistant-ui/react@latest @assistant-ui/react-ai-sdk@latest + +# For npm +npm install ai@latest @ai-sdk/react@latest @ai-sdk/openai@latest zod@latest + +# For yarn +yarn add ai@latest @ai-sdk/react@latest @ai-sdk/openai@latest zod@latest +``` + +**VERIFY:** Check package.json shows correct versions before continuing. + +### 3.2 Run Codemods + +```bash +npx @ai-sdk/codemod upgrade +``` + +This is the recommended approach - it detects your current version and applies all necessary codemods (v4→v5→v6) automatically. + +**VERIFY:** +- Review codemod output +- Run `git diff` to see what changed +- Check for any errors or warnings + +### 3.3 Apply Manual Changes + +**For EACH file in the plan:** + +1. Add to todo list as "in_progress" +2. Read the ENTIRE file first +3. Make changes ONE AT A TIME +4. After each change, verify syntax is valid +5. After all changes to file, run type check +6. Mark as "completed" only after type check passes + +**IMPORTANT PATTERNS FROM GUIDE:** + +API Route changes: +```typescript +// OLD +const result = streamText({ model, messages, maxSteps: 10 }); +return (await result).toDataStreamResponse(); + +// NEW +import { stepCountIs } from "ai"; +const result = streamText({ + model, + messages: await convertToModelMessages(messages), + stopWhen: stepCountIs(10) +}); +return result.toUIMessageStreamResponse(); +``` + +Tool definitions: +```typescript +// OLD +tools: { + myTool: { + description: "...", + parameters: z.object({ ... }), + execute: async (args) => { ... } + } +} + +// NEW +import { tool, zodSchema } from "ai"; +tools: { + myTool: tool({ + description: "...", + inputSchema: zodSchema(z.object({ ... })), + execute: async (args, options) => { ... } + }) +} +``` + +### 3.4 Type Check After Each File + +```bash +npx tsc --noEmit +# or +pnpm type-check +# or whatever the project uses +``` + +If errors found: +1. Read the error carefully +2. Consult the migration guide below +3. Fix the specific error +4. Re-run type check +5. Repeat until clean + +--- + +## Phase 4: Build Verification + +### 4.1 Full Type Check + +Run full TypeScript compilation: + +```bash +npx tsc --noEmit +``` + +### 4.2 Fix All Type Errors + +For each error: +1. Add to todo list +2. Read the file and surrounding context +3. Identify which v6 change applies +4. Apply the fix from the guide +5. Verify the fix +6. Mark complete + +**Common type errors and fixes:** + +- `Property 'content' does not exist on type 'UIMessage'` → Use `message.parts` array +- `Type 'CoreMessage' not found` → Change to `ModelMessage` +- `maxSteps does not exist` → Use `stopWhen: stepCountIs(n)` +- `toDataStreamResponse not found` → Use `toUIMessageStreamResponse()` + +### 4.3 Build Check + +```bash +pnpm build +# or +npm run build +``` + +Fix any build errors before proceeding. + +--- + +## Phase 5: Test Verification + +### 5.1 Run Test Suite + +```bash +pnpm test +# or +npm test +``` + +### 5.2 Fix Failing Tests + +For EACH failing test: + +1. Read the test file +2. Read the error message carefully +3. Determine if it's a: + - Test mock issue (V2 → V3) + - Assertion issue (message structure changed) + - Implementation issue (missed migration step) +4. Apply appropriate fix +5. Re-run that specific test +6. Verify it passes +7. Move to next failing test + +### 5.3 Full Test Pass + +Run complete test suite again. All tests must pass. + +--- + +## Phase 6: Final Verification + +### 6.1 Manual Testing Checklist + +Ask user to verify: + +- [ ] Dev server starts without errors +- [ ] Chat messages send successfully +- [ ] Streaming responses work +- [ ] Tool calls execute correctly +- [ ] Tool results display properly +- [ ] No console errors + +### 6.2 Cleanup + +- Remove any TODO comments added during migration +- Remove unused imports +- Run linter/formatter + +--- + +## Rollback Plan + +If migration fails catastrophically: + +```bash +git checkout . +git clean -fd +``` + +Then re-analyze what went wrong before retrying. + +--- + +# COMPLETE MIGRATION GUIDE REFERENCE + +**Consult this for EVERY change. Do not guess.** + +## Package Updates + +### Required Package Versions + +```json +{ + "ai": "^6.0.0", + "@ai-sdk/react": "^3.0.0", + "@ai-sdk/provider": "^3.0.0", + "@ai-sdk/provider-utils": "^4.0.0", + "@assistant-ui/react": "^0.11.58", + "@assistant-ui/react-ai-sdk": "^1.2.0" +} +``` + +### Provider Packages + +All `@ai-sdk/*` provider packages should be updated to `^3.0.0`: + +```json +{ + "@ai-sdk/openai": "^3.0.0", + "@ai-sdk/anthropic": "^3.0.0", + "@ai-sdk/google": "^3.0.0", + "@ai-sdk/mistral": "^3.0.0" +} +``` + +### MCP Package (if using MCP) + +MCP has been moved to a separate package: + +```json +{ + "@ai-sdk/mcp": "^1.0.0" +} +``` + +### Zod Support + +AI SDK v6 supports both Zod 3.25+ and Zod 4.x: + +```json +{ + "zod": "^3.25.76 || ^4.1.8" +} +``` + +--- + +## Automated Migration + +The AI SDK provides codemods to automate many migration tasks: + +```bash +# From v4: Run ALL codemods (v4 → v5 → v6) +npx @ai-sdk/codemod upgrade + +# From v5: Run v6 codemods only (v5 → v6) +npx @ai-sdk/codemod v6 + +# Run specific codemods +npx @ai-sdk/codemod v6/rename-core-message-to-model-message src/ +npx @ai-sdk/codemod v6/add-await-converttomodelmessages src/ +npx @ai-sdk/codemod v5/move-maxsteps-to-stopwhen src/ +``` + +**Which command to use:** +- `upgrade` - Recommended for v4 projects. Runs all v4, v5, and v6 codemods. +- `v6` - For v5 projects. Runs only v6 codemods. +- `v5` - For v4 projects wanting incremental migration. Runs only v5 codemods. +- `v4` - For v3 projects. Runs only v4 codemods. + +### Available v6 Codemods (v5 → v6) + +| Codemod | Description | +|---------|-------------| +| `v6/add-await-converttomodelmessages` | Adds `await` to `convertToModelMessages()` calls | +| `v6/rename-converttocoremessages-to-converttomodelmessages` | Updates the conversion function name | +| `v6/rename-core-message-to-model-message` | Renames `CoreMessage` → `ModelMessage` | +| `v6/rename-mock-v2-to-v3` | Updates test mock classes from V2 to V3 | +| `v6/rename-text-embedding-to-embedding` | Renames `textEmbeddingModel` → `embeddingModel` on providers | +| `v6/rename-tool-call-options-to-tool-execution-options` | Renames `ToolCallOptions` → `ToolExecutionOptions` | +| `v6/rename-vertex-provider-metadata-key` | Updates `google` → `vertex` for metadata keys | + +### Key v5 Codemods (v4 → v5, needed for v4 → v6) + +| Codemod | Description | +|---------|-------------| +| `v5/move-maxsteps-to-stopwhen` | Moves `maxSteps` to `stopWhen: stepCountIs(n)` | +| `v5/rename-max-tokens-to-max-output-tokens` | Renames `maxTokens` → `maxOutputTokens` | +| `v5/rename-tool-parameters-to-inputschema` | Renames tool `parameters` → `inputSchema` | +| `v5/replace-usechat-api-with-transport` | Replaces `useChat({ api })` with transport | +| `v5/replace-usechat-input-with-state` | Removes managed input state from useChat | +| `v5/replace-content-with-parts` | Replaces `message.content` with `message.parts` | +| `v5/rename-message-to-ui-message` | Renames `Message` → `UIMessage` | +| `v5/rename-datastream-methods-to-uimessage` | Renames stream methods to UI message variants | + +**Note:** Review all automated changes manually, especially around async/await additions. + +--- + +## Core Breaking Changes + +### 1. Message Type Changes + +`CoreMessage` has been replaced with `ModelMessage`: + +```diff +- import { CoreMessage, convertToCoreMessages } from "ai"; ++ import { ModelMessage, convertToModelMessages } from "ai"; +``` + +`Message` has been replaced with `UIMessage`: + +```diff +- import type { Message } from "ai"; ++ import type { UIMessage } from "ai"; +``` + +### 2. `convertToModelMessages` is Now Async + +This is a critical change that affects all API routes: + +```diff +// Before (v5) +- const modelMessages = convertToCoreMessages(messages); + +// After (v6) - MUST use await ++ const modelMessages = await convertToModelMessages(messages); +``` + +### 3. `maxSteps` Replaced with `stopWhen` + +```diff ++ import { stepCountIs } from "ai"; + +const result = streamText({ + model: openai("gpt-4o"), + messages: modelMessages, +- maxSteps: 10, ++ stopWhen: stepCountIs(10), +}); +``` + +### 4. Agent Class Changes + +`Experimental_Agent` has been replaced with `ToolLoopAgent`: + +```diff +- import { Experimental_Agent } from "ai"; ++ import { ToolLoopAgent } from "ai"; + +const agent = new ToolLoopAgent({ +- system: "You are a helpful assistant", ++ instructions: "You are a helpful assistant", + // Note: Default stopWhen changed from stepCountIs(1) to stepCountIs(20) +}); +``` + +### 4.1 Agent Stream Response Renamed + +```diff +- import { createAgentStreamResponse } from "ai"; ++ import { createAgentUIStreamResponse } from "ai"; + +- return createAgentStreamResponse({ ... }); ++ return createAgentUIStreamResponse({ ... }); +``` + +The `messages` property in the result has been renamed to `uiMessages`: + +```diff +- const { messages } = await createAgentUIStreamResponse({ ... }); ++ const { uiMessages } = await createAgentUIStreamResponse({ ... }); +``` + +### 5. Tool Call Options Renamed + +```diff +- import type { ToolCallOptions } from "ai"; ++ import type { ToolExecutionOptions } from "ai"; +``` + +### 6. Embedding Method Renames + +Provider embedding methods were renamed: + +```diff +- const model = openai.textEmbedding("text-embedding-3-small"); ++ const model = openai.embedding("text-embedding-3-small"); + +// Alternative (also renamed): +- const model = openai.textEmbeddingModel("text-embedding-3-small"); ++ const model = openai.embeddingModel("text-embedding-3-small"); +``` + +**Note:** The core `embed()` and `embedMany()` functions from the "ai" package remain unchanged. Only the provider methods were renamed. + +### 7. MCP Imports Moved to Separate Package + +If you're using MCP (Model Context Protocol), imports have moved from `ai` to `@ai-sdk/mcp`: + +```diff +- import { experimental_createMCPClient } from "ai"; +- import { Experimental_StdioMCPTransport } from "ai/mcp-stdio"; ++ import { experimental_createMCPClient } from "@ai-sdk/mcp"; ++ import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio"; +``` + +**Note:** Install the new package: `pnpm add @ai-sdk/mcp` + +### 8. Warning Type Unification + +Separate warning types consolidated into a single `Warning` type: + +```diff +- import type { GenerateTextWarning, StreamTextWarning, CallWarning } from "ai"; ++ import type { Warning } from "ai"; +``` + +### 9. Finish Reason Change + +The "unknown" finish reason now returns as "other": + +```diff +- if (result.finishReason === "unknown") { } ++ if (result.finishReason === "other") { } +``` + +### 10. Tool UI Helper Renames + +The naming changed to distinguish static vs dynamic tools: + +```diff +// For static tools only: +- import { isToolUIPart, getToolName } from "ai"; ++ import { isStaticToolUIPart, getStaticToolName } from "ai"; + +// For both static and dynamic tools (the new default): +- import { isToolOrDynamicToolUIPart, getToolOrDynamicToolName } from "ai"; ++ import { isToolUIPart, getToolName } from "ai"; +``` + +### 11. Tool.toModelOutput Signature Change + +```diff +const myTool = tool({ + // ... + // Before +- toModelOutput: (output) => processOutput(output), + + // After - requires object destructuring ++ toModelOutput: ({ output }) => processOutput(output), +}); +``` + +### 12. ToolCallRepairFunction Change + +The `system` parameter now accepts different types: + +```diff +// system parameter type changed +- system: string | undefined ++ system: string | SystemModelMessage | undefined + +// Handle both types: +const repair: ToolCallRepairFunction = async ({ system }) => { + const systemText = typeof system === 'string' ? system : system?.content; +}; +``` + +### 13. Token Usage Property Changes + +```diff +// Cached input tokens +- result.usage.cachedInputTokens ++ result.usage.inputTokenDetails.cacheReadTokens + +// Reasoning tokens +- result.usage.reasoningTokens ++ result.usage.outputTokenDetails.reasoningTokens +``` + +### 14. Rerank Score Property Renamed + +```diff +// For reranking results +- result.relevanceScore ++ result.score +``` + +--- + +## UI & React Changes + +### 1. UIMessage Structure + +The fundamental message format changed from a single `content` string to a `parts` array: + +```typescript +// Old structure (v5) +interface Message { + id: string; + role: "user" | "assistant"; + content: string; +} + +// New structure (v6) +interface UIMessage { + id: string; + role: "user" | "assistant" | "system"; + parts: MessagePart[]; + metadata?: Record; +} +``` + +### 2. Message Part Types + +The `parts` array supports multiple content types: + +```typescript +type MessagePart = + | { type: "text"; text: string } + | { type: "file"; file: FileInfo } + | { type: "reasoning"; text: string } + | { type: "tool-invocation"; toolInvocation: ToolInvocation } + | { type: "source-url"; sourceId: string; url: string; title?: string } + | { type: "source-document"; sourceId: string; ... } + | { type: `data-${string}`; data: unknown }; // Custom data parts +``` + +### 3. Reading Text from Messages + +```typescript +// Extract text content from UIMessage +const extractText = (messages: UIMessage[]): string => { + return messages + .map((m) => + m.parts + .filter((p): p is { type: "text"; text: string } => p.type === "text") + .map((p) => p.text) + .join(" ") + ) + .join("\n"); +}; +``` + +### 4. useChat Hook Changes + +The `useChat` hook underwent significant restructuring in v6. + +#### Input State Management + +Input is **no longer managed internally** by the hook: + +```diff +// Before (v5) +- const { input, setInput, handleSubmit } = useChat(); +- setInput(e.target.value)} /> + +// After (v6) - manage input state yourself ++ const [input, setInput] = useState(""); ++ const { sendMessage } = useChat(); ++ ++ const handleSubmit = () => { ++ sendMessage(input); ++ setInput(""); ++ }; +``` + +#### Message Sending + +`append()` replaced with `sendMessage()`: + +```diff +// Before (v5) +- append({ role: "user", content: "Hello" }); + +// After (v6) - multiple valid formats: + +// Option 1: Simple string ++ sendMessage("Hello"); + +// Option 2: Object with text ++ sendMessage({ text: "Hello" }); + +// Option 3: Object with parts array ++ sendMessage({ ++ parts: [{ type: "text", text: "Hello" }] ++ }); + +// With options (headers, body, metadata) ++ sendMessage("Hello", { metadata: { key: "value" } }); +``` + +#### Tool Result Handling + +AI SDK v6 provides two methods for submitting tool results: + +```typescript +// addToolResult - Simple form (without explicit state) +addToolResult({ + tool: "toolName", + toolCallId, + output: result, +}); + +// addToolOutput - With explicit state (for success or error) +addToolOutput({ + state: "output-available", + tool: "toolName", + toolCallId, + output: result, +}); + +// For errors, use addToolOutput with error state: +addToolOutput({ + state: "output-error", + tool: "toolName", + toolCallId, + errorText: "Error message", +}); +``` + +**Note:** Both `addToolResult` and `addToolOutput` are available. Use `addToolOutput` when you need to explicitly set the state (especially for errors). + +### 5. Status States + +The hook returns a `status` field with four possible values: + +```typescript +type ChatStatus = "submitted" | "streaming" | "ready" | "error"; + +const { status } = useChat(); + +// submitted: Message sent, awaiting response stream start +// streaming: Response actively receiving data chunks +// ready: Response complete, ready for new messages +// error: Request failed +``` + +### 6. Transport Configuration + +The transport-based architecture replaces the old `api` option: + +```typescript +import { useChat } from "@ai-sdk/react"; +import { DefaultChatTransport } from "ai"; + +// Before (v5) +const { messages } = useChat({ + api: "/api/chat", +}); + +// After (v6) +const { messages } = useChat({ + transport: new DefaultChatTransport({ + api: "/api/chat", + headers: { /* ... */ }, + body: { /* ... */ }, + credentials: "include", + }), +}); +``` + +### 7. Message Validation for Persistence + +When loading messages from storage that contain tools or custom data, validate them: + +```typescript +import { validateUIMessages } from "ai"; + +// Before using stored messages +const validatedMessages = await validateUIMessages({ + messages: storedMessages, + tools: yourTools, +}); +``` + +--- + +## Tool System Changes + +**Important:** AI SDK and assistant-ui use different property names for tool schemas: +- **AI SDK `tool()` helper** (backend): uses `inputSchema` +- **assistant-ui `useAssistantTool`** (frontend): uses `parameters` + +This distinction matters when defining tools in different contexts. + +### 1. Tool Definition with `tool()` Helper + +The `tool()` helper provides type inference between schema and execute function: + +```typescript +import { tool } from "ai"; +import { z } from "zod"; + +const weatherTool = tool({ + description: "Get weather for a location", + + // inputSchema accepts Zod schemas directly + inputSchema: z.object({ + location: z.string().describe("The location to get weather for"), + unit: z.enum(["celsius", "fahrenheit"]).optional(), + }), + + execute: async ({ location, unit }, options) => { + // options includes: toolCallId, messages, abortSignal + return { temperature: 72, unit: unit ?? "fahrenheit" }; + }, + + // Optional: Enable strict mode for providers that support it + strict: true, +}); +``` + +### 2. Schema Options + +You can use Zod schemas directly (auto-converted) or wrap them with helpers: + +```typescript +import { tool, zodSchema, jsonSchema } from "ai"; +import { z } from "zod"; + +// Option 1: Direct Zod (auto-converted to JSON Schema) +const tool1 = tool({ + inputSchema: z.object({ query: z.string() }), + execute: async ({ query }) => { /* ... */ }, +}); + +// Option 2: zodSchema() wrapper (explicit, recommended for clarity) +// This is what the assistant-ui examples use +const tool2 = tool({ + inputSchema: zodSchema( + z.object({ query: z.string() }), + ), + execute: async ({ query }) => { /* ... */ }, +}); + +// Option 3: zodSchema() with options (for recursive schemas) +const tool3 = tool({ + inputSchema: zodSchema( + z.object({ category: categorySchema }), + { useReferences: true } // Enables recursive schema support + ), + execute: async ({ category }) => { /* ... */ }, +}); + +// Option 4: jsonSchema() for JSON Schema objects +const tool4 = tool({ + inputSchema: jsonSchema<{ query: string }>({ + type: "object", + properties: { query: { type: "string" } }, + required: ["query"], + }), + execute: async ({ query }) => { /* ... */ }, +}); +``` + +**Note:** When using `.describe()` or `.meta()` on Zod schemas, these methods must be called **last** in the chain, as Zod returns new instances for most operations. + +### 3. Per-Tool Strict Mode + +Strict JSON schema validation moved from provider options to individual tools: + +```diff +const result = streamText({ + model: openai("gpt-4o"), +- providerOptions: { +- openai: { strictJsonSchema: true }, +- }, + tools: { + myTool: tool({ + inputSchema: schema, ++ strict: true, // Per-tool strict mode + execute: async (input) => { /* ... */ }, + }), + }, +}); +``` + +### 4. Tool States + +Tool invocations now have explicit states: + +```typescript +type ToolInvocationState = + | "input-streaming" // Arguments being streamed + | "input-available" // Arguments complete, not yet executed + | "output-available" // Execution complete with result + | "output-error"; // Execution failed + +// Access in message parts: +message.parts.forEach(part => { + if (isToolUIPart(part)) { + console.log(part.state); // One of the above states + console.log(part.toolCallId); // Unique ID + console.log(part.input); // Tool arguments + console.log(part.output); // Result (if output-available) + console.log(part.errorText); // Error (if output-error) + } +}); +``` + +### 5. Tool Input Lifecycle Hooks + +Tools now support streaming callbacks: + +```typescript +const myTool = tool({ + inputSchema: z.object({ query: z.string() }), + execute: async ({ query }) => { /* ... */ }, + + // Called when model starts generating arguments + onInputStart: ({ toolCallId }) => { + console.log("Tool input started:", toolCallId); + }, + + // Called for each input chunk (streamText only) + onInputDelta: ({ toolCallId, delta }) => { + console.log("Input delta:", delta); + }, + + // Called when complete, validated input is available + onInputAvailable: ({ toolCallId, input }) => { + console.log("Input ready:", input); + }, +}); +``` + +### 6. Tool Execution Approval + +Tools can require user confirmation: + +```typescript +// Server: Mark tool as needing approval +const dangerousTool = tool({ + description: "Deletes a file", + inputSchema: z.object({ path: z.string() }), + needsApproval: true, // Requires client approval + execute: async ({ path }) => { /* ... */ }, +}); + +// Client: Handle approval +const { addToolApprovalResponse } = useChat(); + +// User approves +addToolApprovalResponse({ toolCallId, approved: true }); + +// User denies +addToolApprovalResponse({ toolCallId, approved: false }); +``` + +### 7. Frontend Tools Helper (assistant-ui) + +When forwarding tools defined in the frontend to your backend: + +```typescript +import { frontendTools } from "@assistant-ui/react-ai-sdk"; + +// In API route +export async function POST(req: Request) { + const { messages, tools } = await req.json(); + + const result = streamText({ + model: openai("gpt-4o"), + messages: await convertToModelMessages(messages), + tools: { + // Wrap frontend tools + ...frontendTools(tools), + // Add backend-only tools + myBackendTool: tool({ /* ... */ }), + }, + }); + + return result.toUIMessageStreamResponse(); +} +``` + +--- + +## Streaming Architecture + +### 1. Stream Response Methods + +```diff +// Before (v5) - result was a promise +- return (await result).toDataStreamResponse(); + +// After (v6) - result is not a promise ++ return result.toDataStreamResponse(); + +// For UI message streams (recommended for assistant-ui): ++ return result.toUIMessageStreamResponse(); +``` + +### 2. toUIMessageStreamResponse Options + +```typescript +return result.toUIMessageStreamResponse({ + // Include reasoning tokens (for models that support it) + sendReasoning: true, + + // Include source citations (for RAG models) + sendSources: true, + + // Custom ID generator for messages + generateMessageId: () => crypto.randomUUID(), + + // Attach metadata to message parts + messageMetadata: { timestamp: Date.now() }, + + // Customize error messages sent to client + getErrorMessage: (error) => `Error: ${error.message}`, + + // Handle completion (good for persistence) + onFinish: ({ messages, responseMessage }) => { + // Save to database + }, +}); +``` + +### 3. Stream Protocol Changes + +The protocol evolved to lifecycle events with three-phase patterns: + +```typescript +// Text streaming uses start/delta/end with unique IDs +{ type: "text-start", id: "text-1" } +{ type: "text-delta", id: "text-1", delta: "Hello" } +{ type: "text-delta", id: "text-1", delta: " world" } +{ type: "text-end", id: "text-1" } + +// Tool inputs stream progressively +{ type: "tool-input-start", toolCallId: "call-1" } +{ type: "tool-input-delta", toolCallId: "call-1", delta: '{"loc' } +{ type: "tool-input-end", toolCallId: "call-1" } +``` + +### 4. Custom Stream Headers + +When providing streams from a custom backend, set the required header: + +```typescript +return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "x-vercel-ai-ui-message-stream": "v1", + }, +}); +``` + +### 5. Creating Custom UI Message Streams + +```typescript +import { createUIMessageStream, createUIMessageStreamResponse } from "ai"; + +const stream = createUIMessageStream({ + execute: async ({ writer }) => { + // Write text manually + writer.write({ type: "text-start", id: "text-1" }); + writer.write({ type: "text-delta", id: "text-1", delta: "Hello" }); + writer.write({ type: "text-end", id: "text-1" }); + + // Write custom data (persistent - saved in message.parts) + writer.write({ + type: "data-weather", + id: "weather-1", + data: { city: "NYC", temp: 72 }, + }); + + // Merge another stream + const result = streamText({ model, messages }); + writer.merge(result.toUIMessageStream()); + }, + onFinish: ({ messages, responseMessage }) => { + // Handle completion + }, +}); + +return createUIMessageStreamResponse({ stream }); +``` + +### 6. Custom Data Parts + +#### Defining Type-Safe Data Parts + +```typescript +// Define your message type with data parts +export type MyUIMessage = UIMessage< + never, + { + weather: { city: string; temp: number; status: "loading" | "ready" }; + notification: { message: string; level: "info" | "warning" | "error" }; + } +>; +``` + +#### Server: Sending Data Parts + +```typescript +const stream = createUIMessageStream({ + execute: ({ writer }) => { + // Persistent data part (appears in message.parts) + writer.write({ + type: "data-weather", + id: "weather-1", + data: { city: "NYC", temp: 72, status: "ready" }, + }); + + // Update same part by using same ID + writer.write({ + type: "data-weather", + id: "weather-1", + data: { city: "NYC", temp: 75, status: "ready" }, + }); + }, +}); +``` + +#### Client: Reading Data Parts + +```typescript +// Persistent parts in message.parts +const weatherData = message.parts + .filter((part) => part.type === "data-weather") + .map((part) => part.data); + +// Transient parts via onData callback (not saved in message history) +const { messages } = useChat({ + onData: (dataPart) => { + if (dataPart.type === "data-notification") { + showToast(dataPart.data.message); + } + }, +}); +``` + +--- + +## Structured Output Changes + +### generateObject and streamObject Deprecated + +Use `generateText` and `streamText` with the `Output` helper instead: + +```diff +// Before (v5) +- import { generateObject } from "ai"; +- const { object } = await generateObject({ +- model: openai("gpt-4o"), +- schema: z.object({ name: z.string() }), +- prompt: "Generate a name", +- }); + +// After (v6) ++ import { generateText, Output } from "ai"; ++ const { output } = await generateText({ ++ model: openai("gpt-4o"), ++ output: Output.object({ ++ schema: z.object({ name: z.string() }), ++ }), ++ prompt: "Generate a name", ++ }); +``` + +### Output Types + +```typescript +import { generateText, streamText, Output } from "ai"; + +// Single object +const { output } = await generateText({ + model, + output: Output.object({ + schema: z.object({ name: z.string(), age: z.number() }), + name: "person", // Optional: helps model understand context + description: "...", // Optional: additional guidance + }), + prompt: "Generate a person", +}); + +// Array of objects +const { output } = await generateText({ + model, + output: Output.array({ + schema: z.object({ name: z.string() }), + }), + prompt: "Generate 5 names", +}); + +// Choice from options +const { output } = await generateText({ + model, + output: Output.choice({ + options: ["positive", "negative", "neutral"], + }), + prompt: "Classify the sentiment", +}); + +// Plain JSON (no validation) +const { output } = await generateText({ + model, + output: Output.json(), + prompt: "Generate JSON data", +}); +``` + +### Streaming Structured Output + +```diff +// Before (v5) +- const { partialObjectStream } = streamObject({ ... }); +- for await (const partial of partialObjectStream) { } + +// After (v6) ++ const result = streamText({ ++ model, ++ output: Output.object({ schema }), ++ prompt: "...", ++ }); ++ for await (const partial of result.partialOutputStream) { } +``` + +For arrays, use `elementStream` to get complete elements: + +```typescript +const result = streamText({ + model, + output: Output.array({ schema }), + prompt: "Generate items", +}); + +// Each element is complete and validated +for await (const element of result.elementStream) { + console.log(element); // Fully validated element +} +``` + +--- + +## Provider-Specific Changes + +### OpenAI + +- `strictJsonSchema` now defaults to `true` (was `false`) +- Disable if needed: + +```typescript +const result = await generateText({ + model: openai("gpt-4o"), + providerOptions: { + openai: { strictJsonSchema: false }, + }, + // ... +}); +``` + +### Azure OpenAI + +- Default behavior switches to Responses API +- Use `azure.chat()` for previous Chat Completions API behavior +- Metadata key changed: `openai` → `azure` + +```diff +// For Responses API (new default) +const model = azure("gpt-4o"); + +// For Chat Completions API (previous behavior) +const model = azure.chat("gpt-4o"); + +// Metadata access +- result.experimental_providerMetadata?.openai ++ result.experimental_providerMetadata?.azure + +// Provider options +- providerOptions: { openai: { ... } } ++ providerOptions: { azure: { ... } } +``` + +### Anthropic + +New `structuredOutputMode` option for Claude Sonnet 4.5+: + +```typescript +const result = await generateText({ + model: anthropic("claude-sonnet-4-5-20250929"), + output: Output.object({ schema }), + providerOptions: { + anthropic: { + // Options: 'outputFormat', 'jsonTool', or 'auto' (default) + structuredOutputMode: "outputFormat", + }, + }, +}); +``` + +### Google Vertex + +Metadata and options key changed: + +```diff +- providerOptions: { google: { safetySettings: [...] } } ++ providerOptions: { vertex: { safetySettings: [...] } } + +- result.experimental_providerMetadata?.google ++ result.experimental_providerMetadata?.vertex +``` + +--- + +## assistant-ui Specific Changes + +### 1. Simplified Client Setup + +The simplest case works with zero configuration: + +```typescript +"use client"; + +import { AssistantRuntimeProvider } from "@assistant-ui/react"; +import { useChatRuntime } from "@assistant-ui/react-ai-sdk"; + +export function Chat() { + // Defaults to AssistantChatTransport with /api/chat endpoint + const runtime = useChatRuntime(); + + return ( + + {/* Your chat UI */} + + ); +} +``` + +### 2. Custom Endpoint Configuration + +For custom API endpoints: + +```typescript +import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/react-ai-sdk"; + +// Option 1: AssistantChatTransport (recommended) +// Automatically forwards system messages and tools from context +const runtime = useChatRuntime({ + transport: new AssistantChatTransport({ + api: "/my-custom-api/chat", + }), +}); +``` + +For standard AI SDK transport without automatic forwarding: + +```typescript +import { useChatRuntime } from "@assistant-ui/react-ai-sdk"; +import { DefaultChatTransport } from "ai"; + +// Option 2: DefaultChatTransport (from "ai" package) +// Does NOT auto-forward system/tools +const runtime = useChatRuntime({ + transport: new DefaultChatTransport({ + api: "/api/chat", + }), +}); +``` + +### 3. Transport Types Summary + +| Transport | Package | Auto-Forwards | Use Case | +|-----------|---------|---------------|----------| +| `AssistantChatTransport` | `@assistant-ui/react-ai-sdk` | Yes (system, tools, callSettings) | Default, recommended | +| `DefaultChatTransport` | `ai` | No | Standard AI SDK usage | +| `DirectChatTransport` | `ai` | No | SSR/testing with direct agent | +| `TextStreamChatTransport` | `ai` | No | Plain text backends | + +### 4. What AssistantChatTransport Forwards + +When using `AssistantChatTransport`, the following are automatically sent to your backend: + +```typescript +// Your backend receives in req.body: +{ + messages: UIMessage[], // Conversation messages + system: string, // System prompt from context + tools: Record, // Frontend tools (as JSON schema) + callSettings: {...}, // Call settings from context + id: string, // Thread ID + trigger: string, // What triggered the request + messageId: string, // Message ID + metadata: {...}, // Request metadata +} +``` + +### 5. Exports from @assistant-ui/react-ai-sdk + +```typescript +import { + // Hooks + useChatRuntime, + useAISDKRuntime, + + // Transports + AssistantChatTransport, + + // Helpers + frontendTools, + + // Types + type UseChatRuntimeOptions, +} from "@assistant-ui/react-ai-sdk"; +``` + +--- + +## Complete Migration Examples + +### API Route (Full Example) + +**Before (AI SDK v5):** +```typescript +import { streamText } from "ai"; +import { openai } from "@ai-sdk/openai"; + +export async function POST(req: Request) { + const { messages } = await req.json(); + + const result = streamText({ + model: openai("gpt-4o"), + messages, + maxSteps: 10, + }); + + return (await result).toDataStreamResponse(); +} +``` + +**After (AI SDK v6):** +```typescript +import { + streamText, + convertToModelMessages, + stepCountIs, + tool, + zodSchema, +} from "ai"; +import type { UIMessage } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { z } from "zod"; + +// Allow streaming responses up to 30 seconds +export const maxDuration = 30; + +export async function POST(req: Request) { + const { messages }: { messages: UIMessage[] } = await req.json(); + + const result = streamText({ + model: openai("gpt-4o"), + messages: await convertToModelMessages(messages), + stopWhen: stepCountIs(10), + tools: { + get_weather: tool({ + description: "Get the current weather", + inputSchema: zodSchema( + z.object({ + city: z.string(), + }), + ), + execute: async ({ city }) => { + return `The weather in ${city} is sunny`; + }, + }), + }, + }); + + return result.toUIMessageStreamResponse(); +} +``` + +### API Route with Frontend Tools + +```typescript +import { + streamText, + convertToModelMessages, + stepCountIs, + tool, + zodSchema, +} from "ai"; +import type { UIMessage } from "ai"; +import { frontendTools } from "@assistant-ui/react-ai-sdk"; +import { openai } from "@ai-sdk/openai"; +import { z } from "zod"; + +export const maxDuration = 30; + +export async function POST(req: Request) { + const { + messages, + system, + tools: clientTools, + }: { + messages: UIMessage[]; + system?: string; + tools?: Record; + } = await req.json(); + + const result = streamText({ + model: openai("gpt-4o"), + system, + messages: await convertToModelMessages(messages), + stopWhen: stepCountIs(10), + tools: { + // Frontend tools forwarded from client + ...frontendTools(clientTools ?? {}), + // Backend-only tools + search_database: tool({ + description: "Search the database", + inputSchema: zodSchema( + z.object({ + query: z.string(), + }), + ), + execute: async ({ query }) => { + // Server-side only logic + return { results: [] }; + }, + }), + }, + }); + + return result.toUIMessageStreamResponse(); +} +``` + +### Custom Stream with Data Parts + +**Before (v5):** +```typescript +import { createDataStreamResponse, streamText } from "ai"; + +return createDataStreamResponse({ + execute: async (writer) => { + writer.writeMessageAnnotation({ + type: "custom-metadata", + timestamp: Date.now(), + }); + + const result = streamText({ model, messages }); + result.mergeIntoDataStream(writer); + }, +}); +``` + +**After (v6):** +```typescript +import { createUIMessageStream, createUIMessageStreamResponse, streamText } from "ai"; + +const stream = createUIMessageStream({ + execute: async ({ writer }) => { + // Custom data part + writer.write({ + type: "data-metadata", + id: "meta-1", + data: { timestamp: Date.now() }, + }); + + // Merge model response + const result = streamText({ model, messages: await convertToModelMessages(messages) }); + writer.merge(result.toUIMessageStream()); + }, + onFinish: ({ messages, responseMessage }) => { + // Persist to database + }, +}); + +return createUIMessageStreamResponse({ stream }); +``` + +### Client Component with Tools + +```typescript +"use client"; + +import { AssistantRuntimeProvider, useAssistantTool } from "@assistant-ui/react"; +import { useChatRuntime } from "@assistant-ui/react-ai-sdk"; +import { z } from "zod"; + +function WeatherTool() { + useAssistantTool({ + toolName: "get_weather", + description: "Get current weather for a location", + parameters: z.object({ + location: z.string(), + }), + execute: async ({ location }) => { + // Client-side execution + const response = await fetch(`/api/weather?location=${location}`); + return response.json(); + }, + render: ({ args, result, status }) => { + if (status === "running") return
Loading weather...
; + if (result) return ; + return null; + }, + }); + + return null; +} + +export function Chat() { + const runtime = useChatRuntime(); + + return ( + + + {/* Your chat UI */} + + ); +} +``` + +--- + +## Environment Configuration + +### Disable Warning Logging + +The new warning logger outputs deprecation warnings by default. Disable with: + +```bash +export AI_SDK_LOG_WARNINGS=false +``` + +--- + +## Test Utilities + +V2 mock classes have been removed. Migrate to V3 equivalents: + +```diff +- import { MockLanguageModelV2 } from "ai/test"; ++ import { MockLanguageModelV3 } from "ai/test"; + +- import { MockEmbeddingModelV2 } from "ai/test"; ++ import { MockEmbeddingModelV3 } from "ai/test"; + +- import { MockProviderV2 } from "ai/test"; ++ import { MockProviderV3 } from "ai/test"; +``` + +--- + +## New Utilities in v6 + +These are new helper functions added in v6 (not breaking changes, but useful): + +```typescript +import { + // Message management + pruneMessages, // Helper to prune message history by token count + safeValidateUIMessages, // Validates UI messages without throwing + + // Type guards + isDataUIPart, // Type guard for data parts + + // Model middleware + wrapEmbeddingModel, // Wrap embedding model with middleware +} from "ai"; +``` + +--- + +## v4-Specific Changes (v4 → v6 Direct Migration) + +If migrating directly from v4 (skipping v5), apply these additional changes: + +### Parameter Renames + +```diff +- maxTokens: 1024, ++ maxOutputTokens: 1024, + +- providerMetadata: { openai: { store: false } }, ++ providerOptions: { openai: { store: false } }, +``` + +### useChat Hook Overhaul + +Input state is no longer managed by the hook: + +```diff +// v4 +- const { input, handleInputChange, handleSubmit } = useChat(); + +// v6 ++ const [input, setInput] = useState(""); ++ const { sendMessage } = useChat({ ++ transport: new DefaultChatTransport({ api: "/api/chat" }), ++ }); ++ ++ const handleSubmit = (e) => { ++ e.preventDefault(); ++ sendMessage({ text: input }); ++ setInput(""); ++ }; +``` + +### append → sendMessage + +```diff +// v4 +- append({ role: "user", content: "Hello" }); + +// v6 ++ sendMessage({ text: "Hello" }); +// Or with parts: ++ sendMessage({ parts: [{ type: "text", text: "Hello" }] }); +``` + +### Tool Input/Output Properties + +```diff +// v4 +- part.args // Tool input +- part.result // Tool output + +// v6 ++ part.input // Tool input ++ part.output // Tool output +``` + +### File Part Changes + +```diff +// v4 +- part.mimeType +- part.data + +// v6 ++ part.mediaType ++ part.url +``` + +### useAssistant Hook Removed + +The `useAssistant` hook has been removed entirely. Use `useChat` with appropriate configuration instead. + +### Package Imports Changed + +```diff +// v4 +- import { useChat } from "ai/react"; + +// v6 ++ import { useChat } from "@ai-sdk/react"; +// Or for assistant-ui: ++ import { useChatRuntime } from "@assistant-ui/react-ai-sdk"; +``` + +### Codemods for v4 + +Run both v5 and v6 codemods: + +```bash +npx @ai-sdk/codemod upgrade # Runs all codemods (v4→v5→v6) +# OR +npx @ai-sdk/codemod v5 # v4→v5 only +npx @ai-sdk/codemod v6 # v5→v6 only +``` + +--- + +## Migration Checklist + +### Package Updates +- [ ] Update `ai` to `^6.0.0` +- [ ] Update `@ai-sdk/react` to `^3.0.0` +- [ ] Update `@ai-sdk/provider` to `^3.0.0` +- [ ] Update `@ai-sdk/provider-utils` to `^4.0.0` +- [ ] Update all `@ai-sdk/*` provider packages to `^3.0.0` +- [ ] Update `zod` to `^3.25.76` or `^4.1.8` (both supported) +- [ ] Update `@assistant-ui/react` to `^0.11.58` +- [ ] Update `@assistant-ui/react-ai-sdk` to `^1.2.0` +- [ ] If using MCP: Install `@ai-sdk/mcp` to `^1.0.0` + +### Automated Migration +- [ ] Run `npx @ai-sdk/codemod v6` (from v5) +- [ ] OR run `npx @ai-sdk/codemod upgrade` (from v4, runs all) +- [ ] Review all automated changes manually + +### Core Changes +- [ ] Replace `CoreMessage` with `ModelMessage` +- [ ] Replace `convertToCoreMessages` with `convertToModelMessages` +- [ ] Add `await` to all `convertToModelMessages()` calls +- [ ] Replace `maxSteps` with `stopWhen: stepCountIs(n)` +- [ ] Update `generateObject`/`streamObject` to use `generateText`/`streamText` with `Output` +- [ ] Rename `ToolCallOptions` to `ToolExecutionOptions` +- [ ] Update embedding provider methods (`textEmbedding`/`textEmbeddingModel` → `embedding`/`embeddingModel`) +- [ ] Update tool UI helpers (`isToolUIPart` → `isStaticToolUIPart` for static tools) +- [ ] Update `Tool.toModelOutput` signature to use destructuring +- [ ] Update token usage property access paths +- [ ] Handle new "other" finish reason (was "unknown") +- [ ] Update rerank results: `relevanceScore` → `score` +- [ ] If using MCP: Update imports from `ai` to `@ai-sdk/mcp` +- [ ] If using Agent: Rename `createAgentStreamResponse` → `createAgentUIStreamResponse` +- [ ] If using Agent: Rename `messages` → `uiMessages` in agent stream results + +### UI & React Changes +- [ ] Update message handling for `parts` array structure +- [ ] Manage input state manually with useChat +- [ ] Replace `append()` with `sendMessage()` +- [ ] Update tool result handling: use `addToolResult({ tool, toolCallId, output })` or `addToolOutput` with state +- [ ] Handle new status states (submitted, streaming, ready, error) +- [ ] Update to transport-based configuration + +### Streaming Changes +- [ ] Update stream response: `result.toUIMessageStreamResponse()` (not awaited) +- [ ] Use custom data parts with `type: "data-*"` pattern +- [ ] Add `x-vercel-ai-ui-message-stream: v1` header for custom backends +- [ ] Implement `onFinish` for message persistence + +### Tool Changes +- [ ] Use `tool()` helper for backend tool definitions +- [ ] Use Zod schemas in `inputSchema` (directly or with `zodSchema()` wrapper) +- [ ] Use `frontendTools()` helper for forwarding frontend tools +- [ ] Move `strictJsonSchema` to per-tool `strict` property +- [ ] Update tool execute signatures for `ToolExecutionOptions` +- [ ] Handle new tool states: `input-streaming`, `input-available`, `output-available`, `output-error` +- [ ] Place `.describe()` and `.meta()` calls last in Zod schema chains + +### Structured Output +- [ ] Replace `generateObject` with `generateText` + `Output.object()` +- [ ] Replace `streamObject` with `streamText` + `Output.object()` +- [ ] Use `partialOutputStream` instead of `partialObjectStream` +- [ ] Use `elementStream` for streaming arrays + +### assistant-ui Specific +- [ ] Simplify client: `useChatRuntime()` works with no config for `/api/chat` +- [ ] Use `AssistantChatTransport` (from @assistant-ui/react-ai-sdk) for custom endpoints +- [ ] Import `DefaultChatTransport` from "ai" package (not assistant-ui) + +### Provider-Specific +- [ ] Handle OpenAI `strictJsonSchema` default change (now `true`) +- [ ] Update Azure metadata key from `openai` to `azure` +- [ ] Update Vertex metadata key from `google` to `vertex` +- [ ] Configure Anthropic `structuredOutputMode` if needed + +### Testing +- [ ] Update mock classes from V2 to V3 +- [ ] Test all streaming functionality +- [ ] Verify tool execution with new states +- [ ] Test custom data parts diff --git a/.agents/skills/update/references/assistant-ui.md b/.agents/skills/update/references/assistant-ui.md new file mode 100644 index 00000000..e4b0bddf --- /dev/null +++ b/.agents/skills/update/references/assistant-ui.md @@ -0,0 +1,261 @@ +# assistant-ui Version Migrations + +Migrations for upgrading between assistant-ui versions. + +## Version Detection + +```bash +npm ls @assistant-ui/react +npm view @assistant-ui/react version # Latest +``` + +## Migration: → 0.11.x (Runtime Rearchitecture) + +### From 0.10.x + +**New unified state API:** + +```typescript +import { + useAssistantApi, + useAssistantState, + useAssistantEvent +} from "@assistant-ui/react"; + +// State access (replaces various useThread* hooks) +const messages = useAssistantState(s => s.thread.messages); +const isRunning = useAssistantState(s => s.thread.isRunning); + +// Actions +const api = useAssistantApi(); +api.thread().append({ role: "user", content: [{ type: "text", text: "Hello" }] }); +api.thread().cancelRun(); + +// Events +useAssistantEvent("message-added", (e) => { + console.log("New message:", e.message); +}); +``` + +**AI SDK v5/v6 support added:** +- Use `useChatRuntime` for AI SDK v6 +- `useAISDKRuntime` still works for migration + +**Renames:** +- `toolUIs` → `tools` (0.11.39) +- `useLocalThreadRuntime` deprecated, use `useLocalRuntime` + +--- + +## Migration: → 0.10.x (ESM Only) + +### From 0.9.x + +**BREAKING: CommonJS dropped** + +Update bundler if needed: +```json +// package.json +{ + "type": "module" +} +``` + +Or configure bundler for ESM: +```javascript +// next.config.js +export default { + experimental: { + esmExternals: true + } +} +``` + +**New APIs:** +- `ContentPart` renamed to `MessagePart` (0.10.25) +- `MessageContent.ToolGroup` added +- `runtime.thread.reset()` added + +--- + +## Migration: → 0.9.x (Edge Split) + +### From 0.8.x + +**Edge package split:** +- Edge runtime utilities moved to separate entry points +- Check imports if using edge runtime + +--- + +## Migration: → 0.8.x (UI Split) + +### From 0.7.x + +**BREAKING: Pre-styled UI moved out of `@assistant-ui/react`** + +0.7.x: `Thread` etc. were re-exported from `@assistant-ui/react` via `./ui` subpath +0.8.0+: Use shadcn/ui registry (recommended) or `@assistant-ui/react-ui` (legacy, not maintained) + +**Option 1: shadcn/ui Registry (Recommended)** + +```bash +# Using assistant-ui CLI +npx assistant-ui add thread thread-list + +# Or using shadcn CLI +npx shadcn@latest add "https://r.assistant-ui.com/thread" +``` + +Components are copied to your project (e.g., `components/assistant-ui/thread.tsx`). + +```diff +// Styled components - now local files +// Note: ThreadWelcome is now embedded inside Thread (shows when thread is empty) +- import { Thread, ThreadWelcome } from "@assistant-ui/react"; ++ import { Thread } from "@/components/assistant-ui/thread"; + +// Primitives remain in @assistant-ui/react (no change) +import { ThreadPrimitive } from "@assistant-ui/react"; +``` + +**Option 2: Legacy Package (Not Recommended)** + +`@assistant-ui/react-ui` exists but is not actively maintained. + +**Search for imports to update:** +```bash +grep -r "from ['\"]@assistant-ui/react['\"]" --include="*.tsx" --include="*.ts" | grep -v "Primitive" +``` + +**setResult/setArtifact merged (0.8.18):** +```diff +- tool.setResult(result); +- tool.setArtifact(artifact); ++ tool.setResponse({ result, artifact }); +``` + +--- + +## Migration: → 0.7.x (Thread API) + +### From 0.6.x or 0.5.x + +**BREAKING (0.7.44): Thread API moved** + +```diff +- runtime.switchToThread(threadId); ++ runtime.threads.switchToThread(threadId); + +- runtime.switchToNewThread(); ++ runtime.threads.switchToNewThread(); + +- runtime.threadList ++ runtime.threads +``` + +**Search:** +```bash +grep -r "runtime\.switchToThread\|runtime\.switchToNewThread\|runtime\.threadList" --include="*.tsx" --include="*.ts" +``` + +**Deprecated features dropped (0.7.0):** +- All previously deprecated APIs removed +- `ThreadListItemPrimitive` introduced + +--- + +## Migration: → 0.5.x (Runtime API) + +### From 0.4.x + +**maxToolRoundtrips → maxSteps (0.5.74):** +```diff +- maxToolRoundtrips: 5, ++ maxSteps: 5, +``` + +**New Runtime API introduced (0.5.61+):** +- `ThreadRuntime.Composer` +- Status/attachments/metadata on all messages + +--- + +## Migration: → 0.4.x (Message Types) + +### From 0.3.x + +**BREAKING: Message type renames** + +```diff +- import type { AssistantMessage, UserMessage } from "@assistant-ui/react"; ++ import type { ThreadAssistantMessage, ThreadUserMessage } from "@assistant-ui/react"; +``` + +**Search:** +```bash +grep -r "AssistantMessage\|UserMessage" --include="*.tsx" --include="*.ts" | grep -v "Thread" +``` + +**System message support added** + +--- + +## Migration: → 0.3.x + +### From 0.2.x + +**BREAKING: Message.InProgress dropped** +- Use message status instead of `Message.InProgress` + +--- + +## Migration: → 0.2.x + +### From 0.1.x + +**BREAKING: MessagePartText renders as `

`** +- Text parts now wrapped in paragraph element +- Adjust CSS if needed + +--- + +## Automated Search Commands + +Find patterns that need updating: + +```bash +# Old thread API +grep -rn "runtime\.switchToThread\|runtime\.threadList" --include="*.tsx" --include="*.ts" + +# Old message types +grep -rn "AssistantMessage\[^C\]\|UserMessage\[^C\]" --include="*.tsx" --include="*.ts" + +# Old tool API +grep -rn "setResult\|setArtifact" --include="*.tsx" --include="*.ts" + +# Styled imports (need shadcn registry migration) +grep -rn "from ['\"]@assistant-ui/react['\"]" --include="*.tsx" | grep -v "Primitive\|Runtime\|use" +``` + +## Verification + +After migration: + +```bash +# Type check +npx tsc --noEmit + +# Build +pnpm build + +# Test +pnpm test +``` + +Manual verification: +- [ ] App starts +- [ ] Chat renders +- [ ] Messages send/receive +- [ ] Tools work +- [ ] Thread switching works diff --git a/.agents/skills/update/references/breaking-changes.md b/.agents/skills/update/references/breaking-changes.md new file mode 100644 index 00000000..55bee589 --- /dev/null +++ b/.agents/skills/update/references/breaking-changes.md @@ -0,0 +1,108 @@ +# Breaking Changes Quick Reference + +Fast lookup for breaking changes by version. + +## By Version + +| Version | Breaking Change | Migration | +|---------|-----------------|-----------| +| **0.11.0** | Runtime rearchitecture | Use `useAssistantApi`, `useAssistantState`, `useAssistantEvent` | +| **0.10.0** | CommonJS dropped | Use ESM, set `"type": "module"` | +| **0.8.18** | `setResult`/`setArtifact` merged | Use `setResponse({ result, artifact })` | +| **0.8.0** | UI moved out of core | Use shadcn registry (recommended) or primitives | +| **0.7.44** | `runtime.switchToThread()` moved | Use `runtime.threads.switchToThread()` | +| **0.7.44** | `runtime.threadList` renamed | Use `runtime.threads` | +| **0.7.0** | Deprecated features dropped | Update to non-deprecated APIs | +| **0.5.74** | `maxToolRoundtrips` renamed | Use `maxSteps` | +| **0.4.0** | `AssistantMessage` renamed | Use `ThreadAssistantMessage` | +| **0.4.0** | `UserMessage` renamed | Use `ThreadUserMessage` | +| **0.3.0** | `Message.InProgress` dropped | Use message status | +| **0.2.0** | `MessagePartText` renders as `

` | Adjust CSS | + +## By Pattern + +### Import Changes + +```diff +# Styled components (0.8.0+) - use shadcn registry (recommended) +- import { Thread } from "@assistant-ui/react"; ++ import { Thread } from "@/components/assistant-ui/thread"; +# Note: Run `npx assistant-ui add thread` to install + +# Message types (0.4.0+) +- import type { AssistantMessage, UserMessage } from "@assistant-ui/react"; ++ import type { ThreadAssistantMessage, ThreadUserMessage } from "@assistant-ui/react"; + +# AI SDK v6 (react-ai-sdk 1.0+) +- import { useChat } from "ai/react"; +- import { useAISDKRuntime } from "@assistant-ui/react-ai-sdk"; ++ import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/react-ai-sdk"; +``` + +### API Changes + +```diff +# Thread switching (0.7.44+) +- runtime.switchToThread(id); +- runtime.switchToNewThread(); +- runtime.threadList ++ runtime.threads.switchToThread(id); ++ runtime.threads.switchToNewThread(); ++ runtime.threads + +# Tool response (0.8.18+) +- tool.setResult(result); +- tool.setArtifact(artifact); ++ tool.setResponse({ result, artifact }); + +# State access (0.11.0+) +- const { messages } = useThread(); ++ const messages = useAssistantState(s => s.thread.messages); + +# Actions (0.11.0+) +- useThreadActions().append(...) ++ useAssistantApi().thread().append(...) +``` + +### Config Changes + +```diff +# Tool steps (0.5.74+) +- maxToolRoundtrips: 5, ++ maxSteps: 5, +``` + +## Search Commands + +Find code needing updates: + +```bash +# All breaking patterns +grep -rn "runtime\.switchToThread\|runtime\.threadList\|AssistantMessage[^C]\|UserMessage[^C]\|setResult\|setArtifact\|maxToolRoundtrips" --include="*.tsx" --include="*.ts" + +# Specific version checks +grep -rn "from ['\"]@assistant-ui/react['\"]" --include="*.tsx" | grep -v Primitive # 0.8.0 +grep -rn "Message\.InProgress" --include="*.tsx" # 0.3.0 +``` + +## AI SDK v6 Changes (Separate) + +See [./ai-sdk-v6.md](./ai-sdk-v6.md) for AI SDK specific migrations: + +| Old | New | +|-----|-----| +| `maxSteps` | `stopWhen: stepCountIs(n)` | +| `parameters` | `inputSchema` (in `tool()`) | +| `toDataStreamResponse()` | `toUIMessageStreamResponse()` | +| `generateObject()` | `generateText() + Output.object()` | +| `CoreMessage` | `ModelMessage` | +| `Message` | `UIMessage` | + +## Version Compatibility + +| @assistant-ui/react | react-ai-sdk | AI SDK | Zod | +|---------------------|--------------|--------|-----| +| 0.11.x | 1.2.x | 6.x | 3.25+ or 4.x | +| 0.10.x | 0.x | 4.x-5.x | 3.x | +| 0.8.x-0.9.x | 0.x | 4.x | 3.x | +| < 0.8.0 | 0.x | 4.x | 3.x | diff --git a/.agents/skills/vercel-react-best-practices/AGENTS.md b/.agents/skills/vercel-react-best-practices/AGENTS.md new file mode 100644 index 00000000..f9b9e99c --- /dev/null +++ b/.agents/skills/vercel-react-best-practices/AGENTS.md @@ -0,0 +1,2410 @@ +# React Best Practices + +**Version 1.0.0** +Vercel Engineering +January 2026 + +> **Note:** +> This document is mainly for agents and LLMs to follow when maintaining, +> generating, or refactoring React and Next.js codebases at Vercel. Humans +> may also find it useful, but guidance here is optimized for automation +> and consistency by AI-assisted workflows. + +--- + +## Abstract + +Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation. + +--- + +## Table of Contents + +1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL** + - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed) + - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization) + - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes) + - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations) + - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries) +2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL** + - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports) + - 2.2 [Conditional Module Loading](#22-conditional-module-loading) + - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries) + - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components) + - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent) +3. [Server-Side Performance](#3-server-side-performance) — **HIGH** + - 3.1 [Cross-Request LRU Caching](#31-cross-request-lru-caching) + - 3.2 [Minimize Serialization at RSC Boundaries](#32-minimize-serialization-at-rsc-boundaries) + - 3.3 [Parallel Data Fetching with Component Composition](#33-parallel-data-fetching-with-component-composition) + - 3.4 [Per-Request Deduplication with React.cache()](#34-per-request-deduplication-with-reactcache) + - 3.5 [Use after() for Non-Blocking Operations](#35-use-after-for-non-blocking-operations) +4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH** + - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners) + - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance) + - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication) + - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data) +5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM** + - 5.1 [Defer State Reads to Usage Point](#51-defer-state-reads-to-usage-point) + - 5.2 [Extract to Memoized Components](#52-extract-to-memoized-components) + - 5.3 [Narrow Effect Dependencies](#53-narrow-effect-dependencies) + - 5.4 [Subscribe to Derived State](#54-subscribe-to-derived-state) + - 5.5 [Use Functional setState Updates](#55-use-functional-setstate-updates) + - 5.6 [Use Lazy State Initialization](#56-use-lazy-state-initialization) + - 5.7 [Use Transitions for Non-Urgent Updates](#57-use-transitions-for-non-urgent-updates) +6. [Rendering Performance](#6-rendering-performance) — **MEDIUM** + - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element) + - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists) + - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements) + - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision) + - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering) + - 6.6 [Use Activity Component for Show/Hide](#66-use-activity-component-for-showhide) + - 6.7 [Use Explicit Conditional Rendering](#67-use-explicit-conditional-rendering) +7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM** + - 7.1 [Batch DOM CSS Changes](#71-batch-dom-css-changes) + - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups) + - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops) + - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls) + - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls) + - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations) + - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons) + - 7.8 [Early Return from Functions](#78-early-return-from-functions) + - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation) + - 7.10 [Use Loop for Min/Max Instead of Sort](#710-use-loop-for-minmax-instead-of-sort) + - 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups) + - 7.12 [Use toSorted() Instead of sort() for Immutability](#712-use-tosorted-instead-of-sort-for-immutability) +8. [Advanced Patterns](#8-advanced-patterns) — **LOW** + - 8.1 [Store Event Handlers in Refs](#81-store-event-handlers-in-refs) + - 8.2 [useLatest for Stable Callback Refs](#82-uselatest-for-stable-callback-refs) + +--- + +## 1. Eliminating Waterfalls + +**Impact: CRITICAL** + +Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains. + +### 1.1 Defer Await Until Needed + +**Impact: HIGH (avoids blocking unused code paths)** + +Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them. + +**Incorrect: blocks both branches** + +```typescript +async function handleRequest(userId: string, skipProcessing: boolean) { + const userData = await fetchUserData(userId) + + if (skipProcessing) { + // Returns immediately but still waited for userData + return { skipped: true } + } + + // Only this branch uses userData + return processUserData(userData) +} +``` + +**Correct: only blocks when needed** + +```typescript +async function handleRequest(userId: string, skipProcessing: boolean) { + if (skipProcessing) { + // Returns immediately without waiting + return { skipped: true } + } + + // Fetch only when needed + const userData = await fetchUserData(userId) + return processUserData(userData) +} +``` + +**Another example: early return optimization** + +```typescript +// Incorrect: always fetches permissions +async function updateResource(resourceId: string, userId: string) { + const permissions = await fetchPermissions(userId) + const resource = await getResource(resourceId) + + if (!resource) { + return { error: 'Not found' } + } + + if (!permissions.canEdit) { + return { error: 'Forbidden' } + } + + return await updateResourceData(resource, permissions) +} + +// Correct: fetches only when needed +async function updateResource(resourceId: string, userId: string) { + const resource = await getResource(resourceId) + + if (!resource) { + return { error: 'Not found' } + } + + const permissions = await fetchPermissions(userId) + + if (!permissions.canEdit) { + return { error: 'Forbidden' } + } + + return await updateResourceData(resource, permissions) +} +``` + +This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive. + +### 1.2 Dependency-Based Parallelization + +**Impact: CRITICAL (2-10× improvement)** + +For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment. + +**Incorrect: profile waits for config unnecessarily** + +```typescript +const [user, config] = await Promise.all([ + fetchUser(), + fetchConfig() +]) +const profile = await fetchProfile(user.id) +``` + +**Correct: config and profile run in parallel** + +```typescript +import { all } from 'better-all' + +const { user, config, profile } = await all({ + async user() { return fetchUser() }, + async config() { return fetchConfig() }, + async profile() { + return fetchProfile((await this.$.user).id) + } +}) +``` + +Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all) + +### 1.3 Prevent Waterfall Chains in API Routes + +**Impact: CRITICAL (2-10× improvement)** + +In API routes and Server Actions, start independent operations immediately, even if you don't await them yet. + +**Incorrect: config waits for auth, data waits for both** + +```typescript +export async function GET(request: Request) { + const session = await auth() + const config = await fetchConfig() + const data = await fetchData(session.user.id) + return Response.json({ data, config }) +} +``` + +**Correct: auth and config start immediately** + +```typescript +export async function GET(request: Request) { + const sessionPromise = auth() + const configPromise = fetchConfig() + const session = await sessionPromise + const [config, data] = await Promise.all([ + configPromise, + fetchData(session.user.id) + ]) + return Response.json({ data, config }) +} +``` + +For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization). + +### 1.4 Promise.all() for Independent Operations + +**Impact: CRITICAL (2-10× improvement)** + +When async operations have no interdependencies, execute them concurrently using `Promise.all()`. + +**Incorrect: sequential execution, 3 round trips** + +```typescript +const user = await fetchUser() +const posts = await fetchPosts() +const comments = await fetchComments() +``` + +**Correct: parallel execution, 1 round trip** + +```typescript +const [user, posts, comments] = await Promise.all([ + fetchUser(), + fetchPosts(), + fetchComments() +]) +``` + +### 1.5 Strategic Suspense Boundaries + +**Impact: HIGH (faster initial paint)** + +Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads. + +**Incorrect: wrapper blocked by data fetching** + +```tsx +async function Page() { + const data = await fetchData() // Blocks entire page + + return ( +

+
Sidebar
+
Header
+
+ +
+
Footer
+
+ ) +} +``` + +The entire layout waits for data even though only the middle section needs it. + +**Correct: wrapper shows immediately, data streams in** + +```tsx +function Page() { + return ( +
+
Sidebar
+
Header
+
+ }> + + +
+
Footer
+
+ ) +} + +async function DataDisplay() { + const data = await fetchData() // Only blocks this component + return
{data.content}
+} +``` + +Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data. + +**Alternative: share promise across components** + +```tsx +function Page() { + // Start fetch immediately, but don't await + const dataPromise = fetchData() + + return ( +
+
Sidebar
+
Header
+ }> + + + +
Footer
+
+ ) +} + +function DataDisplay({ dataPromise }: { dataPromise: Promise }) { + const data = use(dataPromise) // Unwraps the promise + return
{data.content}
+} + +function DataSummary({ dataPromise }: { dataPromise: Promise }) { + const data = use(dataPromise) // Reuses the same promise + return
{data.summary}
+} +``` + +Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together. + +**When NOT to use this pattern:** + +- Critical data needed for layout decisions (affects positioning) + +- SEO-critical content above the fold + +- Small, fast queries where suspense overhead isn't worth it + +- When you want to avoid layout shift (loading → content jump) + +**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities. + +--- + +## 2. Bundle Size Optimization + +**Impact: CRITICAL** + +Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint. + +### 2.1 Avoid Barrel File Imports + +**Impact: CRITICAL (200-800ms import cost, slow builds)** + +Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`). + +Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts. + +**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph. + +**Incorrect: imports entire library** + +```tsx +import { Check, X, Menu } from 'lucide-react' +// Loads 1,583 modules, takes ~2.8s extra in dev +// Runtime cost: 200-800ms on every cold start + +import { Button, TextField } from '@mui/material' +// Loads 2,225 modules, takes ~4.2s extra in dev +``` + +**Correct: imports only what you need** + +```tsx +import Check from 'lucide-react/dist/esm/icons/check' +import X from 'lucide-react/dist/esm/icons/x' +import Menu from 'lucide-react/dist/esm/icons/menu' +// Loads only 3 modules (~2KB vs ~1MB) + +import Button from '@mui/material/Button' +import TextField from '@mui/material/TextField' +// Loads only what you use +``` + +**Alternative: Next.js 13.5+** + +```js +// next.config.js - use optimizePackageImports +module.exports = { + experimental: { + optimizePackageImports: ['lucide-react', '@mui/material'] + } +} + +// Then you can keep the ergonomic barrel imports: +import { Check, X, Menu } from 'lucide-react' +// Automatically transformed to direct imports at build time +``` + +Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR. + +Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`. + +Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) + +### 2.2 Conditional Module Loading + +**Impact: HIGH (loads large data only when needed)** + +Load large data or modules only when a feature is activated. + +**Example: lazy-load animation frames** + +```tsx +function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch> }) { + const [frames, setFrames] = useState(null) + + useEffect(() => { + if (enabled && !frames && typeof window !== 'undefined') { + import('./animation-frames.js') + .then(mod => setFrames(mod.frames)) + .catch(() => setEnabled(false)) + } + }, [enabled, frames, setEnabled]) + + if (!frames) return + return +} +``` + +The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed. + +### 2.3 Defer Non-Critical Third-Party Libraries + +**Impact: MEDIUM (loads after hydration)** + +Analytics, logging, and error tracking don't block user interaction. Load them after hydration. + +**Incorrect: blocks initial bundle** + +```tsx +import { Analytics } from '@vercel/analytics/react' + +export default function RootLayout({ children }) { + return ( + + + {children} + + + + ) +} +``` + +**Correct: loads after hydration** + +```tsx +import dynamic from 'next/dynamic' + +const Analytics = dynamic( + () => import('@vercel/analytics/react').then(m => m.Analytics), + { ssr: false } +) + +export default function RootLayout({ children }) { + return ( + + + {children} + + + + ) +} +``` + +### 2.4 Dynamic Imports for Heavy Components + +**Impact: CRITICAL (directly affects TTI and LCP)** + +Use `next/dynamic` to lazy-load large components not needed on initial render. + +**Incorrect: Monaco bundles with main chunk ~300KB** + +```tsx +import { MonacoEditor } from './monaco-editor' + +function CodePanel({ code }: { code: string }) { + return +} +``` + +**Correct: Monaco loads on demand** + +```tsx +import dynamic from 'next/dynamic' + +const MonacoEditor = dynamic( + () => import('./monaco-editor').then(m => m.MonacoEditor), + { ssr: false } +) + +function CodePanel({ code }: { code: string }) { + return +} +``` + +### 2.5 Preload Based on User Intent + +**Impact: MEDIUM (reduces perceived latency)** + +Preload heavy bundles before they're needed to reduce perceived latency. + +**Example: preload on hover/focus** + +```tsx +function EditorButton({ onClick }: { onClick: () => void }) { + const preload = () => { + if (typeof window !== 'undefined') { + void import('./monaco-editor') + } + } + + return ( + + ) +} +``` + +**Example: preload when feature flag is enabled** + +```tsx +function FlagsProvider({ children, flags }: Props) { + useEffect(() => { + if (flags.editorEnabled && typeof window !== 'undefined') { + void import('./monaco-editor').then(mod => mod.init()) + } + }, [flags.editorEnabled]) + + return + {children} + +} +``` + +The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed. + +--- + +## 3. Server-Side Performance + +**Impact: HIGH** + +Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times. + +### 3.1 Cross-Request LRU Caching + +**Impact: HIGH (caches across requests)** + +`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache. + +**Implementation:** + +```typescript +import { LRUCache } from 'lru-cache' + +const cache = new LRUCache({ + max: 1000, + ttl: 5 * 60 * 1000 // 5 minutes +}) + +export async function getUser(id: string) { + const cached = cache.get(id) + if (cached) return cached + + const user = await db.user.findUnique({ where: { id } }) + cache.set(id, user) + return user +} + +// Request 1: DB query, result cached +// Request 2: cache hit, no DB query +``` + +Use when sequential user actions hit multiple endpoints needing the same data within seconds. + +**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis. + +**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching. + +Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache) + +### 3.2 Minimize Serialization at RSC Boundaries + +**Impact: HIGH (reduces data transfer size)** + +The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses. + +**Incorrect: serializes all 50 fields** + +```tsx +async function Page() { + const user = await fetchUser() // 50 fields + return +} + +'use client' +function Profile({ user }: { user: User }) { + return
{user.name}
// uses 1 field +} +``` + +**Correct: serializes only 1 field** + +```tsx +async function Page() { + const user = await fetchUser() + return +} + +'use client' +function Profile({ name }: { name: string }) { + return
{name}
+} +``` + +### 3.3 Parallel Data Fetching with Component Composition + +**Impact: CRITICAL (eliminates server-side waterfalls)** + +React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching. + +**Incorrect: Sidebar waits for Page's fetch to complete** + +```tsx +export default async function Page() { + const header = await fetchHeader() + return ( +
+
{header}
+ +
+ ) +} + +async function Sidebar() { + const items = await fetchSidebarItems() + return +} +``` + +**Correct: both fetch simultaneously** + +```tsx +async function Header() { + const data = await fetchHeader() + return
{data}
+} + +async function Sidebar() { + const items = await fetchSidebarItems() + return +} + +export default function Page() { + return ( +
+
+ +
+ ) +} +``` + +**Alternative with children prop:** + +```tsx +async function Header() { + const data = await fetchHeader() + return
{data}
+} + +async function Sidebar() { + const items = await fetchSidebarItems() + return +} + +function Layout({ children }: { children: ReactNode }) { + return ( +
+
+ {children} +
+ ) +} + +export default function Page() { + return ( + + + + ) +} +``` + +### 3.4 Per-Request Deduplication with React.cache() + +**Impact: MEDIUM (deduplicates within request)** + +Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most. + +**Usage:** + +```typescript +import { cache } from 'react' + +export const getCurrentUser = cache(async () => { + const session = await auth() + if (!session?.user?.id) return null + return await db.user.findUnique({ + where: { id: session.user.id } + }) +}) +``` + +Within a single request, multiple calls to `getCurrentUser()` execute the query only once. + +**Avoid inline objects as arguments:** + +`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits. + +**Incorrect: always cache miss** + +```typescript +const getUser = cache(async (params: { uid: number }) => { + return await db.user.findUnique({ where: { id: params.uid } }) +}) + +// Each call creates new object, never hits cache +getUser({ uid: 1 }) +getUser({ uid: 1 }) // Cache miss, runs query again +``` + +**Correct: cache hit** + +```typescript +const params = { uid: 1 } +getUser(params) // Query runs +getUser(params) // Cache hit (same reference) +``` + +If you must pass objects, pass the same reference: + +**Next.js-Specific Note:** + +In Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks: + +- Database queries (Prisma, Drizzle, etc.) + +- Heavy computations + +- Authentication checks + +- File system operations + +- Any non-fetch async work + +Use `React.cache()` to deduplicate these operations across your component tree. + +Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache) + +### 3.5 Use after() for Non-Blocking Operations + +**Impact: MEDIUM (faster response times)** + +Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response. + +**Incorrect: blocks response** + +```tsx +import { logUserAction } from '@/app/utils' + +export async function POST(request: Request) { + // Perform mutation + await updateDatabase(request) + + // Logging blocks the response + const userAgent = request.headers.get('user-agent') || 'unknown' + await logUserAction({ userAgent }) + + return new Response(JSON.stringify({ status: 'success' }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }) +} +``` + +**Correct: non-blocking** + +```tsx +import { after } from 'next/server' +import { headers, cookies } from 'next/headers' +import { logUserAction } from '@/app/utils' + +export async function POST(request: Request) { + // Perform mutation + await updateDatabase(request) + + // Log after response is sent + after(async () => { + const userAgent = (await headers()).get('user-agent') || 'unknown' + const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous' + + logUserAction({ sessionCookie, userAgent }) + }) + + return new Response(JSON.stringify({ status: 'success' }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }) +} +``` + +The response is sent immediately while logging happens in the background. + +**Common use cases:** + +- Analytics tracking + +- Audit logging + +- Sending notifications + +- Cache invalidation + +- Cleanup tasks + +**Important notes:** + +- `after()` runs even if the response fails or redirects + +- Works in Server Actions, Route Handlers, and Server Components + +Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after) + +--- + +## 4. Client-Side Data Fetching + +**Impact: MEDIUM-HIGH** + +Automatic deduplication and efficient data fetching patterns reduce redundant network requests. + +### 4.1 Deduplicate Global Event Listeners + +**Impact: LOW (single listener for N components)** + +Use `useSWRSubscription()` to share global event listeners across component instances. + +**Incorrect: N instances = N listeners** + +```tsx +function useKeyboardShortcut(key: string, callback: () => void) { + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.metaKey && e.key === key) { + callback() + } + } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }, [key, callback]) +} +``` + +When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener. + +**Correct: N instances = 1 listener** + +```tsx +import useSWRSubscription from 'swr/subscription' + +// Module-level Map to track callbacks per key +const keyCallbacks = new Map void>>() + +function useKeyboardShortcut(key: string, callback: () => void) { + // Register this callback in the Map + useEffect(() => { + if (!keyCallbacks.has(key)) { + keyCallbacks.set(key, new Set()) + } + keyCallbacks.get(key)!.add(callback) + + return () => { + const set = keyCallbacks.get(key) + if (set) { + set.delete(callback) + if (set.size === 0) { + keyCallbacks.delete(key) + } + } + } + }, [key, callback]) + + useSWRSubscription('global-keydown', () => { + const handler = (e: KeyboardEvent) => { + if (e.metaKey && keyCallbacks.has(e.key)) { + keyCallbacks.get(e.key)!.forEach(cb => cb()) + } + } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }) +} + +function Profile() { + // Multiple shortcuts will share the same listener + useKeyboardShortcut('p', () => { /* ... */ }) + useKeyboardShortcut('k', () => { /* ... */ }) + // ... +} +``` + +### 4.2 Use Passive Event Listeners for Scrolling Performance + +**Impact: MEDIUM (eliminates scroll delay caused by event listeners)** + +Add `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay. + +**Incorrect:** + +```typescript +useEffect(() => { + const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX) + const handleWheel = (e: WheelEvent) => console.log(e.deltaY) + + document.addEventListener('touchstart', handleTouch) + document.addEventListener('wheel', handleWheel) + + return () => { + document.removeEventListener('touchstart', handleTouch) + document.removeEventListener('wheel', handleWheel) + } +}, []) +``` + +**Correct:** + +```typescript +useEffect(() => { + const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX) + const handleWheel = (e: WheelEvent) => console.log(e.deltaY) + + document.addEventListener('touchstart', handleTouch, { passive: true }) + document.addEventListener('wheel', handleWheel, { passive: true }) + + return () => { + document.removeEventListener('touchstart', handleTouch) + document.removeEventListener('wheel', handleWheel) + } +}, []) +``` + +**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`. + +**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`. + +### 4.3 Use SWR for Automatic Deduplication + +**Impact: MEDIUM-HIGH (automatic deduplication)** + +SWR enables request deduplication, caching, and revalidation across component instances. + +**Incorrect: no deduplication, each instance fetches** + +```tsx +function UserList() { + const [users, setUsers] = useState([]) + useEffect(() => { + fetch('/api/users') + .then(r => r.json()) + .then(setUsers) + }, []) +} +``` + +**Correct: multiple instances share one request** + +```tsx +import useSWR from 'swr' + +function UserList() { + const { data: users } = useSWR('/api/users', fetcher) +} +``` + +**For immutable data:** + +```tsx +import { useImmutableSWR } from '@/lib/swr' + +function StaticContent() { + const { data } = useImmutableSWR('/api/config', fetcher) +} +``` + +**For mutations:** + +```tsx +import { useSWRMutation } from 'swr/mutation' + +function UpdateButton() { + const { trigger } = useSWRMutation('/api/user', updateUser) + return +} +``` + +Reference: [https://swr.vercel.app](https://swr.vercel.app) + +### 4.4 Version and Minimize localStorage Data + +**Impact: MEDIUM (prevents schema conflicts, reduces storage size)** + +Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data. + +**Incorrect:** + +```typescript +// No version, stores everything, no error handling +localStorage.setItem('userConfig', JSON.stringify(fullUserObject)) +const data = localStorage.getItem('userConfig') +``` + +**Correct:** + +```typescript +const VERSION = 'v2' + +function saveConfig(config: { theme: string; language: string }) { + try { + localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config)) + } catch { + // Throws in incognito/private browsing, quota exceeded, or disabled + } +} + +function loadConfig() { + try { + const data = localStorage.getItem(`userConfig:${VERSION}`) + return data ? JSON.parse(data) : null + } catch { + return null + } +} + +// Migration from v1 to v2 +function migrate() { + try { + const v1 = localStorage.getItem('userConfig:v1') + if (v1) { + const old = JSON.parse(v1) + saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang }) + localStorage.removeItem('userConfig:v1') + } + } catch {} +} +``` + +**Store minimal fields from server responses:** + +```typescript +// User object has 20+ fields, only store what UI needs +function cachePrefs(user: FullUser) { + try { + localStorage.setItem('prefs:v1', JSON.stringify({ + theme: user.preferences.theme, + notifications: user.preferences.notifications + })) + } catch {} +} +``` + +**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled. + +**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags. + +--- + +## 5. Re-render Optimization + +**Impact: MEDIUM** + +Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness. + +### 5.1 Defer State Reads to Usage Point + +**Impact: MEDIUM (avoids unnecessary subscriptions)** + +Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks. + +**Incorrect: subscribes to all searchParams changes** + +```tsx +function ShareButton({ chatId }: { chatId: string }) { + const searchParams = useSearchParams() + + const handleShare = () => { + const ref = searchParams.get('ref') + shareChat(chatId, { ref }) + } + + return +} +``` + +**Correct: reads on demand, no subscription** + +```tsx +function ShareButton({ chatId }: { chatId: string }) { + const handleShare = () => { + const params = new URLSearchParams(window.location.search) + const ref = params.get('ref') + shareChat(chatId, { ref }) + } + + return +} +``` + +### 5.2 Extract to Memoized Components + +**Impact: MEDIUM (enables early returns)** + +Extract expensive work into memoized components to enable early returns before computation. + +**Incorrect: computes avatar even when loading** + +```tsx +function Profile({ user, loading }: Props) { + const avatar = useMemo(() => { + const id = computeAvatarId(user) + return + }, [user]) + + if (loading) return + return
{avatar}
+} +``` + +**Correct: skips computation when loading** + +```tsx +const UserAvatar = memo(function UserAvatar({ user }: { user: User }) { + const id = useMemo(() => computeAvatarId(user), [user]) + return +}) + +function Profile({ user, loading }: Props) { + if (loading) return + return ( +
+ +
+ ) +} +``` + +**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders. + +### 5.3 Narrow Effect Dependencies + +**Impact: LOW (minimizes effect re-runs)** + +Specify primitive dependencies instead of objects to minimize effect re-runs. + +**Incorrect: re-runs on any user field change** + +```tsx +useEffect(() => { + console.log(user.id) +}, [user]) +``` + +**Correct: re-runs only when id changes** + +```tsx +useEffect(() => { + console.log(user.id) +}, [user.id]) +``` + +**For derived state, compute outside effect:** + +```tsx +// Incorrect: runs on width=767, 766, 765... +useEffect(() => { + if (width < 768) { + enableMobileMode() + } +}, [width]) + +// Correct: runs only on boolean transition +const isMobile = width < 768 +useEffect(() => { + if (isMobile) { + enableMobileMode() + } +}, [isMobile]) +``` + +### 5.4 Subscribe to Derived State + +**Impact: MEDIUM (reduces re-render frequency)** + +Subscribe to derived boolean state instead of continuous values to reduce re-render frequency. + +**Incorrect: re-renders on every pixel change** + +```tsx +function Sidebar() { + const width = useWindowWidth() // updates continuously + const isMobile = width < 768 + return