initial commit
This commit is contained in:
174
.agents/skills/better-auth-best-practices/SKILL.md
Normal file
174
.agents/skills/better-auth-best-practices/SKILL.md
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
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<typeof auth>()`.
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
914
.agents/skills/turborepo/SKILL.md
Normal file
914
.agents/skills/turborepo/SKILL.md
Normal file
@@ -0,0 +1,914 @@
|
||||
---
|
||||
name: turborepo
|
||||
description: |
|
||||
Turborepo monorepo build system guidance. Triggers on: turbo.json, task pipelines,
|
||||
dependsOn, caching, remote cache, the "turbo" CLI, --filter, --affected, CI optimization, environment
|
||||
variables, internal packages, monorepo structure/best practices, and boundaries.
|
||||
|
||||
Use when user: configures tasks/workflows/pipelines, creates packages, sets up
|
||||
monorepo, shares code between apps, runs changed/affected packages, debugs cache,
|
||||
or has apps/packages directories.
|
||||
metadata:
|
||||
version: 2.8.11-canary.27
|
||||
---
|
||||
|
||||
# Turborepo Skill
|
||||
|
||||
Build system for JavaScript/TypeScript monorepos. Turborepo caches task outputs and runs tasks in parallel based on dependency graph.
|
||||
|
||||
## IMPORTANT: Package Tasks, Not Root Tasks
|
||||
|
||||
**DO NOT create Root Tasks. ALWAYS create package tasks.**
|
||||
|
||||
When creating tasks/scripts/pipelines, you MUST:
|
||||
|
||||
1. Add the script to each relevant package's `package.json`
|
||||
2. Register the task in root `turbo.json`
|
||||
3. Root `package.json` only delegates via `turbo run <task>`
|
||||
|
||||
**DO NOT** put task logic in root `package.json`. This defeats Turborepo's parallelization.
|
||||
|
||||
```json
|
||||
// DO THIS: Scripts in each package
|
||||
// apps/web/package.json
|
||||
{ "scripts": { "build": "next build", "lint": "eslint .", "test": "vitest" } }
|
||||
|
||||
// apps/api/package.json
|
||||
{ "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } }
|
||||
|
||||
// packages/ui/package.json
|
||||
{ "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } }
|
||||
```
|
||||
|
||||
```json
|
||||
// turbo.json - register tasks
|
||||
{
|
||||
"tasks": {
|
||||
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
|
||||
"lint": {},
|
||||
"test": { "dependsOn": ["build"] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// Root package.json - ONLY delegates, no task logic
|
||||
{
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"lint": "turbo run lint",
|
||||
"test": "turbo run test"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// DO NOT DO THIS - defeats parallelization
|
||||
// Root package.json
|
||||
{
|
||||
"scripts": {
|
||||
"build": "cd apps/web && next build && cd ../api && tsc",
|
||||
"lint": "eslint apps/ packages/",
|
||||
"test": "vitest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Root Tasks (`//#taskname`) are ONLY for tasks that truly cannot exist in packages (rare).
|
||||
|
||||
## Secondary Rule: `turbo run` vs `turbo`
|
||||
|
||||
**Always use `turbo run` when the command is written into code:**
|
||||
|
||||
```json
|
||||
// package.json - ALWAYS "turbo run"
|
||||
{
|
||||
"scripts": {
|
||||
"build": "turbo run build"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# CI workflows - ALWAYS "turbo run"
|
||||
- run: turbo run build --affected
|
||||
```
|
||||
|
||||
**The shorthand `turbo <tasks>` is ONLY for one-off terminal commands** typed directly by humans or agents. Never write `turbo build` into package.json, CI, or scripts.
|
||||
|
||||
## Quick Decision Trees
|
||||
|
||||
### "I need to configure a task"
|
||||
|
||||
```
|
||||
Configure a task?
|
||||
├─ Define task dependencies → references/configuration/tasks.md
|
||||
├─ Lint/check-types (parallel + caching) → Use Transit Nodes pattern (see below)
|
||||
├─ Specify build outputs → references/configuration/tasks.md#outputs
|
||||
├─ Handle environment variables → references/environment/RULE.md
|
||||
├─ Set up dev/watch tasks → references/configuration/tasks.md#persistent
|
||||
├─ Package-specific config → references/configuration/RULE.md#package-configurations
|
||||
└─ Global settings (cacheDir, daemon) → references/configuration/global-options.md
|
||||
```
|
||||
|
||||
### "My cache isn't working"
|
||||
|
||||
```
|
||||
Cache problems?
|
||||
├─ Tasks run but outputs not restored → Missing `outputs` key
|
||||
├─ Cache misses unexpectedly → references/caching/gotchas.md
|
||||
├─ Need to debug hash inputs → Use --summarize or --dry
|
||||
├─ Want to skip cache entirely → Use --force or cache: false
|
||||
├─ Remote cache not working → references/caching/remote-cache.md
|
||||
└─ Environment causing misses → references/environment/gotchas.md
|
||||
```
|
||||
|
||||
### "I want to run only changed packages"
|
||||
|
||||
```
|
||||
Run only what changed?
|
||||
├─ Changed packages + dependents (RECOMMENDED) → turbo run build --affected
|
||||
├─ Custom base branch → --affected --affected-base=origin/develop
|
||||
├─ Manual git comparison → --filter=...[origin/main]
|
||||
└─ See all filter options → references/filtering/RULE.md
|
||||
```
|
||||
|
||||
**`--affected` is the primary way to run only changed packages.** It automatically compares against the default branch and includes dependents.
|
||||
|
||||
### "I want to filter packages"
|
||||
|
||||
```
|
||||
Filter packages?
|
||||
├─ Only changed packages → --affected (see above)
|
||||
├─ By package name → --filter=web
|
||||
├─ By directory → --filter=./apps/*
|
||||
├─ Package + dependencies → --filter=web...
|
||||
├─ Package + dependents → --filter=...web
|
||||
└─ Complex combinations → references/filtering/patterns.md
|
||||
```
|
||||
|
||||
### "Environment variables aren't working"
|
||||
|
||||
```
|
||||
Environment issues?
|
||||
├─ Vars not available at runtime → Strict mode filtering (default)
|
||||
├─ Cache hits with wrong env → Var not in `env` key
|
||||
├─ .env changes not causing rebuilds → .env not in `inputs`
|
||||
├─ CI variables missing → references/environment/gotchas.md
|
||||
└─ Framework vars (NEXT_PUBLIC_*) → Auto-included via inference
|
||||
```
|
||||
|
||||
### "I need to set up CI"
|
||||
|
||||
```
|
||||
CI setup?
|
||||
├─ GitHub Actions → references/ci/github-actions.md
|
||||
├─ Vercel deployment → references/ci/vercel.md
|
||||
├─ Remote cache in CI → references/caching/remote-cache.md
|
||||
├─ Only build changed packages → --affected flag
|
||||
├─ Skip unnecessary builds → turbo-ignore (references/cli/commands.md)
|
||||
└─ Skip container setup when no changes → turbo-ignore
|
||||
```
|
||||
|
||||
### "I want to watch for changes during development"
|
||||
|
||||
```
|
||||
Watch mode?
|
||||
├─ Re-run tasks on change → turbo watch (references/watch/RULE.md)
|
||||
├─ Dev servers with dependencies → Use `with` key (references/configuration/tasks.md#with)
|
||||
├─ Restart dev server on dep change → Use `interruptible: true`
|
||||
└─ Persistent dev tasks → Use `persistent: true`
|
||||
```
|
||||
|
||||
### "I need to create/structure a package"
|
||||
|
||||
```
|
||||
Package creation/structure?
|
||||
├─ Create an internal package → references/best-practices/packages.md
|
||||
├─ Repository structure → references/best-practices/structure.md
|
||||
├─ Dependency management → references/best-practices/dependencies.md
|
||||
├─ Best practices overview → references/best-practices/RULE.md
|
||||
├─ JIT vs Compiled packages → references/best-practices/packages.md#compilation-strategies
|
||||
└─ Sharing code between apps → references/best-practices/RULE.md#package-types
|
||||
```
|
||||
|
||||
### "How should I structure my monorepo?"
|
||||
|
||||
```
|
||||
Monorepo structure?
|
||||
├─ Standard layout (apps/, packages/) → references/best-practices/RULE.md
|
||||
├─ Package types (apps vs libraries) → references/best-practices/RULE.md#package-types
|
||||
├─ Creating internal packages → references/best-practices/packages.md
|
||||
├─ TypeScript configuration → references/best-practices/structure.md#typescript-configuration
|
||||
├─ ESLint configuration → references/best-practices/structure.md#eslint-configuration
|
||||
├─ Dependency management → references/best-practices/dependencies.md
|
||||
└─ Enforce package boundaries → references/boundaries/RULE.md
|
||||
```
|
||||
|
||||
### "I want to enforce architectural boundaries"
|
||||
|
||||
```
|
||||
Enforce boundaries?
|
||||
├─ Check for violations → turbo boundaries
|
||||
├─ Tag packages → references/boundaries/RULE.md#tags
|
||||
├─ Restrict which packages can import others → references/boundaries/RULE.md#rule-types
|
||||
└─ Prevent cross-package file imports → references/boundaries/RULE.md
|
||||
```
|
||||
|
||||
## Critical Anti-Patterns
|
||||
|
||||
### Using `turbo` Shorthand in Code
|
||||
|
||||
**`turbo run` is recommended in package.json scripts and CI pipelines.** The shorthand `turbo <task>` is intended for interactive terminal use.
|
||||
|
||||
```json
|
||||
// WRONG - using shorthand in package.json
|
||||
{
|
||||
"scripts": {
|
||||
"build": "turbo build",
|
||||
"dev": "turbo dev"
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT
|
||||
{
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# WRONG - using shorthand in CI
|
||||
- run: turbo build --affected
|
||||
|
||||
# CORRECT
|
||||
- run: turbo run build --affected
|
||||
```
|
||||
|
||||
### Root Scripts Bypassing Turbo
|
||||
|
||||
Root `package.json` scripts MUST delegate to `turbo run`, not run tasks directly.
|
||||
|
||||
```json
|
||||
// WRONG - bypasses turbo entirely
|
||||
{
|
||||
"scripts": {
|
||||
"build": "bun build",
|
||||
"dev": "bun dev"
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT - delegates to turbo
|
||||
{
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using `&&` to Chain Turbo Tasks
|
||||
|
||||
Don't chain turbo tasks with `&&`. Let turbo orchestrate.
|
||||
|
||||
```json
|
||||
// WRONG - turbo task not using turbo run
|
||||
{
|
||||
"scripts": {
|
||||
"changeset:publish": "bun build && changeset publish"
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT
|
||||
{
|
||||
"scripts": {
|
||||
"changeset:publish": "turbo run build && changeset publish"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `prebuild` Scripts That Manually Build Dependencies
|
||||
|
||||
Scripts like `prebuild` that manually build other packages bypass Turborepo's dependency graph.
|
||||
|
||||
```json
|
||||
// WRONG - manually building dependencies
|
||||
{
|
||||
"scripts": {
|
||||
"prebuild": "cd ../../packages/types && bun run build && cd ../utils && bun run build",
|
||||
"build": "next build"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**However, the fix depends on whether workspace dependencies are declared:**
|
||||
|
||||
1. **If dependencies ARE declared** (e.g., `"@repo/types": "workspace:*"` in package.json), remove the `prebuild` script. Turbo's `dependsOn: ["^build"]` handles this automatically.
|
||||
|
||||
2. **If dependencies are NOT declared**, the `prebuild` exists because `^build` won't trigger without a dependency relationship. The fix is to:
|
||||
- Add the dependency to package.json: `"@repo/types": "workspace:*"`
|
||||
- Then remove the `prebuild` script
|
||||
|
||||
```json
|
||||
// CORRECT - declare dependency, let turbo handle build order
|
||||
// package.json
|
||||
{
|
||||
"dependencies": {
|
||||
"@repo/types": "workspace:*",
|
||||
"@repo/utils": "workspace:*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "next build"
|
||||
}
|
||||
}
|
||||
|
||||
// turbo.json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key insight:** `^build` only runs build in packages listed as dependencies. No dependency declaration = no automatic build ordering.
|
||||
|
||||
### Overly Broad `globalDependencies`
|
||||
|
||||
`globalDependencies` affects ALL tasks in ALL packages. Be specific.
|
||||
|
||||
```json
|
||||
// WRONG - heavy hammer, affects all hashes
|
||||
{
|
||||
"globalDependencies": ["**/.env.*local"]
|
||||
}
|
||||
|
||||
// BETTER - move to task-level inputs
|
||||
{
|
||||
"globalDependencies": [".env"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||
"outputs": ["dist/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Repetitive Task Configuration
|
||||
|
||||
Look for repeated configuration across tasks that can be collapsed. Turborepo supports shared configuration patterns.
|
||||
|
||||
```json
|
||||
// WRONG - repetitive env and inputs across tasks
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"env": ["API_URL", "DATABASE_URL"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"]
|
||||
},
|
||||
"test": {
|
||||
"env": ["API_URL", "DATABASE_URL"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"]
|
||||
},
|
||||
"dev": {
|
||||
"env": ["API_URL", "DATABASE_URL"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BETTER - use globalEnv and globalDependencies for shared config
|
||||
{
|
||||
"globalEnv": ["API_URL", "DATABASE_URL"],
|
||||
"globalDependencies": [".env*"],
|
||||
"tasks": {
|
||||
"build": {},
|
||||
"test": {},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When to use global vs task-level:**
|
||||
|
||||
- `globalEnv` / `globalDependencies` - affects ALL tasks, use for truly shared config
|
||||
- Task-level `env` / `inputs` - use when only specific tasks need it
|
||||
|
||||
### NOT an Anti-Pattern: Large `env` Arrays
|
||||
|
||||
A large `env` array (even 50+ variables) is **not** a problem. It usually means the user was thorough about declaring their build's environment dependencies. Do not flag this as an issue.
|
||||
|
||||
### Using `--parallel` Flag
|
||||
|
||||
The `--parallel` flag bypasses Turborepo's dependency graph. If tasks need parallel execution, configure `dependsOn` correctly instead.
|
||||
|
||||
```bash
|
||||
# WRONG - bypasses dependency graph
|
||||
turbo run lint --parallel
|
||||
|
||||
# CORRECT - configure tasks to allow parallel execution
|
||||
# In turbo.json, set dependsOn appropriately (or use transit nodes)
|
||||
turbo run lint
|
||||
```
|
||||
|
||||
### Package-Specific Task Overrides in Root turbo.json
|
||||
|
||||
When multiple packages need different task configurations, use **Package Configurations** (`turbo.json` in each package) instead of cluttering root `turbo.json` with `package#task` overrides.
|
||||
|
||||
```json
|
||||
// WRONG - root turbo.json with many package-specific overrides
|
||||
{
|
||||
"tasks": {
|
||||
"test": { "dependsOn": ["build"] },
|
||||
"@repo/web#test": { "outputs": ["coverage/**"] },
|
||||
"@repo/api#test": { "outputs": ["coverage/**"] },
|
||||
"@repo/utils#test": { "outputs": [] },
|
||||
"@repo/cli#test": { "outputs": [] },
|
||||
"@repo/core#test": { "outputs": [] }
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT - use Package Configurations
|
||||
// Root turbo.json - base config only
|
||||
{
|
||||
"tasks": {
|
||||
"test": { "dependsOn": ["build"] }
|
||||
}
|
||||
}
|
||||
|
||||
// packages/web/turbo.json - package-specific override
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"test": { "outputs": ["coverage/**"] }
|
||||
}
|
||||
}
|
||||
|
||||
// packages/api/turbo.json
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"test": { "outputs": ["coverage/**"] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits of Package Configurations:**
|
||||
|
||||
- Keeps configuration close to the code it affects
|
||||
- Root turbo.json stays clean and focused on base patterns
|
||||
- Easier to understand what's special about each package
|
||||
- Works with `$TURBO_EXTENDS$` to inherit + extend arrays
|
||||
|
||||
**When to use `package#task` in root:**
|
||||
|
||||
- Single package needs a unique dependency (e.g., `"deploy": { "dependsOn": ["web#build"] }`)
|
||||
- Temporary override while migrating
|
||||
|
||||
See `references/configuration/RULE.md#package-configurations` for full details.
|
||||
|
||||
### Using `../` to Traverse Out of Package in `inputs`
|
||||
|
||||
Don't use relative paths like `../` to reference files outside the package. Use `$TURBO_ROOT$` instead.
|
||||
|
||||
```json
|
||||
// WRONG - traversing out of package
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"inputs": ["$TURBO_DEFAULT$", "../shared-config.json"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT - use $TURBO_ROOT$ for repo root
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/shared-config.json"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Missing `outputs` for File-Producing Tasks
|
||||
|
||||
**Before flagging missing `outputs`, check what the task actually produces:**
|
||||
|
||||
1. Read the package's script (e.g., `"build": "tsc"`, `"test": "vitest"`)
|
||||
2. Determine if it writes files to disk or only outputs to stdout
|
||||
3. Only flag if the task produces files that should be cached
|
||||
|
||||
```json
|
||||
// WRONG: build produces files but they're not cached
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT: build outputs are cached
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Common outputs by framework:
|
||||
|
||||
- Next.js: `[".next/**", "!.next/cache/**"]`
|
||||
- Vite/Rollup: `["dist/**"]`
|
||||
- tsc: `["dist/**"]` or custom `outDir`
|
||||
|
||||
**TypeScript `--noEmit` can still produce cache files:**
|
||||
|
||||
When `incremental: true` in tsconfig.json, `tsc --noEmit` writes `.tsbuildinfo` files even without emitting JS. Check the tsconfig before assuming no outputs:
|
||||
|
||||
```json
|
||||
// If tsconfig has incremental: true, tsc --noEmit produces cache files
|
||||
{
|
||||
"tasks": {
|
||||
"typecheck": {
|
||||
"outputs": ["node_modules/.cache/tsbuildinfo.json"] // or wherever tsBuildInfoFile points
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To determine correct outputs for TypeScript tasks:
|
||||
|
||||
1. Check if `incremental` or `composite` is enabled in tsconfig
|
||||
2. Check `tsBuildInfoFile` for custom cache location (default: alongside `outDir` or in project root)
|
||||
3. If no incremental mode, `tsc --noEmit` produces no files
|
||||
|
||||
### `^build` vs `build` Confusion
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
// ^build = run build in DEPENDENCIES first (other packages this one imports)
|
||||
"build": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
// build (no ^) = run build in SAME PACKAGE first
|
||||
"test": {
|
||||
"dependsOn": ["build"]
|
||||
},
|
||||
// pkg#task = specific package's task
|
||||
"deploy": {
|
||||
"dependsOn": ["web#build"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables Not Hashed
|
||||
|
||||
```json
|
||||
// WRONG: API_URL changes won't cause rebuilds
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"outputs": ["dist/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT: API_URL changes invalidate cache
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"outputs": ["dist/**"],
|
||||
"env": ["API_URL", "API_KEY"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `.env` Files Not in Inputs
|
||||
|
||||
Turbo does NOT load `.env` files - your framework does. But Turbo needs to know about changes:
|
||||
|
||||
```json
|
||||
// WRONG: .env changes don't invalidate cache
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"env": ["API_URL"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT: .env file changes invalidate cache
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"env": ["API_URL"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env", ".env.*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Root `.env` File in Monorepo
|
||||
|
||||
A `.env` file at the repo root is an anti-pattern — even for small monorepos or starter templates. It creates implicit coupling between packages and makes it unclear which packages depend on which variables.
|
||||
|
||||
```
|
||||
// WRONG - root .env affects all packages implicitly
|
||||
my-monorepo/
|
||||
├── .env # Which packages use this?
|
||||
├── apps/
|
||||
│ ├── web/
|
||||
│ └── api/
|
||||
└── packages/
|
||||
|
||||
// CORRECT - .env files in packages that need them
|
||||
my-monorepo/
|
||||
├── apps/
|
||||
│ ├── web/
|
||||
│ │ └── .env # Clear: web needs DATABASE_URL
|
||||
│ └── api/
|
||||
│ └── .env # Clear: api needs API_KEY
|
||||
└── packages/
|
||||
```
|
||||
|
||||
**Problems with root `.env`:**
|
||||
|
||||
- Unclear which packages consume which variables
|
||||
- All packages get all variables (even ones they don't need)
|
||||
- Cache invalidation is coarse-grained (root .env change invalidates everything)
|
||||
- Security risk: packages may accidentally access sensitive vars meant for others
|
||||
- Bad habits start small — starter templates should model correct patterns
|
||||
|
||||
**If you must share variables**, use `globalEnv` to be explicit about what's shared, and document why.
|
||||
|
||||
### Strict Mode Filtering CI Variables
|
||||
|
||||
By default, Turborepo filters environment variables to only those in `env`/`globalEnv`. CI variables may be missing:
|
||||
|
||||
```json
|
||||
// If CI scripts need GITHUB_TOKEN but it's not in env:
|
||||
{
|
||||
"globalPassThroughEnv": ["GITHUB_TOKEN", "CI"],
|
||||
"tasks": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
Or use `--env-mode=loose` (not recommended for production).
|
||||
|
||||
### Shared Code in Apps (Should Be a Package)
|
||||
|
||||
```
|
||||
// WRONG: Shared code inside an app
|
||||
apps/
|
||||
web/
|
||||
shared/ # This breaks monorepo principles!
|
||||
utils.ts
|
||||
|
||||
// CORRECT: Extract to a package
|
||||
packages/
|
||||
utils/
|
||||
src/utils.ts
|
||||
```
|
||||
|
||||
### Accessing Files Across Package Boundaries
|
||||
|
||||
```typescript
|
||||
// WRONG: Reaching into another package's internals
|
||||
import { Button } from "../../packages/ui/src/button";
|
||||
|
||||
// CORRECT: Install and import properly
|
||||
import { Button } from "@repo/ui/button";
|
||||
```
|
||||
|
||||
### Too Many Root Dependencies
|
||||
|
||||
```json
|
||||
// WRONG: App dependencies in root
|
||||
{
|
||||
"dependencies": {
|
||||
"react": "^18",
|
||||
"next": "^14"
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT: Only repo tools in root
|
||||
{
|
||||
"devDependencies": {
|
||||
"turbo": "latest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Task Configurations
|
||||
|
||||
### Standard Build Pipeline
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://turborepo.dev/schema.v2.json",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add a `transit` task if you have tasks that need parallel execution with cache invalidation (see below).
|
||||
|
||||
### Dev Task with `^dev` Pattern (for `turbo watch`)
|
||||
|
||||
A `dev` task with `dependsOn: ["^dev"]` and `persistent: false` in root turbo.json may look unusual but is **correct for `turbo watch` workflows**:
|
||||
|
||||
```json
|
||||
// Root turbo.json
|
||||
{
|
||||
"tasks": {
|
||||
"dev": {
|
||||
"dependsOn": ["^dev"],
|
||||
"cache": false,
|
||||
"persistent": false // Packages have one-shot dev scripts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Package turbo.json (apps/web/turbo.json)
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"dev": {
|
||||
"persistent": true // Apps run long-running dev servers
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
|
||||
- **Packages** (e.g., `@acme/db`, `@acme/validators`) have `"dev": "tsc"` — one-shot type generation that completes quickly
|
||||
- **Apps** override with `persistent: true` for actual dev servers (Next.js, etc.)
|
||||
- **`turbo watch`** re-runs the one-shot package `dev` scripts when source files change, keeping types in sync
|
||||
|
||||
**Intended usage:** Run `turbo watch dev` (not `turbo run dev`). Watch mode re-executes one-shot tasks on file changes while keeping persistent tasks running.
|
||||
|
||||
**Alternative pattern:** Use a separate task name like `prepare` or `generate` for one-shot dependency builds to make the intent clearer:
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"prepare": {
|
||||
"dependsOn": ["^prepare"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"dev": {
|
||||
"dependsOn": ["prepare"],
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Transit Nodes for Parallel Tasks with Cache Invalidation
|
||||
|
||||
Some tasks can run in parallel (don't need built output from dependencies) but must invalidate cache when dependency source code changes.
|
||||
|
||||
**The problem with `dependsOn: ["^taskname"]`:**
|
||||
|
||||
- Forces sequential execution (slow)
|
||||
|
||||
**The problem with `dependsOn: []` (no dependencies):**
|
||||
|
||||
- Allows parallel execution (fast)
|
||||
- But cache is INCORRECT - changing dependency source won't invalidate cache
|
||||
|
||||
**Transit Nodes solve both:**
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"transit": { "dependsOn": ["^transit"] },
|
||||
"my-task": { "dependsOn": ["transit"] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `transit` task creates dependency relationships without matching any actual script, so tasks run in parallel with correct cache invalidation.
|
||||
|
||||
**How to identify tasks that need this pattern:** Look for tasks that read source files from dependencies but don't need their build outputs.
|
||||
|
||||
### With Environment Variables
|
||||
|
||||
```json
|
||||
{
|
||||
"globalEnv": ["NODE_ENV"],
|
||||
"globalDependencies": [".env"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"],
|
||||
"env": ["API_URL", "DATABASE_URL"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Reference Index
|
||||
|
||||
### Configuration
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------------------------------------------------------------------- | -------------------------------------------------------- |
|
||||
| [configuration/RULE.md](./references/configuration/RULE.md) | turbo.json overview, Package Configurations |
|
||||
| [configuration/tasks.md](./references/configuration/tasks.md) | dependsOn, outputs, inputs, env, cache, persistent |
|
||||
| [configuration/global-options.md](./references/configuration/global-options.md) | globalEnv, globalDependencies, cacheDir, daemon, envMode |
|
||||
| [configuration/gotchas.md](./references/configuration/gotchas.md) | Common configuration mistakes |
|
||||
|
||||
### Caching
|
||||
|
||||
| File | Purpose |
|
||||
| --------------------------------------------------------------- | -------------------------------------------- |
|
||||
| [caching/RULE.md](./references/caching/RULE.md) | How caching works, hash inputs |
|
||||
| [caching/remote-cache.md](./references/caching/remote-cache.md) | Vercel Remote Cache, self-hosted, login/link |
|
||||
| [caching/gotchas.md](./references/caching/gotchas.md) | Debugging cache misses, --summarize, --dry |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------------------------------------------------- | ----------------------------------------- |
|
||||
| [environment/RULE.md](./references/environment/RULE.md) | env, globalEnv, passThroughEnv |
|
||||
| [environment/modes.md](./references/environment/modes.md) | Strict vs Loose mode, framework inference |
|
||||
| [environment/gotchas.md](./references/environment/gotchas.md) | .env files, CI issues |
|
||||
|
||||
### Filtering
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------------------------------------------- | ------------------------ |
|
||||
| [filtering/RULE.md](./references/filtering/RULE.md) | --filter syntax overview |
|
||||
| [filtering/patterns.md](./references/filtering/patterns.md) | Common filter patterns |
|
||||
|
||||
### CI/CD
|
||||
|
||||
| File | Purpose |
|
||||
| --------------------------------------------------------- | ------------------------------- |
|
||||
| [ci/RULE.md](./references/ci/RULE.md) | General CI principles |
|
||||
| [ci/github-actions.md](./references/ci/github-actions.md) | Complete GitHub Actions setup |
|
||||
| [ci/vercel.md](./references/ci/vercel.md) | Vercel deployment, turbo-ignore |
|
||||
| [ci/patterns.md](./references/ci/patterns.md) | --affected, caching strategies |
|
||||
|
||||
### CLI
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------------------------------- | --------------------------------------------- |
|
||||
| [cli/RULE.md](./references/cli/RULE.md) | turbo run basics |
|
||||
| [cli/commands.md](./references/cli/commands.md) | turbo run flags, turbo-ignore, other commands |
|
||||
|
||||
### Best Practices
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| [best-practices/RULE.md](./references/best-practices/RULE.md) | Monorepo best practices overview |
|
||||
| [best-practices/structure.md](./references/best-practices/structure.md) | Repository structure, workspace config, TypeScript/ESLint setup |
|
||||
| [best-practices/packages.md](./references/best-practices/packages.md) | Creating internal packages, JIT vs Compiled, exports |
|
||||
| [best-practices/dependencies.md](./references/best-practices/dependencies.md) | Dependency management, installing, version sync |
|
||||
|
||||
### Watch Mode
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------------------------------- | ----------------------------------------------- |
|
||||
| [watch/RULE.md](./references/watch/RULE.md) | turbo watch, interruptible tasks, dev workflows |
|
||||
|
||||
### Boundaries (Experimental)
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------------------------------------- | ----------------------------------------------------- |
|
||||
| [boundaries/RULE.md](./references/boundaries/RULE.md) | Enforce package isolation, tag-based dependency rules |
|
||||
|
||||
## Source Documentation
|
||||
|
||||
This skill is based on the official Turborepo documentation at:
|
||||
|
||||
- Source: `apps/docs/content/docs/` in the Turborepo repository
|
||||
- Live: https://turborepo.dev/docs
|
||||
70
.agents/skills/turborepo/command/turborepo.md
Normal file
70
.agents/skills/turborepo/command/turborepo.md
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
description: Load Turborepo skill for creating workflows, tasks, and pipelines in monorepos. Use when users ask to "create a workflow", "make a task", "generate a pipeline", or set up build orchestration.
|
||||
---
|
||||
|
||||
Load the Turborepo skill and help with monorepo task orchestration: creating workflows, configuring tasks, setting up pipelines, and optimizing builds.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Load turborepo skill
|
||||
|
||||
```
|
||||
skill({ name: 'turborepo' })
|
||||
```
|
||||
|
||||
### Step 2: Identify task type from user request
|
||||
|
||||
Analyze $ARGUMENTS to determine:
|
||||
|
||||
- **Topic**: configuration, caching, filtering, environment, CI, or CLI
|
||||
- **Task type**: new setup, debugging, optimization, or implementation
|
||||
|
||||
Use decision trees in SKILL.md to select the relevant reference files.
|
||||
|
||||
### Step 3: Read relevant reference files
|
||||
|
||||
Based on task type, read from `references/<topic>/`:
|
||||
|
||||
| Task | Files to Read |
|
||||
| -------------------- | ------------------------------------------------------- |
|
||||
| Configure turbo.json | `configuration/RULE.md` + `configuration/tasks.md` |
|
||||
| Debug cache issues | `caching/gotchas.md` |
|
||||
| Set up remote cache | `caching/remote-cache.md` |
|
||||
| Filter packages | `filtering/RULE.md` + `filtering/patterns.md` |
|
||||
| Environment problems | `environment/gotchas.md` + `environment/modes.md` |
|
||||
| Set up CI | `ci/RULE.md` + `ci/github-actions.md` or `ci/vercel.md` |
|
||||
| CLI usage | `cli/commands.md` |
|
||||
|
||||
### Step 4: Execute task
|
||||
|
||||
Apply Turborepo-specific patterns from references to complete the user's request.
|
||||
|
||||
**CRITICAL - When creating tasks/scripts/pipelines:**
|
||||
|
||||
1. **DO NOT create Root Tasks** - Always create package tasks
|
||||
2. Add scripts to each relevant package's `package.json` (e.g., `apps/web/package.json`, `packages/ui/package.json`)
|
||||
3. Register the task in root `turbo.json`
|
||||
4. Root `package.json` only contains `turbo run <task>` - never actual task logic
|
||||
|
||||
**Other things to verify:**
|
||||
|
||||
- `outputs` defined for cacheable tasks
|
||||
- `dependsOn` uses correct syntax (`^task` vs `task`)
|
||||
- Environment variables in `env` key
|
||||
- `.env` files in `inputs` if used
|
||||
- Use `turbo run` (not `turbo`) in package.json and CI
|
||||
|
||||
### Step 5: Summarize
|
||||
|
||||
```
|
||||
=== Turborepo Task Complete ===
|
||||
|
||||
Topic: <configuration|caching|filtering|environment|ci|cli>
|
||||
Files referenced: <reference files consulted>
|
||||
|
||||
<brief summary of what was done>
|
||||
```
|
||||
|
||||
<user-request>
|
||||
$ARGUMENTS
|
||||
</user-request>
|
||||
241
.agents/skills/turborepo/references/best-practices/RULE.md
Normal file
241
.agents/skills/turborepo/references/best-practices/RULE.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# Monorepo Best Practices
|
||||
|
||||
Essential patterns for structuring and maintaining a healthy Turborepo monorepo.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
### Standard Layout
|
||||
|
||||
```
|
||||
my-monorepo/
|
||||
├── apps/ # Application packages (deployable)
|
||||
│ ├── web/
|
||||
│ ├── docs/
|
||||
│ └── api/
|
||||
├── packages/ # Library packages (shared code)
|
||||
│ ├── ui/
|
||||
│ ├── utils/
|
||||
│ └── config-*/ # Shared configs (eslint, typescript, etc.)
|
||||
├── package.json # Root package.json (minimal deps)
|
||||
├── turbo.json # Turborepo configuration
|
||||
├── pnpm-workspace.yaml # (pnpm) or workspaces in package.json
|
||||
└── pnpm-lock.yaml # Lockfile (required)
|
||||
```
|
||||
|
||||
### Key Principles
|
||||
|
||||
1. **`apps/` for deployables**: Next.js sites, APIs, CLIs - things that get deployed
|
||||
2. **`packages/` for libraries**: Shared code consumed by apps or other packages
|
||||
3. **One purpose per package**: Each package should do one thing well
|
||||
4. **No nested packages**: Don't put packages inside packages
|
||||
|
||||
## Package Types
|
||||
|
||||
### Application Packages (`apps/`)
|
||||
|
||||
- **Deployable**: These are the "endpoints" of your package graph
|
||||
- **Not installed by other packages**: Apps shouldn't be dependencies of other packages
|
||||
- **No shared code**: If code needs sharing, extract to `packages/`
|
||||
|
||||
```json
|
||||
// apps/web/package.json
|
||||
{
|
||||
"name": "web",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@repo/ui": "workspace:*",
|
||||
"next": "latest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Library Packages (`packages/`)
|
||||
|
||||
- **Shared code**: Utilities, components, configs
|
||||
- **Namespaced names**: Use `@repo/` or `@yourorg/` prefix
|
||||
- **Clear exports**: Define what the package exposes
|
||||
|
||||
```json
|
||||
// packages/ui/package.json
|
||||
{
|
||||
"name": "@repo/ui",
|
||||
"exports": {
|
||||
"./button": "./src/button.tsx",
|
||||
"./card": "./src/card.tsx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Package Compilation Strategies
|
||||
|
||||
### Just-in-Time (Simplest)
|
||||
|
||||
Export TypeScript directly; let the app's bundler compile it.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/ui",
|
||||
"exports": {
|
||||
"./button": "./src/button.tsx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pros**: Zero build config, instant changes
|
||||
**Cons**: Can't cache builds, requires app bundler support
|
||||
|
||||
### Compiled (Recommended for Libraries)
|
||||
|
||||
Package compiles itself with `tsc` or bundler.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/ui",
|
||||
"exports": {
|
||||
"./button": {
|
||||
"types": "./src/button.tsx",
|
||||
"default": "./dist/button.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pros**: Cacheable by Turborepo, works everywhere
|
||||
**Cons**: More configuration
|
||||
|
||||
## Dependency Management
|
||||
|
||||
### Install Where Used
|
||||
|
||||
Install dependencies in the package that uses them, not the root.
|
||||
|
||||
```bash
|
||||
# Good: Install in the package that needs it
|
||||
pnpm add lodash --filter=@repo/utils
|
||||
|
||||
# Avoid: Installing everything at root
|
||||
pnpm add lodash -w # Only for repo-level tools
|
||||
```
|
||||
|
||||
### Root Dependencies
|
||||
|
||||
Only these belong in root `package.json`:
|
||||
|
||||
- `turbo` - The build system
|
||||
- `husky`, `lint-staged` - Git hooks
|
||||
- Repository-level tooling
|
||||
|
||||
### Internal Dependencies
|
||||
|
||||
Use workspace protocol for internal packages:
|
||||
|
||||
```json
|
||||
// pnpm/bun
|
||||
{ "@repo/ui": "workspace:*" }
|
||||
|
||||
// npm/yarn
|
||||
{ "@repo/ui": "*" }
|
||||
```
|
||||
|
||||
## Exports Best Practices
|
||||
|
||||
### Use `exports` Field (Not `main`)
|
||||
|
||||
```json
|
||||
{
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./button": "./src/button.tsx",
|
||||
"./utils": "./src/utils.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid Barrel Files
|
||||
|
||||
Don't create `index.ts` files that re-export everything:
|
||||
|
||||
```typescript
|
||||
// BAD: packages/ui/src/index.ts
|
||||
export * from './button';
|
||||
export * from './card';
|
||||
export * from './modal';
|
||||
// ... imports everything even if you need one thing
|
||||
|
||||
// GOOD: Direct exports in package.json
|
||||
{
|
||||
"exports": {
|
||||
"./button": "./src/button.tsx",
|
||||
"./card": "./src/card.tsx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Namespace Your Packages
|
||||
|
||||
```json
|
||||
// Good
|
||||
{ "name": "@repo/ui" }
|
||||
{ "name": "@acme/utils" }
|
||||
|
||||
// Avoid (conflicts with npm registry)
|
||||
{ "name": "ui" }
|
||||
{ "name": "utils" }
|
||||
```
|
||||
|
||||
## Common Anti-Patterns
|
||||
|
||||
### Accessing Files Across Package Boundaries
|
||||
|
||||
```typescript
|
||||
// BAD: Reaching into another package
|
||||
import { Button } from "../../packages/ui/src/button";
|
||||
|
||||
// GOOD: Install and import properly
|
||||
import { Button } from "@repo/ui/button";
|
||||
```
|
||||
|
||||
### Shared Code in Apps
|
||||
|
||||
```
|
||||
// BAD
|
||||
apps/
|
||||
web/
|
||||
shared/ # This should be a package!
|
||||
utils.ts
|
||||
|
||||
// GOOD
|
||||
packages/
|
||||
utils/ # Proper shared package
|
||||
src/utils.ts
|
||||
```
|
||||
|
||||
### Too Many Root Dependencies
|
||||
|
||||
```json
|
||||
// BAD: Root has app dependencies
|
||||
{
|
||||
"dependencies": {
|
||||
"react": "^18",
|
||||
"next": "^14",
|
||||
"lodash": "^4"
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Root only has repo tools
|
||||
{
|
||||
"devDependencies": {
|
||||
"turbo": "latest",
|
||||
"husky": "latest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [structure.md](./structure.md) - Detailed repository structure patterns
|
||||
- [packages.md](./packages.md) - Creating and managing internal packages
|
||||
- [dependencies.md](./dependencies.md) - Dependency management strategies
|
||||
@@ -0,0 +1,246 @@
|
||||
# Dependency Management
|
||||
|
||||
Best practices for managing dependencies in a Turborepo monorepo.
|
||||
|
||||
## Core Principle: Install Where Used
|
||||
|
||||
Dependencies belong in the package that uses them, not the root.
|
||||
|
||||
```bash
|
||||
# Good: Install in specific package
|
||||
pnpm add react --filter=@repo/ui
|
||||
pnpm add next --filter=web
|
||||
|
||||
# Avoid: Installing in root
|
||||
pnpm add react -w # Only for repo-level tools!
|
||||
```
|
||||
|
||||
## Benefits of Local Installation
|
||||
|
||||
### 1. Clarity
|
||||
|
||||
Each package's `package.json` lists exactly what it needs:
|
||||
|
||||
```json
|
||||
// packages/ui/package.json
|
||||
{
|
||||
"dependencies": {
|
||||
"react": "^18.0.0",
|
||||
"class-variance-authority": "^0.7.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Flexibility
|
||||
|
||||
Different packages can use different versions when needed:
|
||||
|
||||
```json
|
||||
// packages/legacy-ui/package.json
|
||||
{ "dependencies": { "react": "^17.0.0" } }
|
||||
|
||||
// packages/ui/package.json
|
||||
{ "dependencies": { "react": "^18.0.0" } }
|
||||
```
|
||||
|
||||
### 3. Better Caching
|
||||
|
||||
Installing in root changes workspace lockfile, invalidating all caches.
|
||||
|
||||
### 4. Pruning Support
|
||||
|
||||
`turbo prune` can remove unused dependencies for Docker images.
|
||||
|
||||
## What Belongs in Root
|
||||
|
||||
Only repository-level tools:
|
||||
|
||||
```json
|
||||
// Root package.json
|
||||
{
|
||||
"devDependencies": {
|
||||
"turbo": "latest",
|
||||
"husky": "^8.0.0",
|
||||
"lint-staged": "^15.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**NOT** application dependencies:
|
||||
|
||||
- react, next, express
|
||||
- lodash, axios, zod
|
||||
- Testing libraries (unless truly repo-wide)
|
||||
|
||||
## Installing Dependencies
|
||||
|
||||
### Single Package
|
||||
|
||||
```bash
|
||||
# pnpm
|
||||
pnpm add lodash --filter=@repo/utils
|
||||
|
||||
# npm
|
||||
npm install lodash --workspace=@repo/utils
|
||||
|
||||
# yarn
|
||||
yarn workspace @repo/utils add lodash
|
||||
|
||||
# bun
|
||||
cd packages/utils && bun add lodash
|
||||
```
|
||||
|
||||
### Multiple Packages
|
||||
|
||||
```bash
|
||||
# pnpm
|
||||
pnpm add jest --save-dev --filter=web --filter=@repo/ui
|
||||
|
||||
# npm
|
||||
npm install jest --save-dev --workspace=web --workspace=@repo/ui
|
||||
|
||||
# yarn (v2+)
|
||||
yarn workspaces foreach -R --from '{web,@repo/ui}' add jest --dev
|
||||
```
|
||||
|
||||
### Internal Packages
|
||||
|
||||
```bash
|
||||
# pnpm
|
||||
pnpm add @repo/ui --filter=web
|
||||
|
||||
# This updates package.json:
|
||||
{
|
||||
"dependencies": {
|
||||
"@repo/ui": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Keeping Versions in Sync
|
||||
|
||||
### Option 1: Tooling
|
||||
|
||||
```bash
|
||||
# syncpack - Check and fix version mismatches
|
||||
npx syncpack list-mismatches
|
||||
npx syncpack fix-mismatches
|
||||
|
||||
# manypkg - Similar functionality
|
||||
npx @manypkg/cli check
|
||||
npx @manypkg/cli fix
|
||||
|
||||
# sherif - Rust-based, very fast
|
||||
npx sherif
|
||||
```
|
||||
|
||||
### Option 2: Package Manager Commands
|
||||
|
||||
```bash
|
||||
# pnpm - Update everywhere
|
||||
pnpm up --recursive typescript@latest
|
||||
|
||||
# npm - Update in all workspaces
|
||||
npm install typescript@latest --workspaces
|
||||
```
|
||||
|
||||
### Option 3: pnpm Catalogs (pnpm 9.5+)
|
||||
|
||||
```yaml
|
||||
# pnpm-workspace.yaml
|
||||
packages:
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
|
||||
catalog:
|
||||
react: ^18.2.0
|
||||
typescript: ^5.3.0
|
||||
```
|
||||
|
||||
```json
|
||||
// Any package.json
|
||||
{
|
||||
"dependencies": {
|
||||
"react": "catalog:" // Uses version from catalog
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Internal vs External Dependencies
|
||||
|
||||
### Internal (Workspace)
|
||||
|
||||
```json
|
||||
// pnpm/bun
|
||||
{ "@repo/ui": "workspace:*" }
|
||||
|
||||
// npm/yarn
|
||||
{ "@repo/ui": "*" }
|
||||
```
|
||||
|
||||
Turborepo understands these relationships and orders builds accordingly.
|
||||
|
||||
### External (npm Registry)
|
||||
|
||||
```json
|
||||
{ "lodash": "^4.17.21" }
|
||||
```
|
||||
|
||||
Standard semver versioning from npm.
|
||||
|
||||
## Peer Dependencies
|
||||
|
||||
For library packages that expect the consumer to provide dependencies:
|
||||
|
||||
```json
|
||||
// packages/ui/package.json
|
||||
{
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"react": "^18.0.0", // For development/testing
|
||||
"react-dom": "^18.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### "Module not found"
|
||||
|
||||
1. Check the dependency is installed in the right package
|
||||
2. Run `pnpm install` / `npm install` to update lockfile
|
||||
3. Check exports are defined in the package
|
||||
|
||||
### Version Conflicts
|
||||
|
||||
Packages can use different versions - this is a feature, not a bug. But if you need consistency:
|
||||
|
||||
1. Use tooling (syncpack, manypkg)
|
||||
2. Use pnpm catalogs
|
||||
3. Create a lint rule
|
||||
|
||||
### Hoisting Issues
|
||||
|
||||
Some tools expect dependencies in specific locations. Use package manager config:
|
||||
|
||||
```yaml
|
||||
# .npmrc (pnpm)
|
||||
public-hoist-pattern[]=*eslint*
|
||||
public-hoist-pattern[]=*prettier*
|
||||
```
|
||||
|
||||
## Lockfile
|
||||
|
||||
**Required** for:
|
||||
|
||||
- Reproducible builds
|
||||
- Turborepo dependency analysis
|
||||
- Cache correctness
|
||||
|
||||
```bash
|
||||
# Commit your lockfile!
|
||||
git add pnpm-lock.yaml # or package-lock.json, yarn.lock
|
||||
```
|
||||
335
.agents/skills/turborepo/references/best-practices/packages.md
Normal file
335
.agents/skills/turborepo/references/best-practices/packages.md
Normal file
@@ -0,0 +1,335 @@
|
||||
# Creating Internal Packages
|
||||
|
||||
How to create and structure internal packages in your monorepo.
|
||||
|
||||
## Package Creation Checklist
|
||||
|
||||
1. Create directory in `packages/`
|
||||
2. Add `package.json` with name and exports
|
||||
3. Add source code in `src/`
|
||||
4. Add `tsconfig.json` if using TypeScript
|
||||
5. Install as dependency in consuming packages
|
||||
6. Run package manager install to update lockfile
|
||||
|
||||
## Package Compilation Strategies
|
||||
|
||||
### Just-in-Time (JIT)
|
||||
|
||||
Export TypeScript directly. The consuming app's bundler compiles it.
|
||||
|
||||
```json
|
||||
// packages/ui/package.json
|
||||
{
|
||||
"name": "@repo/ui",
|
||||
"exports": {
|
||||
"./button": "./src/button.tsx",
|
||||
"./card": "./src/card.tsx"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"check-types": "tsc --noEmit"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Apps use modern bundlers (Turbopack, webpack, Vite)
|
||||
- You want minimal configuration
|
||||
- Build times are acceptable without caching
|
||||
|
||||
**Limitations:**
|
||||
|
||||
- No Turborepo cache for the package itself
|
||||
- Consumer must support TypeScript compilation
|
||||
- Can't use TypeScript `paths` (use Node.js subpath imports instead)
|
||||
|
||||
### Compiled
|
||||
|
||||
Package handles its own compilation.
|
||||
|
||||
```json
|
||||
// packages/ui/package.json
|
||||
{
|
||||
"name": "@repo/ui",
|
||||
"exports": {
|
||||
"./button": {
|
||||
"types": "./src/button.tsx",
|
||||
"default": "./dist/button.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// packages/ui/tsconfig.json
|
||||
{
|
||||
"extends": "@repo/typescript-config/library.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
|
||||
- You want Turborepo to cache builds
|
||||
- Package will be used by non-bundler tools
|
||||
- You need maximum compatibility
|
||||
|
||||
**Remember:** Add `dist/**` to turbo.json outputs!
|
||||
|
||||
## Defining Exports
|
||||
|
||||
### Multiple Entrypoints
|
||||
|
||||
```json
|
||||
{
|
||||
"exports": {
|
||||
".": "./src/index.ts", // @repo/ui
|
||||
"./button": "./src/button.tsx", // @repo/ui/button
|
||||
"./card": "./src/card.tsx", // @repo/ui/card
|
||||
"./hooks": "./src/hooks/index.ts" // @repo/ui/hooks
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Conditional Exports (Compiled)
|
||||
|
||||
```json
|
||||
{
|
||||
"exports": {
|
||||
"./button": {
|
||||
"types": "./src/button.tsx",
|
||||
"import": "./dist/button.mjs",
|
||||
"require": "./dist/button.cjs",
|
||||
"default": "./dist/button.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Installing Internal Packages
|
||||
|
||||
### Add to Consuming Package
|
||||
|
||||
```json
|
||||
// apps/web/package.json
|
||||
{
|
||||
"dependencies": {
|
||||
"@repo/ui": "workspace:*" // pnpm/bun
|
||||
// "@repo/ui": "*" // npm/yarn
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Run Install
|
||||
|
||||
```bash
|
||||
pnpm install # Updates lockfile with new dependency
|
||||
```
|
||||
|
||||
### Import and Use
|
||||
|
||||
```typescript
|
||||
// apps/web/src/page.tsx
|
||||
import { Button } from '@repo/ui/button';
|
||||
|
||||
export default function Page() {
|
||||
return <Button>Click me</Button>;
|
||||
}
|
||||
```
|
||||
|
||||
## One Purpose Per Package
|
||||
|
||||
### Good Examples
|
||||
|
||||
```
|
||||
packages/
|
||||
├── ui/ # Shared UI components
|
||||
├── utils/ # General utilities
|
||||
├── auth/ # Authentication logic
|
||||
├── database/ # Database client/schemas
|
||||
├── eslint-config/ # ESLint configuration
|
||||
├── typescript-config/ # TypeScript configuration
|
||||
└── api-client/ # Generated API client
|
||||
```
|
||||
|
||||
### Avoid Mega-Packages
|
||||
|
||||
```
|
||||
// BAD: One package for everything
|
||||
packages/
|
||||
└── shared/
|
||||
├── components/
|
||||
├── utils/
|
||||
├── hooks/
|
||||
├── types/
|
||||
└── api/
|
||||
|
||||
// GOOD: Separate by purpose
|
||||
packages/
|
||||
├── ui/ # Components
|
||||
├── utils/ # Utilities
|
||||
├── hooks/ # React hooks
|
||||
├── types/ # Shared TypeScript types
|
||||
└── api-client/ # API utilities
|
||||
```
|
||||
|
||||
## Config Packages
|
||||
|
||||
### TypeScript Config
|
||||
|
||||
```json
|
||||
// packages/typescript-config/package.json
|
||||
{
|
||||
"name": "@repo/typescript-config",
|
||||
"exports": {
|
||||
"./base.json": "./base.json",
|
||||
"./nextjs.json": "./nextjs.json",
|
||||
"./library.json": "./library.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ESLint Config
|
||||
|
||||
```json
|
||||
// packages/eslint-config/package.json
|
||||
{
|
||||
"name": "@repo/eslint-config",
|
||||
"exports": {
|
||||
"./base": "./base.js",
|
||||
"./next": "./next.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint": "^8.0.0",
|
||||
"eslint-config-next": "latest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### Forgetting to Export
|
||||
|
||||
```json
|
||||
// BAD: No exports defined
|
||||
{
|
||||
"name": "@repo/ui"
|
||||
}
|
||||
|
||||
// GOOD: Clear exports
|
||||
{
|
||||
"name": "@repo/ui",
|
||||
"exports": {
|
||||
"./button": "./src/button.tsx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Wrong Workspace Syntax
|
||||
|
||||
```json
|
||||
// pnpm/bun
|
||||
{ "@repo/ui": "workspace:*" } // Correct
|
||||
|
||||
// npm/yarn
|
||||
{ "@repo/ui": "*" } // Correct
|
||||
{ "@repo/ui": "workspace:*" } // Wrong for npm/yarn!
|
||||
```
|
||||
|
||||
### Missing from turbo.json Outputs
|
||||
|
||||
```json
|
||||
// Package builds to dist/, but turbo.json doesn't know
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"outputs": [".next/**"] // Missing dist/**!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Correct
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"outputs": [".next/**", "dist/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Best Practices
|
||||
|
||||
### Use Node.js Subpath Imports (Not `paths`)
|
||||
|
||||
TypeScript `compilerOptions.paths` breaks with JIT packages. Use Node.js subpath imports instead (TypeScript 5.4+).
|
||||
|
||||
**JIT Package:**
|
||||
|
||||
```json
|
||||
// packages/ui/package.json
|
||||
{
|
||||
"imports": {
|
||||
"#*": "./src/*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// packages/ui/button.tsx
|
||||
import { MY_STRING } from "#utils.ts"; // Uses .ts extension
|
||||
```
|
||||
|
||||
**Compiled Package:**
|
||||
|
||||
```json
|
||||
// packages/ui/package.json
|
||||
{
|
||||
"imports": {
|
||||
"#*": "./dist/*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// packages/ui/button.tsx
|
||||
import { MY_STRING } from "#utils.js"; // Uses .js extension
|
||||
```
|
||||
|
||||
### Use `tsc` for Internal Packages
|
||||
|
||||
For internal packages, prefer `tsc` over bundlers. Bundlers can mangle code before it reaches your app's bundler, causing hard-to-debug issues.
|
||||
|
||||
### Enable Go-to-Definition
|
||||
|
||||
For Compiled Packages, enable declaration maps:
|
||||
|
||||
```json
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This creates `.d.ts` and `.d.ts.map` files for IDE navigation.
|
||||
|
||||
### No Root tsconfig.json Needed
|
||||
|
||||
Each package should have its own `tsconfig.json`. A root one causes all tasks to miss cache when changed. Only use root `tsconfig.json` for non-package scripts.
|
||||
|
||||
### Avoid TypeScript Project References
|
||||
|
||||
They add complexity and another caching layer. Turborepo handles dependencies better.
|
||||
270
.agents/skills/turborepo/references/best-practices/structure.md
Normal file
270
.agents/skills/turborepo/references/best-practices/structure.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# Repository Structure
|
||||
|
||||
Detailed guidance on structuring a Turborepo monorepo.
|
||||
|
||||
## Workspace Configuration
|
||||
|
||||
### pnpm (Recommended)
|
||||
|
||||
```yaml
|
||||
# pnpm-workspace.yaml
|
||||
packages:
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
```
|
||||
|
||||
### npm/yarn/bun
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"workspaces": ["apps/*", "packages/*"]
|
||||
}
|
||||
```
|
||||
|
||||
## Root package.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-monorepo",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.0.0",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"lint": "turbo run lint",
|
||||
"test": "turbo run test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"turbo": "latest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
- `private: true` - Prevents accidental publishing
|
||||
- `packageManager` - Enforces consistent package manager version
|
||||
- **Scripts only delegate to `turbo run`** - No actual build logic here!
|
||||
- Minimal devDependencies (just turbo and repo tools)
|
||||
|
||||
## Always Prefer Package Tasks
|
||||
|
||||
**Always use package tasks. Only use Root Tasks if you cannot succeed with package tasks.**
|
||||
|
||||
```json
|
||||
// packages/web/package.json
|
||||
{
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"lint": "eslint .",
|
||||
"test": "vitest",
|
||||
"typecheck": "tsc --noEmit"
|
||||
}
|
||||
}
|
||||
|
||||
// packages/api/package.json
|
||||
{
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"lint": "eslint .",
|
||||
"test": "vitest",
|
||||
"typecheck": "tsc --noEmit"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Package tasks enable Turborepo to:
|
||||
|
||||
1. **Parallelize** - Run `web#lint` and `api#lint` simultaneously
|
||||
2. **Cache individually** - Each package's task output is cached separately
|
||||
3. **Filter precisely** - Run `turbo run test --filter=web` for just one package
|
||||
|
||||
**Root Tasks are a fallback** for tasks that truly cannot run per-package:
|
||||
|
||||
```json
|
||||
// AVOID unless necessary - sequential, not parallelized, can't filter
|
||||
{
|
||||
"scripts": {
|
||||
"lint": "eslint apps/web && eslint apps/api && eslint packages/ui"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Root turbo.json
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://turborepo.dev/schema.v2.json",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
|
||||
},
|
||||
"lint": {},
|
||||
"test": {
|
||||
"dependsOn": ["build"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Directory Organization
|
||||
|
||||
### Grouping Packages
|
||||
|
||||
You can group packages by adding more workspace paths:
|
||||
|
||||
```yaml
|
||||
# pnpm-workspace.yaml
|
||||
packages:
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
- "packages/config/*" # Grouped configs
|
||||
- "packages/features/*" # Feature packages
|
||||
```
|
||||
|
||||
This allows:
|
||||
|
||||
```
|
||||
packages/
|
||||
├── ui/
|
||||
├── utils/
|
||||
├── config/
|
||||
│ ├── eslint/
|
||||
│ ├── typescript/
|
||||
│ └── tailwind/
|
||||
└── features/
|
||||
├── auth/
|
||||
└── payments/
|
||||
```
|
||||
|
||||
### What NOT to Do
|
||||
|
||||
```yaml
|
||||
# BAD: Nested wildcards cause ambiguous behavior
|
||||
packages:
|
||||
- "packages/**" # Don't do this!
|
||||
```
|
||||
|
||||
## Package Anatomy
|
||||
|
||||
### Minimum Required Files
|
||||
|
||||
```
|
||||
packages/ui/
|
||||
├── package.json # Required: Makes it a package
|
||||
├── src/ # Source code
|
||||
│ └── button.tsx
|
||||
└── tsconfig.json # TypeScript config (if using TS)
|
||||
```
|
||||
|
||||
### package.json Requirements
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/ui", // Unique, namespaced name
|
||||
"version": "0.0.0", // Version (can be 0.0.0 for internal)
|
||||
"private": true, // Prevents accidental publishing
|
||||
"exports": {
|
||||
// Entry points
|
||||
"./button": "./src/button.tsx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Configuration
|
||||
|
||||
### Shared Base Config
|
||||
|
||||
Create a shared TypeScript config package:
|
||||
|
||||
```
|
||||
packages/
|
||||
└── typescript-config/
|
||||
├── package.json
|
||||
├── base.json
|
||||
├── nextjs.json
|
||||
└── library.json
|
||||
```
|
||||
|
||||
```json
|
||||
// packages/typescript-config/base.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"module": "ESNext",
|
||||
"target": "ES2022"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Extending in Packages
|
||||
|
||||
```json
|
||||
// packages/ui/tsconfig.json
|
||||
{
|
||||
"extends": "@repo/typescript-config/library.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
### No Root tsconfig.json
|
||||
|
||||
You likely don't need a `tsconfig.json` in the workspace root. Each package should have its own config extending from the shared config package.
|
||||
|
||||
## ESLint Configuration
|
||||
|
||||
### Shared Config Package
|
||||
|
||||
```
|
||||
packages/
|
||||
└── eslint-config/
|
||||
├── package.json
|
||||
├── base.js
|
||||
├── next.js
|
||||
└── library.js
|
||||
```
|
||||
|
||||
```json
|
||||
// packages/eslint-config/package.json
|
||||
{
|
||||
"name": "@repo/eslint-config",
|
||||
"exports": {
|
||||
"./base": "./base.js",
|
||||
"./next": "./next.js",
|
||||
"./library": "./library.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using in Packages
|
||||
|
||||
```js
|
||||
// apps/web/.eslintrc.js
|
||||
module.exports = {
|
||||
extends: ["@repo/eslint-config/next"],
|
||||
};
|
||||
```
|
||||
|
||||
## Lockfile
|
||||
|
||||
A lockfile is **required** for:
|
||||
|
||||
- Reproducible builds
|
||||
- Turborepo to understand package dependencies
|
||||
- Cache correctness
|
||||
|
||||
Without a lockfile, you'll see unpredictable behavior.
|
||||
126
.agents/skills/turborepo/references/boundaries/RULE.md
Normal file
126
.agents/skills/turborepo/references/boundaries/RULE.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Boundaries
|
||||
|
||||
**Experimental feature** - See [RFC](https://github.com/vercel/turborepo/discussions/9435)
|
||||
|
||||
Full docs: https://turborepo.dev/docs/reference/boundaries
|
||||
|
||||
Boundaries enforce package isolation by detecting:
|
||||
|
||||
1. Imports of files outside the package's directory
|
||||
2. Imports of packages not declared in `package.json` dependencies
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
turbo boundaries
|
||||
```
|
||||
|
||||
Run this to check for workspace violations across your monorepo.
|
||||
|
||||
## Tags
|
||||
|
||||
Tags allow you to create rules for which packages can depend on each other.
|
||||
|
||||
### Adding Tags to a Package
|
||||
|
||||
```json
|
||||
// packages/ui/turbo.json
|
||||
{
|
||||
"tags": ["internal"]
|
||||
}
|
||||
```
|
||||
|
||||
### Configuring Tag Rules
|
||||
|
||||
Rules go in root `turbo.json`:
|
||||
|
||||
```json
|
||||
// turbo.json
|
||||
{
|
||||
"boundaries": {
|
||||
"tags": {
|
||||
"public": {
|
||||
"dependencies": {
|
||||
"deny": ["internal"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This prevents `public`-tagged packages from importing `internal`-tagged packages.
|
||||
|
||||
### Rule Types
|
||||
|
||||
**Allow-list approach** (only allow specific tags):
|
||||
|
||||
```json
|
||||
{
|
||||
"boundaries": {
|
||||
"tags": {
|
||||
"public": {
|
||||
"dependencies": {
|
||||
"allow": ["public"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Deny-list approach** (block specific tags):
|
||||
|
||||
```json
|
||||
{
|
||||
"boundaries": {
|
||||
"tags": {
|
||||
"public": {
|
||||
"dependencies": {
|
||||
"deny": ["internal"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Restrict dependents** (who can import this package):
|
||||
|
||||
```json
|
||||
{
|
||||
"boundaries": {
|
||||
"tags": {
|
||||
"private": {
|
||||
"dependents": {
|
||||
"deny": ["public"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Package Names
|
||||
|
||||
Package names work in place of tags:
|
||||
|
||||
```json
|
||||
{
|
||||
"boundaries": {
|
||||
"tags": {
|
||||
"private": {
|
||||
"dependents": {
|
||||
"deny": ["@repo/my-pkg"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Rules apply transitively (dependencies of dependencies)
|
||||
- Helps enforce architectural boundaries at scale
|
||||
- Catches violations before runtime/build errors
|
||||
107
.agents/skills/turborepo/references/caching/RULE.md
Normal file
107
.agents/skills/turborepo/references/caching/RULE.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# How Turborepo Caching Works
|
||||
|
||||
Turborepo's core principle: **never do the same work twice**.
|
||||
|
||||
## The Cache Equation
|
||||
|
||||
```
|
||||
fingerprint(inputs) → stored outputs
|
||||
```
|
||||
|
||||
If inputs haven't changed, restore outputs from cache instead of re-running the task.
|
||||
|
||||
## What Determines the Cache Key
|
||||
|
||||
### Global Hash Inputs
|
||||
|
||||
These affect ALL tasks in the repo:
|
||||
|
||||
- `package-lock.json` / `yarn.lock` / `pnpm-lock.yaml`
|
||||
- Files listed in `globalDependencies`
|
||||
- Environment variables in `globalEnv`
|
||||
- `turbo.json` configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"globalDependencies": [".env", "tsconfig.base.json"],
|
||||
"globalEnv": ["CI", "NODE_ENV"]
|
||||
}
|
||||
```
|
||||
|
||||
### Task Hash Inputs
|
||||
|
||||
These affect specific tasks:
|
||||
|
||||
- All files in the package (unless filtered by `inputs`)
|
||||
- `package.json` contents
|
||||
- Environment variables in task's `env` key
|
||||
- Task configuration (command, outputs, dependencies)
|
||||
- Hashes of dependent tasks (`dependsOn`)
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["src/**", "package.json", "tsconfig.json"],
|
||||
"env": ["API_URL"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## What Gets Cached
|
||||
|
||||
1. **File outputs** - files/directories specified in `outputs`
|
||||
2. **Task logs** - stdout/stderr for replay on cache hit
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"outputs": ["dist/**", ".next/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Local Cache Location
|
||||
|
||||
```
|
||||
.turbo/cache/
|
||||
├── <hash1>.tar.zst # compressed outputs
|
||||
├── <hash2>.tar.zst
|
||||
└── ...
|
||||
```
|
||||
|
||||
Add `.turbo` to `.gitignore`.
|
||||
|
||||
## Cache Restoration
|
||||
|
||||
On cache hit, Turborepo:
|
||||
|
||||
1. Extracts archived outputs to their original locations
|
||||
2. Replays the logged stdout/stderr
|
||||
3. Reports the task as cached (shows `FULL TURBO` in output)
|
||||
|
||||
## Example Flow
|
||||
|
||||
```bash
|
||||
# First run - executes build, caches result
|
||||
turbo build
|
||||
# → packages/ui: cache miss, executing...
|
||||
# → packages/web: cache miss, executing...
|
||||
|
||||
# Second run - same inputs, restores from cache
|
||||
turbo build
|
||||
# → packages/ui: cache hit, replaying output
|
||||
# → packages/web: cache hit, replaying output
|
||||
# → FULL TURBO
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Cache is content-addressed (based on input hash, not timestamps)
|
||||
- Empty `outputs` array means task runs but nothing is cached
|
||||
- Tasks without `outputs` key cache nothing (use `"outputs": []` to be explicit)
|
||||
- Cache is invalidated when ANY input changes
|
||||
169
.agents/skills/turborepo/references/caching/gotchas.md
Normal file
169
.agents/skills/turborepo/references/caching/gotchas.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Debugging Cache Issues
|
||||
|
||||
## Diagnostic Tools
|
||||
|
||||
### `--summarize`
|
||||
|
||||
Generates a JSON file with all hash inputs. Compare two runs to find differences.
|
||||
|
||||
```bash
|
||||
turbo build --summarize
|
||||
# Creates .turbo/runs/<run-id>.json
|
||||
```
|
||||
|
||||
The summary includes:
|
||||
|
||||
- Global hash and its inputs
|
||||
- Per-task hashes and their inputs
|
||||
- Environment variables that affected the hash
|
||||
|
||||
**Comparing runs:**
|
||||
|
||||
```bash
|
||||
# Run twice, compare the summaries
|
||||
diff .turbo/runs/<first-run>.json .turbo/runs/<second-run>.json
|
||||
```
|
||||
|
||||
### `--dry` / `--dry=json`
|
||||
|
||||
See what would run without executing anything:
|
||||
|
||||
```bash
|
||||
turbo build --dry
|
||||
turbo build --dry=json # machine-readable output
|
||||
```
|
||||
|
||||
Shows cache status for each task without running them.
|
||||
|
||||
### `--force`
|
||||
|
||||
Skip reading cache, re-execute all tasks:
|
||||
|
||||
```bash
|
||||
turbo build --force
|
||||
```
|
||||
|
||||
Useful to verify tasks actually work (not just cached results).
|
||||
|
||||
## Unexpected Cache Misses
|
||||
|
||||
**Symptom:** Task runs when you expected a cache hit.
|
||||
|
||||
### Environment Variable Changed
|
||||
|
||||
Check if an env var in the `env` key changed:
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"env": ["API_URL", "NODE_ENV"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Different `API_URL` between runs = cache miss.
|
||||
|
||||
### .env File Changed
|
||||
|
||||
`.env` files aren't tracked by default. Add to `inputs`:
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env", ".env.local"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or use `globalDependencies` for repo-wide env files:
|
||||
|
||||
```json
|
||||
{
|
||||
"globalDependencies": [".env"]
|
||||
}
|
||||
```
|
||||
|
||||
### Lockfile Changed
|
||||
|
||||
Installing/updating packages changes the global hash.
|
||||
|
||||
### Source Files Changed
|
||||
|
||||
Any file in the package (or in `inputs`) triggers a miss.
|
||||
|
||||
### turbo.json Changed
|
||||
|
||||
Config changes invalidate the global hash.
|
||||
|
||||
## Incorrect Cache Hits
|
||||
|
||||
**Symptom:** Cached output is stale/wrong.
|
||||
|
||||
### Missing Environment Variable
|
||||
|
||||
Task uses an env var not listed in `env`:
|
||||
|
||||
```javascript
|
||||
// build.js
|
||||
const apiUrl = process.env.API_URL; // not tracked!
|
||||
```
|
||||
|
||||
Fix: add to task config:
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"env": ["API_URL"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Missing File in Inputs
|
||||
|
||||
Task reads a file outside default inputs:
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"inputs": [
|
||||
"$TURBO_DEFAULT$",
|
||||
"../../shared-config.json" // file outside package
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Useful Flags
|
||||
|
||||
```bash
|
||||
# Only show output for cache misses
|
||||
turbo build --output-logs=new-only
|
||||
|
||||
# Show output for everything (debugging)
|
||||
turbo build --output-logs=full
|
||||
|
||||
# See why tasks are running
|
||||
turbo build --verbosity=2
|
||||
```
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
Cache miss when expected hit:
|
||||
|
||||
1. Run with `--summarize`, compare with previous run
|
||||
2. Check env vars with `--dry=json`
|
||||
3. Look for lockfile/config changes in git
|
||||
|
||||
Cache hit when expected miss:
|
||||
|
||||
1. Verify env var is in `env` array
|
||||
2. Verify file is in `inputs` array
|
||||
3. Check if file is outside package directory
|
||||
127
.agents/skills/turborepo/references/caching/remote-cache.md
Normal file
127
.agents/skills/turborepo/references/caching/remote-cache.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Remote Caching
|
||||
|
||||
Share cache artifacts across your team and CI pipelines.
|
||||
|
||||
## Benefits
|
||||
|
||||
- Team members get cache hits from each other's work
|
||||
- CI gets cache hits from local development (and vice versa)
|
||||
- Dramatically faster CI runs after first build
|
||||
- No more "works on my machine" rebuilds
|
||||
|
||||
## Vercel Remote Cache
|
||||
|
||||
Free, zero-config when deploying on Vercel. For local dev and other CI:
|
||||
|
||||
### Local Development Setup
|
||||
|
||||
```bash
|
||||
# Authenticate with Vercel
|
||||
npx turbo login
|
||||
|
||||
# Link repo to your Vercel team
|
||||
npx turbo link
|
||||
```
|
||||
|
||||
This creates `.turbo/config.json` with your team info (gitignored by default).
|
||||
|
||||
### CI Setup
|
||||
|
||||
Set these environment variables:
|
||||
|
||||
```bash
|
||||
TURBO_TOKEN=<your-token>
|
||||
TURBO_TEAM=<your-team-slug>
|
||||
```
|
||||
|
||||
Get your token from Vercel dashboard → Settings → Tokens.
|
||||
|
||||
**GitHub Actions example:**
|
||||
|
||||
```yaml
|
||||
- name: Build
|
||||
run: npx turbo build
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
```
|
||||
|
||||
## Configuration in turbo.json
|
||||
|
||||
```json
|
||||
{
|
||||
"remoteCache": {
|
||||
"enabled": true,
|
||||
"signature": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- `enabled`: toggle remote cache (default: true when authenticated)
|
||||
- `signature`: require artifact signing (default: false)
|
||||
|
||||
## Artifact Signing
|
||||
|
||||
Verify cache artifacts haven't been tampered with:
|
||||
|
||||
```bash
|
||||
# Set a secret key (use same key across all environments)
|
||||
export TURBO_REMOTE_CACHE_SIGNATURE_KEY="your-secret-key"
|
||||
```
|
||||
|
||||
Enable in config:
|
||||
|
||||
```json
|
||||
{
|
||||
"remoteCache": {
|
||||
"signature": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Signed artifacts can only be restored if the signature matches.
|
||||
|
||||
## Self-Hosted Options
|
||||
|
||||
Community implementations for running your own cache server:
|
||||
|
||||
- **turbo-remote-cache** (Node.js) - supports S3, GCS, Azure
|
||||
- **turborepo-remote-cache** (Go) - lightweight, S3-compatible
|
||||
- **ducktape** (Rust) - high-performance option
|
||||
|
||||
Configure with environment variables:
|
||||
|
||||
```bash
|
||||
TURBO_API=https://your-cache-server.com
|
||||
TURBO_TOKEN=your-auth-token
|
||||
TURBO_TEAM=your-team
|
||||
```
|
||||
|
||||
## Cache Behavior Control
|
||||
|
||||
```bash
|
||||
# Disable remote cache for a run
|
||||
turbo build --remote-cache-read-only # read but don't write
|
||||
turbo build --no-cache # skip cache entirely
|
||||
|
||||
# Environment variable alternative
|
||||
TURBO_REMOTE_ONLY=true # only use remote, skip local
|
||||
```
|
||||
|
||||
## Debugging Remote Cache
|
||||
|
||||
```bash
|
||||
# Verbose output shows cache operations
|
||||
turbo build --verbosity=2
|
||||
|
||||
# Check if remote cache is configured
|
||||
turbo config
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- "Remote caching enabled" in output
|
||||
- Upload/download messages during runs
|
||||
- "cache hit, replaying output" with remote cache indicator
|
||||
79
.agents/skills/turborepo/references/ci/RULE.md
Normal file
79
.agents/skills/turborepo/references/ci/RULE.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# CI/CD with Turborepo
|
||||
|
||||
General principles for running Turborepo in continuous integration environments.
|
||||
|
||||
## Core Principles
|
||||
|
||||
### Always Use `turbo run` in CI
|
||||
|
||||
**Never use the `turbo <tasks>` shorthand in CI or scripts.** Always use `turbo run`:
|
||||
|
||||
```bash
|
||||
# CORRECT - Always use in CI, package.json, scripts
|
||||
turbo run build test lint
|
||||
|
||||
# WRONG - Shorthand is only for one-off terminal commands
|
||||
turbo build test lint
|
||||
```
|
||||
|
||||
The shorthand `turbo <tasks>` is only for one-off invocations typed directly in terminal by humans or agents. Anywhere the command is written into code (CI, package.json, scripts), use `turbo run`.
|
||||
|
||||
### Enable Remote Caching
|
||||
|
||||
Remote caching dramatically speeds up CI by sharing cached artifacts across runs.
|
||||
|
||||
Required environment variables:
|
||||
|
||||
```bash
|
||||
TURBO_TOKEN=your_vercel_token
|
||||
TURBO_TEAM=your_team_slug
|
||||
```
|
||||
|
||||
### Use --affected for PR Builds
|
||||
|
||||
The `--affected` flag only runs tasks for packages changed since the base branch:
|
||||
|
||||
```bash
|
||||
turbo run build test --affected
|
||||
```
|
||||
|
||||
This requires Git history to compute what changed.
|
||||
|
||||
## Git History Requirements
|
||||
|
||||
### Fetch Depth
|
||||
|
||||
`--affected` needs access to the merge base. Shallow clones break this.
|
||||
|
||||
```yaml
|
||||
# GitHub Actions
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2 # Minimum for --affected
|
||||
# Use 0 for full history if merge base is far
|
||||
```
|
||||
|
||||
### Why Shallow Clones Break --affected
|
||||
|
||||
Turborepo compares the current HEAD to the merge base with `main`. If that commit isn't fetched, `--affected` falls back to running everything.
|
||||
|
||||
For PRs with many commits, consider:
|
||||
|
||||
```yaml
|
||||
fetch-depth: 0 # Full history
|
||||
```
|
||||
|
||||
## Environment Variables Reference
|
||||
|
||||
| Variable | Purpose |
|
||||
| ------------------- | ------------------------------------ |
|
||||
| `TURBO_TOKEN` | Vercel access token for remote cache |
|
||||
| `TURBO_TEAM` | Your Vercel team slug |
|
||||
| `TURBO_REMOTE_ONLY` | Skip local cache, use remote only |
|
||||
| `TURBO_LOG_ORDER` | Set to `grouped` for cleaner CI logs |
|
||||
|
||||
## See Also
|
||||
|
||||
- [github-actions.md](./github-actions.md) - GitHub Actions setup
|
||||
- [vercel.md](./vercel.md) - Vercel deployment
|
||||
- [patterns.md](./patterns.md) - CI optimization patterns
|
||||
162
.agents/skills/turborepo/references/ci/github-actions.md
Normal file
162
.agents/skills/turborepo/references/ci/github-actions.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# GitHub Actions
|
||||
|
||||
Complete setup guide for Turborepo with GitHub Actions.
|
||||
|
||||
## Basic Workflow Structure
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build and Test
|
||||
run: turbo run build test lint
|
||||
```
|
||||
|
||||
## Package Manager Setup
|
||||
|
||||
### pnpm
|
||||
|
||||
```yaml
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
```
|
||||
|
||||
### Yarn
|
||||
|
||||
```yaml
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "yarn"
|
||||
|
||||
- run: yarn install --frozen-lockfile
|
||||
```
|
||||
|
||||
### Bun
|
||||
|
||||
```yaml
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- run: bun install --frozen-lockfile
|
||||
```
|
||||
|
||||
## Remote Cache Setup
|
||||
|
||||
### 1. Create Vercel Access Token
|
||||
|
||||
1. Go to [Vercel Dashboard](https://vercel.com/account/tokens)
|
||||
2. Create a new token with appropriate scope
|
||||
3. Copy the token value
|
||||
|
||||
### 2. Add Secrets and Variables
|
||||
|
||||
In your GitHub repository settings:
|
||||
|
||||
**Secrets** (Settings > Secrets and variables > Actions > Secrets):
|
||||
|
||||
- `TURBO_TOKEN`: Your Vercel access token
|
||||
|
||||
**Variables** (Settings > Secrets and variables > Actions > Variables):
|
||||
|
||||
- `TURBO_TEAM`: Your Vercel team slug
|
||||
|
||||
### 3. Add to Workflow
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
```
|
||||
|
||||
## Alternative: actions/cache
|
||||
|
||||
If you can't use remote cache, cache Turborepo's local cache directory:
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: turbo-${{ runner.os }}-${{ hashFiles('**/turbo.json', '**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
turbo-${{ runner.os }}-
|
||||
```
|
||||
|
||||
Note: This is less effective than remote cache since it's per-branch.
|
||||
|
||||
## Complete Example
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: turbo run build --affected
|
||||
|
||||
- name: Test
|
||||
run: turbo run test --affected
|
||||
|
||||
- name: Lint
|
||||
run: turbo run lint --affected
|
||||
```
|
||||
145
.agents/skills/turborepo/references/ci/patterns.md
Normal file
145
.agents/skills/turborepo/references/ci/patterns.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# CI Optimization Patterns
|
||||
|
||||
Strategies for efficient CI/CD with Turborepo.
|
||||
|
||||
## PR vs Main Branch Builds
|
||||
|
||||
### PR Builds: Only Affected
|
||||
|
||||
Test only what changed in the PR:
|
||||
|
||||
```yaml
|
||||
- name: Test (PR)
|
||||
if: github.event_name == 'pull_request'
|
||||
run: turbo run build test --affected
|
||||
```
|
||||
|
||||
### Main Branch: Full Build
|
||||
|
||||
Ensure complete validation on merge:
|
||||
|
||||
```yaml
|
||||
- name: Test (Main)
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: turbo run build test
|
||||
```
|
||||
|
||||
## Custom Git Ranges with --filter
|
||||
|
||||
For advanced scenarios, use `--filter` with git refs:
|
||||
|
||||
```bash
|
||||
# Changes since specific commit
|
||||
turbo run test --filter="...[abc123]"
|
||||
|
||||
# Changes between refs
|
||||
turbo run test --filter="...[main...HEAD]"
|
||||
|
||||
# Changes in last 3 commits
|
||||
turbo run test --filter="...[HEAD~3]"
|
||||
```
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
### Remote Cache (Recommended)
|
||||
|
||||
Best performance - shared across all CI runs and developers:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
```
|
||||
|
||||
### actions/cache Fallback
|
||||
|
||||
When remote cache isn't available:
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: turbo-${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
turbo-${{ runner.os }}-${{ github.ref }}-
|
||||
turbo-${{ runner.os }}-
|
||||
```
|
||||
|
||||
Limitations:
|
||||
|
||||
- Cache is branch-scoped
|
||||
- PRs restore from base branch cache
|
||||
- Less efficient than remote cache
|
||||
|
||||
## Matrix Builds
|
||||
|
||||
Test across Node versions:
|
||||
|
||||
```yaml
|
||||
strategy:
|
||||
matrix:
|
||||
node: [18, 20, 22]
|
||||
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- run: turbo run test
|
||||
```
|
||||
|
||||
## Parallelizing Across Jobs
|
||||
|
||||
Split tasks into separate jobs:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: turbo run lint --affected
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: turbo run test --affected
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, test]
|
||||
steps:
|
||||
- run: turbo run build
|
||||
```
|
||||
|
||||
### Cache Considerations
|
||||
|
||||
When parallelizing:
|
||||
|
||||
- Each job has separate cache writes
|
||||
- Remote cache handles this automatically
|
||||
- With actions/cache, use unique keys per job to avoid conflicts
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: turbo-${{ runner.os }}-${{ github.job }}-${{ github.sha }}
|
||||
```
|
||||
|
||||
## Conditional Tasks
|
||||
|
||||
Skip expensive tasks on draft PRs:
|
||||
|
||||
```yaml
|
||||
- name: E2E Tests
|
||||
if: github.event.pull_request.draft == false
|
||||
run: turbo run test:e2e --affected
|
||||
```
|
||||
|
||||
Or require label for full test:
|
||||
|
||||
```yaml
|
||||
- name: Full Test Suite
|
||||
if: contains(github.event.pull_request.labels.*.name, 'full-test')
|
||||
run: turbo run test
|
||||
```
|
||||
103
.agents/skills/turborepo/references/ci/vercel.md
Normal file
103
.agents/skills/turborepo/references/ci/vercel.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Vercel Deployment
|
||||
|
||||
Turborepo integrates seamlessly with Vercel for monorepo deployments.
|
||||
|
||||
## Remote Cache
|
||||
|
||||
Remote caching is **automatically enabled** when deploying to Vercel. No configuration needed - Vercel detects Turborepo and enables caching.
|
||||
|
||||
This means:
|
||||
|
||||
- No `TURBO_TOKEN` or `TURBO_TEAM` setup required on Vercel
|
||||
- Cache is shared across all deployments
|
||||
- Preview and production builds benefit from cache
|
||||
|
||||
## turbo-ignore
|
||||
|
||||
Skip unnecessary builds when a package hasn't changed using `turbo-ignore`.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npx turbo-ignore
|
||||
```
|
||||
|
||||
Or install globally in your project:
|
||||
|
||||
```bash
|
||||
pnpm add -D turbo-ignore
|
||||
```
|
||||
|
||||
### Setup in Vercel
|
||||
|
||||
1. Go to your project in Vercel Dashboard
|
||||
2. Navigate to Settings > Git > Ignored Build Step
|
||||
3. Select "Custom" and enter:
|
||||
|
||||
```bash
|
||||
npx turbo-ignore
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
`turbo-ignore` checks if the current package (or its dependencies) changed since the last successful deployment:
|
||||
|
||||
1. Compares current commit to last deployed commit
|
||||
2. Uses Turborepo's dependency graph
|
||||
3. Returns exit code 0 (skip) if no changes
|
||||
4. Returns exit code 1 (build) if changes detected
|
||||
|
||||
### Options
|
||||
|
||||
```bash
|
||||
# Check specific package
|
||||
npx turbo-ignore web
|
||||
|
||||
# Use specific comparison ref
|
||||
npx turbo-ignore --fallback=HEAD~1
|
||||
|
||||
# Verbose output
|
||||
npx turbo-ignore --verbose
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Set environment variables in Vercel Dashboard:
|
||||
|
||||
1. Go to Project Settings > Environment Variables
|
||||
2. Add variables for each environment (Production, Preview, Development)
|
||||
|
||||
Common variables:
|
||||
|
||||
- `DATABASE_URL`
|
||||
- `API_KEY`
|
||||
- Package-specific config
|
||||
|
||||
## Monorepo Root Directory
|
||||
|
||||
For monorepos, set the root directory in Vercel:
|
||||
|
||||
1. Project Settings > General > Root Directory
|
||||
2. Set to the package path (e.g., `apps/web`)
|
||||
|
||||
Vercel automatically:
|
||||
|
||||
- Installs dependencies from monorepo root
|
||||
- Runs build from the package directory
|
||||
- Detects framework settings
|
||||
|
||||
## Build Command
|
||||
|
||||
Vercel auto-detects `turbo run build` when `turbo.json` exists at root.
|
||||
|
||||
Override if needed:
|
||||
|
||||
```bash
|
||||
turbo run build --filter=web
|
||||
```
|
||||
|
||||
Or for production-only optimizations:
|
||||
|
||||
```bash
|
||||
turbo run build --filter=web --env-mode=strict
|
||||
```
|
||||
100
.agents/skills/turborepo/references/cli/RULE.md
Normal file
100
.agents/skills/turborepo/references/cli/RULE.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# turbo run
|
||||
|
||||
The primary command for executing tasks across your monorepo.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```bash
|
||||
# Full form (use in CI, package.json, scripts)
|
||||
turbo run <tasks>
|
||||
|
||||
# Shorthand (only for one-off terminal invocations)
|
||||
turbo <tasks>
|
||||
```
|
||||
|
||||
## When to Use `turbo run` vs `turbo`
|
||||
|
||||
**Always use `turbo run` when the command is written into code:**
|
||||
|
||||
- `package.json` scripts
|
||||
- CI/CD workflows (GitHub Actions, etc.)
|
||||
- Shell scripts
|
||||
- Documentation
|
||||
- Any static/committed configuration
|
||||
|
||||
**Only use `turbo` (shorthand) for:**
|
||||
|
||||
- One-off commands typed directly in terminal
|
||||
- Ad-hoc invocations by humans or agents
|
||||
|
||||
```json
|
||||
// package.json - ALWAYS use "turbo run"
|
||||
{
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"lint": "turbo run lint",
|
||||
"test": "turbo run test"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# CI workflow - ALWAYS use "turbo run"
|
||||
- run: turbo run build --affected
|
||||
- run: turbo run test --affected
|
||||
```
|
||||
|
||||
```bash
|
||||
# Terminal one-off - shorthand OK
|
||||
turbo build --filter=web
|
||||
```
|
||||
|
||||
## Running Tasks
|
||||
|
||||
Tasks must be defined in `turbo.json` before running.
|
||||
|
||||
```bash
|
||||
# Single task
|
||||
turbo build
|
||||
|
||||
# Multiple tasks
|
||||
turbo run build lint test
|
||||
|
||||
# See available tasks (run without arguments)
|
||||
turbo run
|
||||
```
|
||||
|
||||
## Passing Arguments to Scripts
|
||||
|
||||
Use `--` to pass arguments through to the underlying package scripts:
|
||||
|
||||
```bash
|
||||
turbo run build -- --sourcemap
|
||||
turbo test -- --watch
|
||||
turbo lint -- --fix
|
||||
```
|
||||
|
||||
Everything after `--` goes directly to the task's script.
|
||||
|
||||
## Package Selection
|
||||
|
||||
By default, turbo runs tasks in all packages. Use `--filter` to narrow scope:
|
||||
|
||||
```bash
|
||||
turbo build --filter=web
|
||||
turbo test --filter=./apps/*
|
||||
```
|
||||
|
||||
See `filtering/` for complete filter syntax.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Goal | Command |
|
||||
| ------------------- | -------------------------- |
|
||||
| Build everything | `turbo build` |
|
||||
| Build one package | `turbo build --filter=web` |
|
||||
| Multiple tasks | `turbo build lint test` |
|
||||
| Pass args to script | `turbo build -- --arg` |
|
||||
| Preview run | `turbo build --dry` |
|
||||
| Force rebuild | `turbo build --force` |
|
||||
297
.agents/skills/turborepo/references/cli/commands.md
Normal file
297
.agents/skills/turborepo/references/cli/commands.md
Normal file
@@ -0,0 +1,297 @@
|
||||
# turbo run Flags Reference
|
||||
|
||||
Full docs: https://turborepo.dev/docs/reference/run
|
||||
|
||||
## Package Selection
|
||||
|
||||
### `--filter` / `-F`
|
||||
|
||||
Select specific packages to run tasks in.
|
||||
|
||||
```bash
|
||||
turbo build --filter=web
|
||||
turbo build -F=@repo/ui -F=@repo/utils
|
||||
turbo test --filter=./apps/*
|
||||
```
|
||||
|
||||
See `filtering/` for complete syntax (globs, dependencies, git ranges).
|
||||
|
||||
### Task Identifier Syntax (v2.2.4+)
|
||||
|
||||
Run specific package tasks directly:
|
||||
|
||||
```bash
|
||||
turbo run web#build # Build web package
|
||||
turbo run web#build docs#lint # Multiple specific tasks
|
||||
```
|
||||
|
||||
### `--affected`
|
||||
|
||||
Run only in packages changed since the base branch.
|
||||
|
||||
```bash
|
||||
turbo build --affected
|
||||
turbo test --affected --filter=./apps/* # combine with filter
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
|
||||
- Default: compares `main...HEAD`
|
||||
- In GitHub Actions: auto-detects `GITHUB_BASE_REF`
|
||||
- Override base: `TURBO_SCM_BASE=development turbo build --affected`
|
||||
- Override head: `TURBO_SCM_HEAD=your-branch turbo build --affected`
|
||||
|
||||
**Requires git history** - shallow clones may fall back to running all tasks.
|
||||
|
||||
## Execution Control
|
||||
|
||||
### `--dry` / `--dry=json`
|
||||
|
||||
Preview what would run without executing.
|
||||
|
||||
```bash
|
||||
turbo build --dry # human-readable
|
||||
turbo build --dry=json # machine-readable
|
||||
```
|
||||
|
||||
### `--force`
|
||||
|
||||
Ignore all cached artifacts, re-run everything.
|
||||
|
||||
```bash
|
||||
turbo build --force
|
||||
```
|
||||
|
||||
### `--concurrency`
|
||||
|
||||
Limit parallel task execution.
|
||||
|
||||
```bash
|
||||
turbo build --concurrency=4 # max 4 tasks
|
||||
turbo build --concurrency=50% # 50% of CPU cores
|
||||
```
|
||||
|
||||
### `--continue`
|
||||
|
||||
Keep running other tasks when one fails.
|
||||
|
||||
```bash
|
||||
turbo build test --continue
|
||||
```
|
||||
|
||||
### `--only`
|
||||
|
||||
Run only the specified task, skip its dependencies.
|
||||
|
||||
```bash
|
||||
turbo build --only # skip running dependsOn tasks
|
||||
```
|
||||
|
||||
### `--parallel` (Discouraged)
|
||||
|
||||
Ignores task graph dependencies, runs all tasks simultaneously. **Avoid using this flag**—if tasks need to run in parallel, configure `dependsOn` correctly instead. Using `--parallel` bypasses Turborepo's dependency graph, which can cause race conditions and incorrect builds.
|
||||
|
||||
## Cache Control
|
||||
|
||||
### `--cache`
|
||||
|
||||
Fine-grained cache behavior control.
|
||||
|
||||
```bash
|
||||
# Default: read/write both local and remote
|
||||
turbo build --cache=local:rw,remote:rw
|
||||
|
||||
# Read-only local, no remote
|
||||
turbo build --cache=local:r,remote:
|
||||
|
||||
# Disable local, read-only remote
|
||||
turbo build --cache=local:,remote:r
|
||||
|
||||
# Disable all caching
|
||||
turbo build --cache=local:,remote:
|
||||
```
|
||||
|
||||
## Output & Debugging
|
||||
|
||||
### `--graph`
|
||||
|
||||
Generate task graph visualization.
|
||||
|
||||
```bash
|
||||
turbo build --graph # opens in browser
|
||||
turbo build --graph=graph.svg # SVG file
|
||||
turbo build --graph=graph.png # PNG file
|
||||
turbo build --graph=graph.json # JSON data
|
||||
turbo build --graph=graph.mermaid # Mermaid diagram
|
||||
```
|
||||
|
||||
### `--summarize`
|
||||
|
||||
Generate JSON run summary for debugging.
|
||||
|
||||
```bash
|
||||
turbo build --summarize
|
||||
# creates .turbo/runs/<run-id>.json
|
||||
```
|
||||
|
||||
### `--output-logs`
|
||||
|
||||
Control log output verbosity.
|
||||
|
||||
```bash
|
||||
turbo build --output-logs=full # all logs (default)
|
||||
turbo build --output-logs=new-only # only cache misses
|
||||
turbo build --output-logs=errors-only # only failures
|
||||
turbo build --output-logs=none # silent
|
||||
```
|
||||
|
||||
### `--profile`
|
||||
|
||||
Generate Chrome tracing profile for performance analysis.
|
||||
|
||||
```bash
|
||||
turbo build --profile=profile.json
|
||||
# open chrome://tracing and load the file
|
||||
```
|
||||
|
||||
### `--verbosity` / `-v`
|
||||
|
||||
Control turbo's own log level.
|
||||
|
||||
```bash
|
||||
turbo build -v # verbose
|
||||
turbo build -vv # more verbose
|
||||
turbo build -vvv # maximum verbosity
|
||||
```
|
||||
|
||||
## Environment
|
||||
|
||||
### `--env-mode`
|
||||
|
||||
Control environment variable handling.
|
||||
|
||||
```bash
|
||||
turbo build --env-mode=strict # only declared env vars (default)
|
||||
turbo build --env-mode=loose # include all env vars in hash
|
||||
```
|
||||
|
||||
## UI
|
||||
|
||||
### `--ui`
|
||||
|
||||
Select output interface.
|
||||
|
||||
```bash
|
||||
turbo build --ui=tui # interactive terminal UI (default in TTY)
|
||||
turbo build --ui=stream # streaming logs (default in CI)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# turbo-ignore
|
||||
|
||||
Full docs: https://turborepo.dev/docs/reference/turbo-ignore
|
||||
|
||||
Skip CI work when nothing relevant changed. Useful for skipping container setup.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```bash
|
||||
# Check if build is needed for current package (uses Automatic Package Scoping)
|
||||
npx turbo-ignore
|
||||
|
||||
# Check specific package
|
||||
npx turbo-ignore web
|
||||
|
||||
# Check specific task
|
||||
npx turbo-ignore --task=test
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
- `0`: No changes detected - skip CI work
|
||||
- `1`: Changes detected - proceed with CI
|
||||
|
||||
## CI Integration Example
|
||||
|
||||
```yaml
|
||||
# GitHub Actions
|
||||
- name: Check for changes
|
||||
id: turbo-ignore
|
||||
run: npx turbo-ignore web
|
||||
continue-on-error: true
|
||||
|
||||
- name: Build
|
||||
if: steps.turbo-ignore.outcome == 'failure' # changes detected
|
||||
run: pnpm build
|
||||
```
|
||||
|
||||
## Comparison Depth
|
||||
|
||||
Default: compares to parent commit (`HEAD^1`).
|
||||
|
||||
```bash
|
||||
# Compare to specific commit
|
||||
npx turbo-ignore --fallback=abc123
|
||||
|
||||
# Compare to branch
|
||||
npx turbo-ignore --fallback=main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Other Commands
|
||||
|
||||
## turbo boundaries
|
||||
|
||||
Check workspace violations (experimental).
|
||||
|
||||
```bash
|
||||
turbo boundaries
|
||||
```
|
||||
|
||||
See `references/boundaries/` for configuration.
|
||||
|
||||
## turbo watch
|
||||
|
||||
Re-run tasks on file changes.
|
||||
|
||||
```bash
|
||||
turbo watch build test
|
||||
```
|
||||
|
||||
See `references/watch/` for details.
|
||||
|
||||
## turbo prune
|
||||
|
||||
Create sparse checkout for Docker.
|
||||
|
||||
```bash
|
||||
turbo prune web --docker
|
||||
```
|
||||
|
||||
## turbo link / unlink
|
||||
|
||||
Connect/disconnect Remote Cache.
|
||||
|
||||
```bash
|
||||
turbo link # connect to Vercel Remote Cache
|
||||
turbo unlink # disconnect
|
||||
```
|
||||
|
||||
## turbo login / logout
|
||||
|
||||
Authenticate with Remote Cache provider.
|
||||
|
||||
```bash
|
||||
turbo login # authenticate
|
||||
turbo logout # log out
|
||||
```
|
||||
|
||||
## turbo generate
|
||||
|
||||
Scaffold new packages.
|
||||
|
||||
```bash
|
||||
turbo generate
|
||||
```
|
||||
211
.agents/skills/turborepo/references/configuration/RULE.md
Normal file
211
.agents/skills/turborepo/references/configuration/RULE.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# turbo.json Configuration Overview
|
||||
|
||||
Configuration reference for Turborepo. Full docs: https://turborepo.dev/docs/reference/configuration
|
||||
|
||||
## File Location
|
||||
|
||||
Root `turbo.json` lives at repo root, sibling to root `package.json`:
|
||||
|
||||
```
|
||||
my-monorepo/
|
||||
├── turbo.json # Root configuration
|
||||
├── package.json
|
||||
└── packages/
|
||||
└── web/
|
||||
├── turbo.json # Package Configuration (optional)
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Always Prefer Package Tasks Over Root Tasks
|
||||
|
||||
**Always use package tasks. Only use Root Tasks if you cannot succeed with package tasks.**
|
||||
|
||||
Package tasks enable parallelization, individual caching, and filtering. Define scripts in each package's `package.json`:
|
||||
|
||||
```json
|
||||
// packages/web/package.json
|
||||
{
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"lint": "eslint .",
|
||||
"test": "vitest",
|
||||
"typecheck": "tsc --noEmit"
|
||||
}
|
||||
}
|
||||
|
||||
// packages/api/package.json
|
||||
{
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"lint": "eslint .",
|
||||
"test": "vitest",
|
||||
"typecheck": "tsc --noEmit"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// Root package.json - delegates to turbo
|
||||
{
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"lint": "turbo run lint",
|
||||
"test": "turbo run test",
|
||||
"typecheck": "turbo run typecheck"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When you run `turbo run lint`, Turborepo finds all packages with a `lint` script and runs them **in parallel**.
|
||||
|
||||
**Root Tasks are a fallback**, not the default. Only use them for tasks that truly cannot run per-package (e.g., repo-level CI scripts, workspace-wide config generation).
|
||||
|
||||
```json
|
||||
// AVOID: Task logic in root defeats parallelization
|
||||
{
|
||||
"scripts": {
|
||||
"lint": "eslint apps/web && eslint apps/api && eslint packages/ui"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Basic Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://turborepo.dev/schema.v2.json",
|
||||
"globalEnv": ["CI"],
|
||||
"globalDependencies": ["tsconfig.json"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `$schema` key enables IDE autocompletion and validation.
|
||||
|
||||
## Configuration Sections
|
||||
|
||||
**Global options** - Settings affecting all tasks:
|
||||
|
||||
- `globalEnv`, `globalDependencies`, `globalPassThroughEnv`
|
||||
- `cacheDir`, `daemon`, `envMode`, `ui`, `remoteCache`
|
||||
|
||||
**Task definitions** - Per-task settings in `tasks` object:
|
||||
|
||||
- `dependsOn`, `outputs`, `inputs`, `env`
|
||||
- `cache`, `persistent`, `interactive`, `outputLogs`
|
||||
|
||||
## Package Configurations
|
||||
|
||||
Use `turbo.json` in individual packages to override root settings:
|
||||
|
||||
```json
|
||||
// packages/web/turbo.json
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"outputs": [".next/**", "!.next/cache/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `"extends": ["//"]` is required - it references the root configuration.
|
||||
|
||||
**When to use Package Configurations:**
|
||||
|
||||
- Framework-specific outputs (Next.js, Vite, etc.)
|
||||
- Package-specific env vars
|
||||
- Different caching rules for specific packages
|
||||
- Keeping framework config close to the framework code
|
||||
|
||||
### Extending from Other Packages
|
||||
|
||||
You can extend from config packages instead of just root:
|
||||
|
||||
```json
|
||||
// packages/web/turbo.json
|
||||
{
|
||||
"extends": ["//", "@repo/turbo-config"]
|
||||
}
|
||||
```
|
||||
|
||||
### Adding to Inherited Arrays with `$TURBO_EXTENDS$`
|
||||
|
||||
By default, array fields in Package Configurations **replace** root values. Use `$TURBO_EXTENDS$` to **append** instead:
|
||||
|
||||
```json
|
||||
// Root turbo.json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"outputs": ["dist/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// packages/web/turbo.json
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
// Inherits "dist/**" from root, adds ".next/**"
|
||||
"outputs": ["$TURBO_EXTENDS$", ".next/**", "!.next/cache/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Without `$TURBO_EXTENDS$`, outputs would only be `[".next/**", "!.next/cache/**"]`.
|
||||
|
||||
**Works with:**
|
||||
|
||||
- `dependsOn`
|
||||
- `env`
|
||||
- `inputs`
|
||||
- `outputs`
|
||||
- `passThroughEnv`
|
||||
- `with`
|
||||
|
||||
### Excluding Tasks from Packages
|
||||
|
||||
Use `extends: false` to exclude a task from a package:
|
||||
|
||||
```json
|
||||
// packages/ui/turbo.json
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"e2e": {
|
||||
"extends": false // UI package doesn't have e2e tests
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `turbo.jsonc` for Comments
|
||||
|
||||
Use `turbo.jsonc` extension to add comments with IDE support:
|
||||
|
||||
```jsonc
|
||||
// turbo.jsonc
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
// Next.js outputs
|
||||
"outputs": [".next/**", "!.next/cache/**"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,183 @@
|
||||
# Global Options Reference
|
||||
|
||||
Options that affect all tasks. Full docs: https://turborepo.dev/docs/reference/configuration
|
||||
|
||||
## globalEnv
|
||||
|
||||
Environment variables affecting all task hashes.
|
||||
|
||||
```json
|
||||
{
|
||||
"globalEnv": ["CI", "NODE_ENV", "VERCEL_*"]
|
||||
}
|
||||
```
|
||||
|
||||
Use for variables that should invalidate all caches when changed.
|
||||
|
||||
## globalDependencies
|
||||
|
||||
Files that affect all task hashes.
|
||||
|
||||
```json
|
||||
{
|
||||
"globalDependencies": ["tsconfig.json", ".env", "pnpm-lock.yaml"]
|
||||
}
|
||||
```
|
||||
|
||||
Lockfile is included by default. Add shared configs here.
|
||||
|
||||
## globalPassThroughEnv
|
||||
|
||||
Variables available to tasks but not included in hash.
|
||||
|
||||
```json
|
||||
{
|
||||
"globalPassThroughEnv": ["AWS_SECRET_KEY", "GITHUB_TOKEN"]
|
||||
}
|
||||
```
|
||||
|
||||
Use for credentials that shouldn't affect cache keys.
|
||||
|
||||
## cacheDir
|
||||
|
||||
Custom cache location. Default: `node_modules/.cache/turbo`.
|
||||
|
||||
```json
|
||||
{
|
||||
"cacheDir": ".turbo/cache"
|
||||
}
|
||||
```
|
||||
|
||||
## daemon
|
||||
|
||||
**Deprecated**: The daemon is no longer used for `turbo run` and this option will be removed in version 3.0. The daemon is still used by `turbo watch` and the Turborepo LSP.
|
||||
|
||||
## envMode
|
||||
|
||||
How unspecified env vars are handled. Default: `"strict"`.
|
||||
|
||||
```json
|
||||
{
|
||||
"envMode": "strict" // Only specified vars available
|
||||
// or
|
||||
"envMode": "loose" // All vars pass through
|
||||
}
|
||||
```
|
||||
|
||||
Strict mode catches missing env declarations.
|
||||
|
||||
## ui
|
||||
|
||||
Terminal UI mode. Default: `"stream"`.
|
||||
|
||||
```json
|
||||
{
|
||||
"ui": "tui" // Interactive terminal UI
|
||||
// or
|
||||
"ui": "stream" // Traditional streaming logs
|
||||
}
|
||||
```
|
||||
|
||||
TUI provides better UX for parallel tasks.
|
||||
|
||||
## remoteCache
|
||||
|
||||
Configure remote caching.
|
||||
|
||||
```json
|
||||
{
|
||||
"remoteCache": {
|
||||
"enabled": true,
|
||||
"signature": true,
|
||||
"timeout": 30,
|
||||
"uploadTimeout": 60
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| --------------- | ---------------------- | ------------------------------------------------------ |
|
||||
| `enabled` | `true` | Enable/disable remote caching |
|
||||
| `signature` | `false` | Sign artifacts with `TURBO_REMOTE_CACHE_SIGNATURE_KEY` |
|
||||
| `preflight` | `false` | Send OPTIONS request before cache requests |
|
||||
| `timeout` | `30` | Timeout in seconds for cache operations |
|
||||
| `uploadTimeout` | `60` | Timeout in seconds for uploads |
|
||||
| `apiUrl` | `"https://vercel.com"` | Remote cache API endpoint |
|
||||
| `loginUrl` | `"https://vercel.com"` | Login endpoint |
|
||||
| `teamId` | - | Team ID (must start with `team_`) |
|
||||
| `teamSlug` | - | Team slug for querystring |
|
||||
|
||||
See https://turborepo.dev/docs/core-concepts/remote-caching for setup.
|
||||
|
||||
## concurrency
|
||||
|
||||
Default: `"10"`
|
||||
|
||||
Limit parallel task execution.
|
||||
|
||||
```json
|
||||
{
|
||||
"concurrency": "4" // Max 4 tasks at once
|
||||
// or
|
||||
"concurrency": "50%" // 50% of available CPUs
|
||||
}
|
||||
```
|
||||
|
||||
## futureFlags
|
||||
|
||||
Enable experimental features that will become default in future versions.
|
||||
|
||||
```json
|
||||
{
|
||||
"futureFlags": {
|
||||
"errorsOnlyShowHash": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `errorsOnlyShowHash`
|
||||
|
||||
When using `outputLogs: "errors-only"`, show task hashes on start/completion:
|
||||
|
||||
- Cache miss: `cache miss, executing <hash> (only logging errors)`
|
||||
- Cache hit: `cache hit, replaying logs (no errors) <hash>`
|
||||
|
||||
## noUpdateNotifier
|
||||
|
||||
Disable update notifications when new turbo versions are available.
|
||||
|
||||
```json
|
||||
{
|
||||
"noUpdateNotifier": true
|
||||
}
|
||||
```
|
||||
|
||||
## dangerouslyDisablePackageManagerCheck
|
||||
|
||||
Bypass the `packageManager` field requirement. Use for incremental migration.
|
||||
|
||||
```json
|
||||
{
|
||||
"dangerouslyDisablePackageManagerCheck": true
|
||||
}
|
||||
```
|
||||
|
||||
**Warning**: Unstable lockfiles can cause unpredictable behavior.
|
||||
|
||||
## Git Worktree Cache Sharing
|
||||
|
||||
When working in Git worktrees, Turborepo automatically shares local cache between the main worktree and linked worktrees.
|
||||
|
||||
**How it works:**
|
||||
|
||||
- Detects worktree configuration
|
||||
- Redirects cache to main worktree's `.turbo/cache`
|
||||
- Works alongside Remote Cache
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Cache hits across branches
|
||||
- Reduced disk usage
|
||||
- Faster branch switching
|
||||
|
||||
**Disabled by**: Setting explicit `cacheDir` in turbo.json.
|
||||
348
.agents/skills/turborepo/references/configuration/gotchas.md
Normal file
348
.agents/skills/turborepo/references/configuration/gotchas.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# Configuration Gotchas
|
||||
|
||||
Common mistakes and how to fix them.
|
||||
|
||||
## #1 Root Scripts Not Using `turbo run`
|
||||
|
||||
Root `package.json` scripts for turbo tasks MUST use `turbo run`, not direct commands.
|
||||
|
||||
```json
|
||||
// WRONG - bypasses turbo, no parallelization or caching
|
||||
{
|
||||
"scripts": {
|
||||
"build": "bun build",
|
||||
"dev": "bun dev"
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT - delegates to turbo
|
||||
{
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why this matters:** Running `bun build` or `npm run build` at root bypasses Turborepo entirely - no parallelization, no caching, no dependency graph awareness.
|
||||
|
||||
## #2 Using `&&` to Chain Turbo Tasks
|
||||
|
||||
Don't use `&&` to chain tasks that turbo should orchestrate.
|
||||
|
||||
```json
|
||||
// WRONG - changeset:publish chains turbo task with non-turbo command
|
||||
{
|
||||
"scripts": {
|
||||
"changeset:publish": "bun build && changeset publish"
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT - use turbo run, let turbo handle dependencies
|
||||
{
|
||||
"scripts": {
|
||||
"changeset:publish": "turbo run build && changeset publish"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If the second command (`changeset publish`) depends on build outputs, the turbo task should run through turbo to get caching and parallelization benefits.
|
||||
|
||||
## #3 Overly Broad globalDependencies
|
||||
|
||||
`globalDependencies` affects hash for ALL tasks in ALL packages. Be specific.
|
||||
|
||||
```json
|
||||
// WRONG - affects all hashes
|
||||
{
|
||||
"globalDependencies": ["**/.env.*local"]
|
||||
}
|
||||
|
||||
// CORRECT - move to specific tasks that need it
|
||||
{
|
||||
"globalDependencies": [".env"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||
"outputs": ["dist/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why this matters:** `**/.env.*local` matches .env files in ALL packages, causing unnecessary cache invalidation. Instead:
|
||||
|
||||
- Use `globalDependencies` only for truly global files (root `.env`)
|
||||
- Use task-level `inputs` for package-specific .env files with `$TURBO_DEFAULT$` to preserve default behavior
|
||||
|
||||
## #4 Repetitive Task Configuration
|
||||
|
||||
Look for repeated configuration across tasks that can be collapsed.
|
||||
|
||||
```json
|
||||
// WRONG - repetitive env and inputs across tasks
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"env": ["API_URL", "DATABASE_URL"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"]
|
||||
},
|
||||
"test": {
|
||||
"env": ["API_URL", "DATABASE_URL"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BETTER - use globalEnv and globalDependencies
|
||||
{
|
||||
"globalEnv": ["API_URL", "DATABASE_URL"],
|
||||
"globalDependencies": [".env*"],
|
||||
"tasks": {
|
||||
"build": {},
|
||||
"test": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When to use global vs task-level:**
|
||||
|
||||
- `globalEnv` / `globalDependencies` - affects ALL tasks, use for truly shared config
|
||||
- Task-level `env` / `inputs` - use when only specific tasks need it
|
||||
|
||||
## #5 Using `../` to Traverse Out of Package in `inputs`
|
||||
|
||||
Don't use relative paths like `../` to reference files outside the package. Use `$TURBO_ROOT$` instead.
|
||||
|
||||
```json
|
||||
// WRONG - traversing out of package
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"inputs": ["$TURBO_DEFAULT$", "../shared-config.json"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT - use $TURBO_ROOT$ for repo root
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/shared-config.json"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## #6 MOST COMMON MISTAKE: Creating Root Tasks
|
||||
|
||||
**DO NOT create Root Tasks. ALWAYS create package tasks.**
|
||||
|
||||
When you need to create a task (build, lint, test, typecheck, etc.):
|
||||
|
||||
1. Add the script to **each relevant package's** `package.json`
|
||||
2. Register the task in root `turbo.json`
|
||||
3. Root `package.json` only contains `turbo run <task>`
|
||||
|
||||
```json
|
||||
// WRONG - DO NOT DO THIS
|
||||
// Root package.json with task logic
|
||||
{
|
||||
"scripts": {
|
||||
"build": "cd apps/web && next build && cd ../api && tsc",
|
||||
"lint": "eslint apps/ packages/",
|
||||
"test": "vitest"
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT - DO THIS
|
||||
// apps/web/package.json
|
||||
{ "scripts": { "build": "next build", "lint": "eslint .", "test": "vitest" } }
|
||||
|
||||
// apps/api/package.json
|
||||
{ "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } }
|
||||
|
||||
// packages/ui/package.json
|
||||
{ "scripts": { "build": "tsc", "lint": "eslint .", "test": "vitest" } }
|
||||
|
||||
// Root package.json - ONLY delegates
|
||||
{ "scripts": { "build": "turbo run build", "lint": "turbo run lint", "test": "turbo run test" } }
|
||||
|
||||
// turbo.json - register tasks
|
||||
{
|
||||
"tasks": {
|
||||
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
|
||||
"lint": {},
|
||||
"test": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why this matters:**
|
||||
|
||||
- Package tasks run in **parallel** across all packages
|
||||
- Each package's output is cached **individually**
|
||||
- You can **filter** to specific packages: `turbo run test --filter=web`
|
||||
|
||||
Root Tasks (`//#taskname`) defeat all these benefits. Only use them for tasks that truly cannot exist in any package (extremely rare).
|
||||
|
||||
## #7 Tasks That Need Parallel Execution + Cache Invalidation
|
||||
|
||||
Some tasks can run in parallel (don't need built output from dependencies) but must still invalidate cache when dependency source code changes. Using `dependsOn: ["^taskname"]` forces sequential execution. Using no dependencies breaks cache invalidation.
|
||||
|
||||
**Use Transit Nodes for these tasks:**
|
||||
|
||||
```json
|
||||
// WRONG - forces sequential execution (SLOW)
|
||||
"my-task": {
|
||||
"dependsOn": ["^my-task"]
|
||||
}
|
||||
|
||||
// ALSO WRONG - no dependency awareness (INCORRECT CACHING)
|
||||
"my-task": {}
|
||||
|
||||
// CORRECT - use Transit Nodes for parallel + correct caching
|
||||
{
|
||||
"tasks": {
|
||||
"transit": { "dependsOn": ["^transit"] },
|
||||
"my-task": { "dependsOn": ["transit"] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why Transit Nodes work:**
|
||||
|
||||
- `transit` creates dependency relationships without matching any actual script
|
||||
- Tasks that depend on `transit` gain dependency awareness
|
||||
- Since `transit` completes instantly (no script), tasks run in parallel
|
||||
- Cache correctly invalidates when dependency source code changes
|
||||
|
||||
**How to identify tasks that need this pattern:** Look for tasks that read source files from dependencies but don't need their build outputs.
|
||||
|
||||
## Missing outputs for File-Producing Tasks
|
||||
|
||||
**Before flagging missing `outputs`, check what the task actually produces:**
|
||||
|
||||
1. Read the package's script (e.g., `"build": "tsc"`, `"test": "vitest"`)
|
||||
2. Determine if it writes files to disk or only outputs to stdout
|
||||
3. Only flag if the task produces files that should be cached
|
||||
|
||||
```json
|
||||
// WRONG - build produces files but they're not cached
|
||||
"build": {
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
|
||||
// CORRECT - outputs are cached
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"]
|
||||
}
|
||||
```
|
||||
|
||||
No `outputs` key is fine for stdout-only tasks. For file-producing tasks, missing `outputs` means Turbo has nothing to cache.
|
||||
|
||||
## Forgetting ^ in dependsOn
|
||||
|
||||
```json
|
||||
// WRONG - looks for "build" in SAME package (infinite loop or missing)
|
||||
"build": {
|
||||
"dependsOn": ["build"]
|
||||
}
|
||||
|
||||
// CORRECT - runs dependencies' build first
|
||||
"build": {
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
```
|
||||
|
||||
The `^` means "in dependency packages", not "in this package".
|
||||
|
||||
## Missing persistent on Dev Tasks
|
||||
|
||||
```json
|
||||
// WRONG - dependent tasks hang waiting for dev to "finish"
|
||||
"dev": {
|
||||
"cache": false
|
||||
}
|
||||
|
||||
// CORRECT
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
```
|
||||
|
||||
## Package Config Missing extends
|
||||
|
||||
```json
|
||||
// WRONG - packages/web/turbo.json
|
||||
{
|
||||
"tasks": {
|
||||
"build": { "outputs": [".next/**"] }
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": { "outputs": [".next/**"] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Without `"extends": ["//"]`, Package Configurations are invalid.
|
||||
|
||||
## Root Tasks Need Special Syntax
|
||||
|
||||
To run a task defined only in root `package.json`:
|
||||
|
||||
```bash
|
||||
# WRONG
|
||||
turbo run format
|
||||
|
||||
# CORRECT
|
||||
turbo run //#format
|
||||
```
|
||||
|
||||
And in dependsOn:
|
||||
|
||||
```json
|
||||
"build": {
|
||||
"dependsOn": ["//#codegen"] // Root package's codegen
|
||||
}
|
||||
```
|
||||
|
||||
## Overwriting Default Inputs
|
||||
|
||||
```json
|
||||
// WRONG - only watches test files, ignores source changes
|
||||
"test": {
|
||||
"inputs": ["tests/**"]
|
||||
}
|
||||
|
||||
// CORRECT - extends defaults, adds test files
|
||||
"test": {
|
||||
"inputs": ["$TURBO_DEFAULT$", "tests/**"]
|
||||
}
|
||||
```
|
||||
|
||||
Without `$TURBO_DEFAULT$`, you replace all default file watching.
|
||||
|
||||
## Caching Tasks with Side Effects
|
||||
|
||||
```json
|
||||
// WRONG - deploy might be skipped on cache hit
|
||||
"deploy": {
|
||||
"dependsOn": ["build"]
|
||||
}
|
||||
|
||||
// CORRECT
|
||||
"deploy": {
|
||||
"dependsOn": ["build"],
|
||||
"cache": false
|
||||
}
|
||||
```
|
||||
|
||||
Always disable cache for deploy, publish, or mutation tasks.
|
||||
281
.agents/skills/turborepo/references/configuration/tasks.md
Normal file
281
.agents/skills/turborepo/references/configuration/tasks.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# Task Configuration Reference
|
||||
|
||||
Full docs: https://turborepo.dev/docs/reference/configuration#tasks
|
||||
|
||||
## dependsOn
|
||||
|
||||
Controls task execution order.
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"^build", // Dependencies' build tasks first
|
||||
"codegen", // Same package's codegen task first
|
||||
"shared#build" // Specific package's build task
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Syntax | Meaning |
|
||||
| ---------- | ------------------------------------ |
|
||||
| `^task` | Run `task` in all dependencies first |
|
||||
| `task` | Run `task` in same package first |
|
||||
| `pkg#task` | Run specific package's task first |
|
||||
|
||||
The `^` prefix is crucial - without it, you're referencing the same package.
|
||||
|
||||
### Transit Nodes for Parallel Tasks
|
||||
|
||||
For tasks like `lint` and `check-types` that can run in parallel but need dependency-aware caching:
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"transit": { "dependsOn": ["^transit"] },
|
||||
"lint": { "dependsOn": ["transit"] },
|
||||
"check-types": { "dependsOn": ["transit"] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**DO NOT use `dependsOn: ["^lint"]`** - this forces sequential execution.
|
||||
**DO NOT use `dependsOn: []`** - this breaks cache invalidation.
|
||||
|
||||
The `transit` task creates dependency relationships without running anything (no matching script), so tasks run in parallel with correct caching.
|
||||
|
||||
## outputs
|
||||
|
||||
Glob patterns for files to cache. **If omitted, nothing is cached.**
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"outputs": ["dist/**", "build/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Framework examples:**
|
||||
|
||||
```json
|
||||
// Next.js
|
||||
"outputs": [".next/**", "!.next/cache/**"]
|
||||
|
||||
// Vite
|
||||
"outputs": ["dist/**"]
|
||||
|
||||
// TypeScript (tsc)
|
||||
"outputs": ["dist/**", "*.tsbuildinfo"]
|
||||
|
||||
// No file outputs (lint, typecheck)
|
||||
"outputs": []
|
||||
```
|
||||
|
||||
Use `!` prefix to exclude patterns from caching.
|
||||
|
||||
## inputs
|
||||
|
||||
Files considered when calculating task hash. Defaults to all tracked files in package.
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"test": {
|
||||
"inputs": ["src/**", "tests/**", "vitest.config.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Special values:**
|
||||
|
||||
| Value | Meaning |
|
||||
| --------------------- | --------------------------------------- |
|
||||
| `$TURBO_DEFAULT$` | Include default inputs, then add/remove |
|
||||
| `$TURBO_ROOT$/<path>` | Reference files from repo root |
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"inputs": ["$TURBO_DEFAULT$", "!README.md", "$TURBO_ROOT$/tsconfig.base.json"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## env
|
||||
|
||||
Environment variables to include in task hash.
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"env": [
|
||||
"API_URL",
|
||||
"NEXT_PUBLIC_*", // Wildcard matching
|
||||
"!DEBUG" // Exclude from hash
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Variables listed here affect cache hits - changing the value invalidates cache.
|
||||
|
||||
## cache
|
||||
|
||||
Enable/disable caching for a task. Default: `true`.
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"dev": { "cache": false },
|
||||
"deploy": { "cache": false }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Disable for: dev servers, deploy commands, tasks with side effects.
|
||||
|
||||
## persistent
|
||||
|
||||
Mark long-running tasks that don't exit. Default: `false`.
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Required for dev servers - without it, dependent tasks wait forever.
|
||||
|
||||
## interactive
|
||||
|
||||
Allow task to receive stdin input. Default: `false`.
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"login": {
|
||||
"cache": false,
|
||||
"interactive": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## outputLogs
|
||||
|
||||
Control when logs are shown. Options: `full`, `hash-only`, `new-only`, `errors-only`, `none`.
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"outputLogs": "new-only" // Only show logs on cache miss
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## with
|
||||
|
||||
Run tasks alongside this task. For long-running tasks that need runtime dependencies.
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"dev": {
|
||||
"with": ["api#dev"],
|
||||
"persistent": true,
|
||||
"cache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Unlike `dependsOn`, `with` runs tasks concurrently (not sequentially). Use for dev servers that need other services running.
|
||||
|
||||
## interruptible
|
||||
|
||||
Allow `turbo watch` to restart the task on changes. Default: `false`.
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"dev": {
|
||||
"persistent": true,
|
||||
"interruptible": true,
|
||||
"cache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use for dev servers that don't automatically detect dependency changes.
|
||||
|
||||
## description
|
||||
|
||||
Human-readable description of the task.
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"description": "Compiles the application for production deployment"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For documentation only - doesn't affect execution or caching.
|
||||
|
||||
## passThroughEnv
|
||||
|
||||
Environment variables available at runtime but NOT included in cache hash.
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"passThroughEnv": ["AWS_SECRET_KEY", "GITHUB_TOKEN"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Warning**: Changes to these vars won't cause cache misses. Use `env` if changes should invalidate cache.
|
||||
|
||||
## extends (Package Configuration only)
|
||||
|
||||
Control task inheritance in Package Configurations.
|
||||
|
||||
```json
|
||||
// packages/ui/turbo.json
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"lint": {
|
||||
"extends": false // Exclude from this package
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Value | Behavior |
|
||||
| ---------------- | -------------------------------------------------------------- |
|
||||
| `true` (default) | Inherit from root turbo.json |
|
||||
| `false` | Exclude task from package, or define fresh without inheritance |
|
||||
96
.agents/skills/turborepo/references/environment/RULE.md
Normal file
96
.agents/skills/turborepo/references/environment/RULE.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Environment Variables in Turborepo
|
||||
|
||||
Turborepo provides fine-grained control over which environment variables affect task hashing and runtime availability.
|
||||
|
||||
## Configuration Keys
|
||||
|
||||
### `env` - Task-Specific Variables
|
||||
|
||||
Variables that affect a specific task's hash. When these change, only that task rebuilds.
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"env": ["DATABASE_URL", "API_KEY"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `globalEnv` - Variables Affecting All Tasks
|
||||
|
||||
Variables that affect EVERY task's hash. When these change, all tasks rebuild.
|
||||
|
||||
```json
|
||||
{
|
||||
"globalEnv": ["CI", "NODE_ENV"]
|
||||
}
|
||||
```
|
||||
|
||||
### `passThroughEnv` - Runtime-Only Variables (Not Hashed)
|
||||
|
||||
Variables available at runtime but NOT included in hash. **Use with caution** - changes won't trigger rebuilds.
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"deploy": {
|
||||
"passThroughEnv": ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `globalPassThroughEnv` - Global Runtime Variables
|
||||
|
||||
Same as `passThroughEnv` but for all tasks.
|
||||
|
||||
```json
|
||||
{
|
||||
"globalPassThroughEnv": ["GITHUB_TOKEN"]
|
||||
}
|
||||
```
|
||||
|
||||
## Wildcards and Negation
|
||||
|
||||
### Wildcards
|
||||
|
||||
Match multiple variables with `*`:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": ["MY_API_*", "FEATURE_FLAG_*"]
|
||||
}
|
||||
```
|
||||
|
||||
This matches `MY_API_URL`, `MY_API_KEY`, `FEATURE_FLAG_DARK_MODE`, etc.
|
||||
|
||||
### Negation
|
||||
|
||||
Exclude variables (useful with framework inference):
|
||||
|
||||
```json
|
||||
{
|
||||
"env": ["!NEXT_PUBLIC_ANALYTICS_ID"]
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://turborepo.dev/schema.v2.json",
|
||||
"globalEnv": ["CI", "NODE_ENV"],
|
||||
"globalPassThroughEnv": ["GITHUB_TOKEN", "NPM_TOKEN"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"env": ["DATABASE_URL", "API_*"],
|
||||
"passThroughEnv": ["SENTRY_AUTH_TOKEN"]
|
||||
},
|
||||
"test": {
|
||||
"env": ["TEST_DATABASE_URL"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
141
.agents/skills/turborepo/references/environment/gotchas.md
Normal file
141
.agents/skills/turborepo/references/environment/gotchas.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Environment Variable Gotchas
|
||||
|
||||
Common mistakes and how to fix them.
|
||||
|
||||
## .env Files Must Be in `inputs`
|
||||
|
||||
Turbo does NOT read `.env` files. Your framework (Next.js, Vite, etc.) or `dotenv` loads them. But Turbo needs to know when they change.
|
||||
|
||||
**Wrong:**
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"env": ["DATABASE_URL"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Right:**
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"env": ["DATABASE_URL"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env", ".env.local", ".env.production"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Strict Mode Filters CI Variables
|
||||
|
||||
In strict mode, CI provider variables (GITHUB_TOKEN, GITLAB_CI, etc.) are filtered unless explicitly listed.
|
||||
|
||||
**Symptom:** Task fails with "authentication required" or "permission denied" in CI.
|
||||
|
||||
**Solution:**
|
||||
|
||||
```json
|
||||
{
|
||||
"globalPassThroughEnv": ["GITHUB_TOKEN", "GITLAB_CI", "CI"]
|
||||
}
|
||||
```
|
||||
|
||||
## passThroughEnv Doesn't Affect Hash
|
||||
|
||||
Variables in `passThroughEnv` are available at runtime but changes WON'T trigger rebuilds.
|
||||
|
||||
**Dangerous example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"passThroughEnv": ["API_URL"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If `API_URL` changes from staging to production, Turbo may serve a cached build pointing to the wrong API.
|
||||
|
||||
**Use passThroughEnv only for:**
|
||||
|
||||
- Auth tokens that don't affect output (SENTRY_AUTH_TOKEN)
|
||||
- CI metadata (GITHUB_RUN_ID)
|
||||
- Variables consumed after build (deploy credentials)
|
||||
|
||||
## Runtime-Created Variables Are Invisible
|
||||
|
||||
Turbo captures env vars at startup. Variables created during execution aren't seen.
|
||||
|
||||
**Won't work:**
|
||||
|
||||
```bash
|
||||
# In package.json scripts
|
||||
"build": "export API_URL=$COMPUTED_VALUE && next build"
|
||||
```
|
||||
|
||||
**Solution:** Set vars before invoking turbo:
|
||||
|
||||
```bash
|
||||
API_URL=$COMPUTED_VALUE turbo run build
|
||||
```
|
||||
|
||||
## Different .env Files for Different Environments
|
||||
|
||||
If you use `.env.development` and `.env.production`, both should be in inputs.
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"inputs": [
|
||||
"$TURBO_DEFAULT$",
|
||||
".env",
|
||||
".env.local",
|
||||
".env.development",
|
||||
".env.development.local",
|
||||
".env.production",
|
||||
".env.production.local"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Next.js Example
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://turborepo.dev/schema.v2.json",
|
||||
"globalEnv": ["CI", "NODE_ENV", "VERCEL"],
|
||||
"globalPassThroughEnv": ["GITHUB_TOKEN", "VERCEL_URL"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"env": ["DATABASE_URL", "NEXT_PUBLIC_*", "!NEXT_PUBLIC_ANALYTICS_ID"],
|
||||
"passThroughEnv": ["SENTRY_AUTH_TOKEN"],
|
||||
"inputs": [
|
||||
"$TURBO_DEFAULT$",
|
||||
".env",
|
||||
".env.local",
|
||||
".env.production",
|
||||
".env.production.local"
|
||||
],
|
||||
"outputs": [".next/**", "!.next/cache/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This config:
|
||||
|
||||
- Hashes DATABASE*URL and NEXT_PUBLIC*\* vars (except analytics)
|
||||
- Passes through SENTRY_AUTH_TOKEN without hashing
|
||||
- Includes all .env file variants in the hash
|
||||
- Makes CI tokens available globally
|
||||
101
.agents/skills/turborepo/references/environment/modes.md
Normal file
101
.agents/skills/turborepo/references/environment/modes.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Environment Modes
|
||||
|
||||
Turborepo supports different modes for handling environment variables during task execution.
|
||||
|
||||
## Strict Mode (Default)
|
||||
|
||||
Only explicitly configured variables are available to tasks.
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- Tasks only see vars listed in `env`, `globalEnv`, `passThroughEnv`, or `globalPassThroughEnv`
|
||||
- Unlisted vars are filtered out
|
||||
- Tasks fail if they require unlisted variables
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Guarantees cache correctness
|
||||
- Prevents accidental dependencies on system vars
|
||||
- Reproducible builds across machines
|
||||
|
||||
```bash
|
||||
# Explicit (though it's the default)
|
||||
turbo run build --env-mode=strict
|
||||
```
|
||||
|
||||
## Loose Mode
|
||||
|
||||
All system environment variables are available to tasks.
|
||||
|
||||
```bash
|
||||
turbo run build --env-mode=loose
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- Every system env var is passed through
|
||||
- Only vars in `env`/`globalEnv` affect the hash
|
||||
- Other vars are available but NOT hashed
|
||||
|
||||
**Risks:**
|
||||
|
||||
- Cache may restore incorrect results if unhashed vars changed
|
||||
- "Works on my machine" bugs
|
||||
- CI vs local environment mismatches
|
||||
|
||||
**Use case:** Migrating legacy projects or debugging strict mode issues.
|
||||
|
||||
## Framework Inference (Automatic)
|
||||
|
||||
Turborepo automatically detects frameworks and includes their conventional env vars.
|
||||
|
||||
### Inferred Variables by Framework
|
||||
|
||||
| Framework | Pattern |
|
||||
| ---------------- | ------------------- |
|
||||
| Next.js | `NEXT_PUBLIC_*` |
|
||||
| Vite | `VITE_*` |
|
||||
| Create React App | `REACT_APP_*` |
|
||||
| Gatsby | `GATSBY_*` |
|
||||
| Nuxt | `NUXT_*`, `NITRO_*` |
|
||||
| Expo | `EXPO_PUBLIC_*` |
|
||||
| Astro | `PUBLIC_*` |
|
||||
| SvelteKit | `PUBLIC_*` |
|
||||
| Remix | `REMIX_*` |
|
||||
| Redwood | `REDWOOD_ENV_*` |
|
||||
| Sanity | `SANITY_STUDIO_*` |
|
||||
| Solid | `VITE_*` |
|
||||
|
||||
### Disabling Framework Inference
|
||||
|
||||
Globally via CLI:
|
||||
|
||||
```bash
|
||||
turbo run build --framework-inference=false
|
||||
```
|
||||
|
||||
Or exclude specific patterns in config:
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"build": {
|
||||
"env": ["!NEXT_PUBLIC_*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Why Disable?
|
||||
|
||||
- You want explicit control over all env vars
|
||||
- Framework vars shouldn't bust the cache (e.g., analytics IDs)
|
||||
- Debugging unexpected cache misses
|
||||
|
||||
## Checking Environment Mode
|
||||
|
||||
Use `--dry` to see which vars affect each task:
|
||||
|
||||
```bash
|
||||
turbo run build --dry=json | jq '.tasks[].environmentVariables'
|
||||
```
|
||||
148
.agents/skills/turborepo/references/filtering/RULE.md
Normal file
148
.agents/skills/turborepo/references/filtering/RULE.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Turborepo Filter Syntax Reference
|
||||
|
||||
## Running Only Changed Packages: `--affected`
|
||||
|
||||
**The primary way to run only changed packages is `--affected`:**
|
||||
|
||||
```bash
|
||||
# Run build/test/lint only in changed packages and their dependents
|
||||
turbo run build test lint --affected
|
||||
```
|
||||
|
||||
This compares your current branch to the default branch (usually `main` or `master`) and runs tasks in:
|
||||
|
||||
1. Packages with file changes
|
||||
2. Packages that depend on changed packages (dependents)
|
||||
|
||||
### Why Include Dependents?
|
||||
|
||||
If you change `@repo/ui`, packages that import `@repo/ui` (like `apps/web`) need to re-run their tasks to verify they still work with the changes.
|
||||
|
||||
### Customizing --affected
|
||||
|
||||
```bash
|
||||
# Use a different base branch
|
||||
turbo run build --affected --affected-base=origin/develop
|
||||
|
||||
# Use a different head (current state)
|
||||
turbo run build --affected --affected-head=HEAD~5
|
||||
```
|
||||
|
||||
### Common CI Pattern
|
||||
|
||||
```yaml
|
||||
# .github/workflows/ci.yml
|
||||
- run: turbo run build test lint --affected
|
||||
```
|
||||
|
||||
This is the most efficient CI setup - only run tasks for what actually changed.
|
||||
|
||||
---
|
||||
|
||||
## Manual Git Comparison with --filter
|
||||
|
||||
For more control, use `--filter` with git comparison syntax:
|
||||
|
||||
```bash
|
||||
# Changed packages + dependents (same as --affected)
|
||||
turbo run build --filter=...[origin/main]
|
||||
|
||||
# Only changed packages (no dependents)
|
||||
turbo run build --filter=[origin/main]
|
||||
|
||||
# Changed packages + dependencies (packages they import)
|
||||
turbo run build --filter=[origin/main]...
|
||||
|
||||
# Changed since last commit
|
||||
turbo run build --filter=...[HEAD^1]
|
||||
|
||||
# Changed between two commits
|
||||
turbo run build --filter=[a1b2c3d...e4f5g6h]
|
||||
```
|
||||
|
||||
### Comparison Syntax
|
||||
|
||||
| Syntax | Meaning |
|
||||
| ------------- | ------------------------------------- |
|
||||
| `[ref]` | Packages changed since `ref` |
|
||||
| `...[ref]` | Changed packages + their dependents |
|
||||
| `[ref]...` | Changed packages + their dependencies |
|
||||
| `...[ref]...` | Dependencies, changed, AND dependents |
|
||||
|
||||
---
|
||||
|
||||
## Other Filter Types
|
||||
|
||||
Filters select which packages to include in a `turbo run` invocation.
|
||||
|
||||
### Basic Syntax
|
||||
|
||||
```bash
|
||||
turbo run build --filter=<package-name>
|
||||
turbo run build -F <package-name>
|
||||
```
|
||||
|
||||
Multiple filters combine as a union (packages matching ANY filter run).
|
||||
|
||||
### By Package Name
|
||||
|
||||
```bash
|
||||
--filter=web # exact match
|
||||
--filter=@acme/* # scope glob
|
||||
--filter=*-app # name glob
|
||||
```
|
||||
|
||||
### By Directory
|
||||
|
||||
```bash
|
||||
--filter=./apps/* # all packages in apps/
|
||||
--filter=./packages/ui # specific directory
|
||||
```
|
||||
|
||||
### By Dependencies/Dependents
|
||||
|
||||
| Syntax | Meaning |
|
||||
| ----------- | -------------------------------------- |
|
||||
| `pkg...` | Package AND all its dependencies |
|
||||
| `...pkg` | Package AND all its dependents |
|
||||
| `...pkg...` | Dependencies, package, AND dependents |
|
||||
| `^pkg...` | Only dependencies (exclude pkg itself) |
|
||||
| `...^pkg` | Only dependents (exclude pkg itself) |
|
||||
|
||||
### Negation
|
||||
|
||||
Exclude packages with `!`:
|
||||
|
||||
```bash
|
||||
--filter=!web # exclude web
|
||||
--filter=./apps/* --filter=!admin # apps except admin
|
||||
```
|
||||
|
||||
### Task Identifiers
|
||||
|
||||
Run a specific task in a specific package:
|
||||
|
||||
```bash
|
||||
turbo run web#build # only web's build task
|
||||
turbo run web#build api#test # web build + api test
|
||||
```
|
||||
|
||||
### Combining Filters
|
||||
|
||||
Multiple `--filter` flags create a union:
|
||||
|
||||
```bash
|
||||
turbo run build --filter=web --filter=api # runs in both
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Changed Packages
|
||||
|
||||
| Goal | Command |
|
||||
| ---------------------------------- | ----------------------------------------------------------- |
|
||||
| Changed + dependents (recommended) | `turbo run build --affected` |
|
||||
| Custom base branch | `turbo run build --affected --affected-base=origin/develop` |
|
||||
| Only changed (no dependents) | `turbo run build --filter=[origin/main]` |
|
||||
| Changed + dependencies | `turbo run build --filter=[origin/main]...` |
|
||||
| Since last commit | `turbo run build --filter=...[HEAD^1]` |
|
||||
152
.agents/skills/turborepo/references/filtering/patterns.md
Normal file
152
.agents/skills/turborepo/references/filtering/patterns.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Common Filter Patterns
|
||||
|
||||
Practical examples for typical monorepo scenarios.
|
||||
|
||||
## Single Package
|
||||
|
||||
Run task in one package:
|
||||
|
||||
```bash
|
||||
turbo run build --filter=web
|
||||
turbo run test --filter=@acme/api
|
||||
```
|
||||
|
||||
## Package with Dependencies
|
||||
|
||||
Build a package and everything it depends on:
|
||||
|
||||
```bash
|
||||
turbo run build --filter=web...
|
||||
```
|
||||
|
||||
Useful for: ensuring all dependencies are built before the target.
|
||||
|
||||
## Package Dependents
|
||||
|
||||
Run in all packages that depend on a library:
|
||||
|
||||
```bash
|
||||
turbo run test --filter=...ui
|
||||
```
|
||||
|
||||
Useful for: testing consumers after changing a shared package.
|
||||
|
||||
## Dependents Only (Exclude Target)
|
||||
|
||||
Test packages that depend on ui, but not ui itself:
|
||||
|
||||
```bash
|
||||
turbo run test --filter=...^ui
|
||||
```
|
||||
|
||||
## Changed Packages
|
||||
|
||||
Run only in packages with file changes since last commit:
|
||||
|
||||
```bash
|
||||
turbo run lint --filter=[HEAD^1]
|
||||
```
|
||||
|
||||
Since a specific branch point:
|
||||
|
||||
```bash
|
||||
turbo run lint --filter=[main...HEAD]
|
||||
```
|
||||
|
||||
## Changed + Dependents (PR Builds)
|
||||
|
||||
Run in changed packages AND packages that depend on them:
|
||||
|
||||
```bash
|
||||
turbo run build test --filter=...[HEAD^1]
|
||||
```
|
||||
|
||||
Or use the shortcut:
|
||||
|
||||
```bash
|
||||
turbo run build test --affected
|
||||
```
|
||||
|
||||
## Directory-Based
|
||||
|
||||
Run in all apps:
|
||||
|
||||
```bash
|
||||
turbo run build --filter=./apps/*
|
||||
```
|
||||
|
||||
Run in specific directories:
|
||||
|
||||
```bash
|
||||
turbo run build --filter=./apps/web --filter=./apps/api
|
||||
```
|
||||
|
||||
## Scope-Based
|
||||
|
||||
Run in all packages under a scope:
|
||||
|
||||
```bash
|
||||
turbo run build --filter=@acme/*
|
||||
```
|
||||
|
||||
## Exclusions
|
||||
|
||||
Run in all apps except admin:
|
||||
|
||||
```bash
|
||||
turbo run build --filter=./apps/* --filter=!admin
|
||||
```
|
||||
|
||||
Run everywhere except specific packages:
|
||||
|
||||
```bash
|
||||
turbo run lint --filter=!legacy-app --filter=!deprecated-pkg
|
||||
```
|
||||
|
||||
## Complex Combinations
|
||||
|
||||
Apps that changed, plus their dependents:
|
||||
|
||||
```bash
|
||||
turbo run build --filter=...[HEAD^1] --filter=./apps/*
|
||||
```
|
||||
|
||||
All packages except docs, but only if changed:
|
||||
|
||||
```bash
|
||||
turbo run build --filter=[main...HEAD] --filter=!docs
|
||||
```
|
||||
|
||||
## Debugging Filters
|
||||
|
||||
Use `--dry` to see what would run without executing:
|
||||
|
||||
```bash
|
||||
turbo run build --filter=web... --dry
|
||||
```
|
||||
|
||||
Use `--dry=json` for machine-readable output:
|
||||
|
||||
```bash
|
||||
turbo run build --filter=...[HEAD^1] --dry=json
|
||||
```
|
||||
|
||||
## CI/CD Patterns
|
||||
|
||||
PR validation (most common):
|
||||
|
||||
```bash
|
||||
turbo run build test lint --affected
|
||||
```
|
||||
|
||||
Deploy only changed apps:
|
||||
|
||||
```bash
|
||||
turbo run deploy --filter=./apps/* --filter=[main...HEAD]
|
||||
```
|
||||
|
||||
Full rebuild of specific app and deps:
|
||||
|
||||
```bash
|
||||
turbo run build --filter=production-app...
|
||||
```
|
||||
99
.agents/skills/turborepo/references/watch/RULE.md
Normal file
99
.agents/skills/turborepo/references/watch/RULE.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# turbo watch
|
||||
|
||||
Full docs: https://turborepo.dev/docs/reference/watch
|
||||
|
||||
Re-run tasks automatically when code changes. Dependency-aware.
|
||||
|
||||
```bash
|
||||
turbo watch [tasks]
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```bash
|
||||
# Watch and re-run build task when code changes
|
||||
turbo watch build
|
||||
|
||||
# Watch multiple tasks
|
||||
turbo watch build test lint
|
||||
```
|
||||
|
||||
Tasks re-run in order configured in `turbo.json` when source files change.
|
||||
|
||||
## With Persistent Tasks
|
||||
|
||||
Persistent tasks (`"persistent": true`) won't exit, so they can't be depended on. They work the same in `turbo watch` as `turbo run`.
|
||||
|
||||
### Dependency-Aware Persistent Tasks
|
||||
|
||||
If your tool has built-in watching (like `next dev`), use its watcher:
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"dev": {
|
||||
"persistent": true,
|
||||
"cache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Non-Dependency-Aware Tools
|
||||
|
||||
For tools that don't detect dependency changes, use `interruptible`:
|
||||
|
||||
```json
|
||||
{
|
||||
"tasks": {
|
||||
"dev": {
|
||||
"persistent": true,
|
||||
"interruptible": true,
|
||||
"cache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`turbo watch` will restart interruptible tasks when dependencies change.
|
||||
|
||||
## Limitations
|
||||
|
||||
### Caching
|
||||
|
||||
Caching is experimental with watch mode:
|
||||
|
||||
```bash
|
||||
turbo watch your-tasks --experimental-write-cache
|
||||
```
|
||||
|
||||
### Task Outputs in Source Control
|
||||
|
||||
If tasks write files tracked by git, watch mode may loop infinitely. Watch mode uses file hashes to prevent this but it's not foolproof.
|
||||
|
||||
**Recommendation**: Remove task outputs from git.
|
||||
|
||||
## vs turbo run
|
||||
|
||||
| Feature | `turbo run` | `turbo watch` |
|
||||
| ----------------- | ----------- | ------------- |
|
||||
| Runs once | Yes | No |
|
||||
| Re-runs on change | No | Yes |
|
||||
| Caching | Full | Experimental |
|
||||
| Use case | CI, one-off | Development |
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Development Workflow
|
||||
|
||||
```bash
|
||||
# Run dev servers and watch for build changes
|
||||
turbo watch dev build
|
||||
```
|
||||
|
||||
### Type Checking During Development
|
||||
|
||||
```bash
|
||||
# Watch and re-run type checks
|
||||
turbo watch check-types
|
||||
```
|
||||
917
.agents/skills/vercel-composition-patterns/AGENTS.md
Normal file
917
.agents/skills/vercel-composition-patterns/AGENTS.md
Normal file
@@ -0,0 +1,917 @@
|
||||
# React Composition Patterns
|
||||
|
||||
**Version 1.0.0**
|
||||
Engineering
|
||||
January 2026
|
||||
|
||||
> **Note:**
|
||||
> This document is mainly for agents and LLMs to follow when maintaining,
|
||||
> generating, or refactoring React codebases using composition. Humans
|
||||
> may also find it useful, but guidance here is optimized for automation
|
||||
> and consistency by AI-assisted workflows.
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
Composition patterns for building flexible, maintainable React components. Avoid boolean prop proliferation by using compound components, lifting state, and composing internals. These patterns make codebases easier for both humans and AI agents to work with as they scale.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Component Architecture](#1-component-architecture) — **HIGH**
|
||||
- 1.1 [Avoid Boolean Prop Proliferation](#11-avoid-boolean-prop-proliferation)
|
||||
- 1.2 [Use Compound Components](#12-use-compound-components)
|
||||
2. [State Management](#2-state-management) — **MEDIUM**
|
||||
- 2.1 [Decouple State Management from UI](#21-decouple-state-management-from-ui)
|
||||
- 2.2 [Define Generic Context Interfaces for Dependency Injection](#22-define-generic-context-interfaces-for-dependency-injection)
|
||||
- 2.3 [Lift State into Provider Components](#23-lift-state-into-provider-components)
|
||||
3. [Implementation Patterns](#3-implementation-patterns) — **MEDIUM**
|
||||
- 3.1 [Create Explicit Component Variants](#31-create-explicit-component-variants)
|
||||
- 3.2 [Prefer Composing Children Over Render Props](#32-prefer-composing-children-over-render-props)
|
||||
4. [React 19 APIs](#4-react-19-apis) — **MEDIUM**
|
||||
- 4.1 [React 19 API Changes](#41-react-19-api-changes)
|
||||
|
||||
---
|
||||
|
||||
## 1. Component Architecture
|
||||
|
||||
**Impact: HIGH**
|
||||
|
||||
Fundamental patterns for structuring components to avoid prop
|
||||
proliferation and enable flexible composition.
|
||||
|
||||
### 1.1 Avoid Boolean Prop Proliferation
|
||||
|
||||
**Impact: CRITICAL (prevents unmaintainable component variants)**
|
||||
|
||||
Don't add boolean props like `isThread`, `isEditing`, `isDMThread` to customize
|
||||
|
||||
component behavior. Each boolean doubles possible states and creates
|
||||
|
||||
unmaintainable conditional logic. Use composition instead.
|
||||
|
||||
**Incorrect: boolean props create exponential complexity**
|
||||
|
||||
```tsx
|
||||
function Composer({
|
||||
onSubmit,
|
||||
isThread,
|
||||
channelId,
|
||||
isDMThread,
|
||||
dmId,
|
||||
isEditing,
|
||||
isForwarding,
|
||||
}: Props) {
|
||||
return (
|
||||
<form>
|
||||
<Header />
|
||||
<Input />
|
||||
{isDMThread ? (
|
||||
<AlsoSendToDMField id={dmId} />
|
||||
) : isThread ? (
|
||||
<AlsoSendToChannelField id={channelId} />
|
||||
) : null}
|
||||
{isEditing ? <EditActions /> : isForwarding ? <ForwardActions /> : <DefaultActions />}
|
||||
<Footer onSubmit={onSubmit} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct: composition eliminates conditionals**
|
||||
|
||||
```tsx
|
||||
// Channel composer
|
||||
function ChannelComposer() {
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Header />
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Attachments />
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
);
|
||||
}
|
||||
|
||||
// Thread composer - adds "also send to channel" field
|
||||
function ThreadComposer({ channelId }: { channelId: string }) {
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Header />
|
||||
<Composer.Input />
|
||||
<AlsoSendToChannelField id={channelId} />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
);
|
||||
}
|
||||
|
||||
// Edit composer - different footer actions
|
||||
function EditComposer() {
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.CancelEdit />
|
||||
<Composer.SaveEdit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Each variant is explicit about what it renders. We can share internals without
|
||||
|
||||
sharing a single monolithic parent.
|
||||
|
||||
### 1.2 Use Compound Components
|
||||
|
||||
**Impact: HIGH (enables flexible composition without prop drilling)**
|
||||
|
||||
Structure complex components as compound components with a shared context. Each
|
||||
|
||||
subcomponent accesses shared state via context, not props. Consumers compose the
|
||||
|
||||
pieces they need.
|
||||
|
||||
**Incorrect: monolithic component with render props**
|
||||
|
||||
```tsx
|
||||
function Composer({
|
||||
renderHeader,
|
||||
renderFooter,
|
||||
renderActions,
|
||||
showAttachments,
|
||||
showFormatting,
|
||||
showEmojis,
|
||||
}: Props) {
|
||||
return (
|
||||
<form>
|
||||
{renderHeader?.()}
|
||||
<Input />
|
||||
{showAttachments && <Attachments />}
|
||||
{renderFooter ? (
|
||||
renderFooter()
|
||||
) : (
|
||||
<Footer>
|
||||
{showFormatting && <Formatting />}
|
||||
{showEmojis && <Emojis />}
|
||||
{renderActions?.()}
|
||||
</Footer>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct: compound components with shared context**
|
||||
|
||||
```tsx
|
||||
const ComposerContext = createContext<ComposerContextValue | null>(null);
|
||||
|
||||
function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
|
||||
return <ComposerContext value={{ state, actions, meta }}>{children}</ComposerContext>;
|
||||
}
|
||||
|
||||
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
||||
return <form>{children}</form>;
|
||||
}
|
||||
|
||||
function ComposerInput() {
|
||||
const {
|
||||
state,
|
||||
actions: { update },
|
||||
meta: { inputRef },
|
||||
} = use(ComposerContext);
|
||||
return (
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
value={state.input}
|
||||
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ComposerSubmit() {
|
||||
const {
|
||||
actions: { submit },
|
||||
} = use(ComposerContext);
|
||||
return <Button onPress={submit}>Send</Button>;
|
||||
}
|
||||
|
||||
// Export as compound component
|
||||
const Composer = {
|
||||
Provider: ComposerProvider,
|
||||
Frame: ComposerFrame,
|
||||
Input: ComposerInput,
|
||||
Submit: ComposerSubmit,
|
||||
Header: ComposerHeader,
|
||||
Footer: ComposerFooter,
|
||||
Attachments: ComposerAttachments,
|
||||
Formatting: ComposerFormatting,
|
||||
Emojis: ComposerEmojis,
|
||||
};
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
<Composer.Provider state={state} actions={actions} meta={meta}>
|
||||
<Composer.Frame>
|
||||
<Composer.Header />
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</Composer.Provider>
|
||||
```
|
||||
|
||||
Consumers explicitly compose exactly what they need. No hidden conditionals. And the state, actions and meta are dependency-injected by a parent provider, allowing multiple usages of the same component structure.
|
||||
|
||||
---
|
||||
|
||||
## 2. State Management
|
||||
|
||||
**Impact: MEDIUM**
|
||||
|
||||
Patterns for lifting state and managing shared context across
|
||||
composed components.
|
||||
|
||||
### 2.1 Decouple State Management from UI
|
||||
|
||||
**Impact: MEDIUM (enables swapping state implementations without changing UI)**
|
||||
|
||||
The provider component should be the only place that knows how state is managed.
|
||||
|
||||
UI components consume the context interface—they don't know if state comes from
|
||||
|
||||
useState, Zustand, or a server sync.
|
||||
|
||||
**Incorrect: UI coupled to state implementation**
|
||||
|
||||
```tsx
|
||||
function ChannelComposer({ channelId }: { channelId: string }) {
|
||||
// UI component knows about global state implementation
|
||||
const state = useGlobalChannelState(channelId);
|
||||
const { submit, updateInput } = useChannelSync(channelId);
|
||||
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Input value={state.input} onChange={(text) => sync.updateInput(text)} />
|
||||
<Composer.Submit onPress={() => sync.submit()} />
|
||||
</Composer.Frame>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct: state management isolated in provider**
|
||||
|
||||
```tsx
|
||||
// Provider handles all state management details
|
||||
function ChannelProvider({
|
||||
channelId,
|
||||
children,
|
||||
}: {
|
||||
channelId: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { state, update, submit } = useGlobalChannel(channelId);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
return (
|
||||
<Composer.Provider state={state} actions={{ update, submit }} meta={{ inputRef }}>
|
||||
{children}
|
||||
</Composer.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// UI component only knows about the context interface
|
||||
function ChannelComposer() {
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Header />
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
function Channel({ channelId }: { channelId: string }) {
|
||||
return (
|
||||
<ChannelProvider channelId={channelId}>
|
||||
<ChannelComposer />
|
||||
</ChannelProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Different providers, same UI:**
|
||||
|
||||
```tsx
|
||||
// Local state for ephemeral forms
|
||||
function ForwardMessageProvider({ children }) {
|
||||
const [state, setState] = useState(initialState);
|
||||
const forwardMessage = useForwardMessage();
|
||||
|
||||
return (
|
||||
<Composer.Provider state={state} actions={{ update: setState, submit: forwardMessage }}>
|
||||
{children}
|
||||
</Composer.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Global synced state for channels
|
||||
function ChannelProvider({ channelId, children }) {
|
||||
const { state, update, submit } = useGlobalChannel(channelId);
|
||||
|
||||
return (
|
||||
<Composer.Provider state={state} actions={{ update, submit }}>
|
||||
{children}
|
||||
</Composer.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The same `Composer.Input` component works with both providers because it only
|
||||
|
||||
depends on the context interface, not the implementation.
|
||||
|
||||
### 2.2 Define Generic Context Interfaces for Dependency Injection
|
||||
|
||||
**Impact: HIGH (enables dependency-injectable state across use-cases)**
|
||||
|
||||
Define a **generic interface** for your component context with three parts:
|
||||
|
||||
`state`, `actions`, and `meta`. This interface is a contract that any provider
|
||||
|
||||
can implement—enabling the same UI components to work with completely different
|
||||
|
||||
state implementations.
|
||||
|
||||
**Core principle:** Lift state, compose internals, make state
|
||||
|
||||
dependency-injectable.
|
||||
|
||||
**Incorrect: UI coupled to specific state implementation**
|
||||
|
||||
```tsx
|
||||
function ComposerInput() {
|
||||
// Tightly coupled to a specific hook
|
||||
const { input, setInput } = useChannelComposerState();
|
||||
return <TextInput value={input} onChangeText={setInput} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct: generic interface enables dependency injection**
|
||||
|
||||
```tsx
|
||||
// Define a GENERIC interface that any provider can implement
|
||||
interface ComposerState {
|
||||
input: string;
|
||||
attachments: Attachment[];
|
||||
isSubmitting: boolean;
|
||||
}
|
||||
|
||||
interface ComposerActions {
|
||||
update: (updater: (state: ComposerState) => ComposerState) => void;
|
||||
submit: () => void;
|
||||
}
|
||||
|
||||
interface ComposerMeta {
|
||||
inputRef: React.RefObject<TextInput>;
|
||||
}
|
||||
|
||||
interface ComposerContextValue {
|
||||
state: ComposerState;
|
||||
actions: ComposerActions;
|
||||
meta: ComposerMeta;
|
||||
}
|
||||
|
||||
const ComposerContext = createContext<ComposerContextValue | null>(null);
|
||||
```
|
||||
|
||||
**UI components consume the interface, not the implementation:**
|
||||
|
||||
```tsx
|
||||
function ComposerInput() {
|
||||
const {
|
||||
state,
|
||||
actions: { update },
|
||||
meta,
|
||||
} = use(ComposerContext);
|
||||
|
||||
// This component works with ANY provider that implements the interface
|
||||
return (
|
||||
<TextInput
|
||||
ref={meta.inputRef}
|
||||
value={state.input}
|
||||
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Different providers implement the same interface:**
|
||||
|
||||
```tsx
|
||||
// Provider A: Local state for ephemeral forms
|
||||
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, setState] = useState(initialState);
|
||||
const inputRef = useRef(null);
|
||||
const submit = useForwardMessage();
|
||||
|
||||
return (
|
||||
<ComposerContext
|
||||
value={{
|
||||
state,
|
||||
actions: { update: setState, submit },
|
||||
meta: { inputRef },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ComposerContext>
|
||||
);
|
||||
}
|
||||
|
||||
// Provider B: Global synced state for channels
|
||||
function ChannelProvider({ channelId, children }: Props) {
|
||||
const { state, update, submit } = useGlobalChannel(channelId);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
return (
|
||||
<ComposerContext
|
||||
value={{
|
||||
state,
|
||||
actions: { update, submit },
|
||||
meta: { inputRef },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ComposerContext>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**The same composed UI works with both:**
|
||||
|
||||
```tsx
|
||||
// Works with ForwardMessageProvider (local state)
|
||||
<ForwardMessageProvider>
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<Composer.Submit />
|
||||
</Composer.Frame>
|
||||
</ForwardMessageProvider>
|
||||
|
||||
// Works with ChannelProvider (global synced state)
|
||||
<ChannelProvider channelId="abc">
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<Composer.Submit />
|
||||
</Composer.Frame>
|
||||
</ChannelProvider>
|
||||
```
|
||||
|
||||
**Custom UI outside the component can access state and actions:**
|
||||
|
||||
```tsx
|
||||
function ForwardMessageDialog() {
|
||||
return (
|
||||
<ForwardMessageProvider>
|
||||
<Dialog>
|
||||
{/* The composer UI */}
|
||||
<Composer.Frame>
|
||||
<Composer.Input placeholder="Add a message, if you'd like." />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
|
||||
{/* Custom UI OUTSIDE the composer, but INSIDE the provider */}
|
||||
<MessagePreview />
|
||||
|
||||
{/* Actions at the bottom of the dialog */}
|
||||
<DialogActions>
|
||||
<CancelButton />
|
||||
<ForwardButton />
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</ForwardMessageProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// This button lives OUTSIDE Composer.Frame but can still submit based on its context!
|
||||
function ForwardButton() {
|
||||
const {
|
||||
actions: { submit },
|
||||
} = use(ComposerContext);
|
||||
return <Button onPress={submit}>Forward</Button>;
|
||||
}
|
||||
|
||||
// This preview lives OUTSIDE Composer.Frame but can read composer's state!
|
||||
function MessagePreview() {
|
||||
const { state } = use(ComposerContext);
|
||||
return <Preview message={state.input} attachments={state.attachments} />;
|
||||
}
|
||||
```
|
||||
|
||||
The provider boundary is what matters—not the visual nesting. Components that
|
||||
|
||||
need shared state don't have to be inside the `Composer.Frame`. They just need
|
||||
|
||||
to be within the provider.
|
||||
|
||||
The `ForwardButton` and `MessagePreview` are not visually inside the composer
|
||||
|
||||
box, but they can still access its state and actions. This is the power of
|
||||
|
||||
lifting state into providers.
|
||||
|
||||
The UI is reusable bits you compose together. The state is dependency-injected
|
||||
|
||||
by the provider. Swap the provider, keep the UI.
|
||||
|
||||
### 2.3 Lift State into Provider Components
|
||||
|
||||
**Impact: HIGH (enables state sharing outside component boundaries)**
|
||||
|
||||
Move state management into dedicated provider components. This allows sibling
|
||||
|
||||
components outside the main UI to access and modify state without prop drilling
|
||||
|
||||
or awkward refs.
|
||||
|
||||
**Incorrect: state trapped inside component**
|
||||
|
||||
```tsx
|
||||
function ForwardMessageComposer() {
|
||||
const [state, setState] = useState(initialState);
|
||||
const forwardMessage = useForwardMessage();
|
||||
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<Composer.Footer />
|
||||
</Composer.Frame>
|
||||
);
|
||||
}
|
||||
|
||||
// Problem: How does this button access composer state?
|
||||
function ForwardMessageDialog() {
|
||||
return (
|
||||
<Dialog>
|
||||
<ForwardMessageComposer />
|
||||
<MessagePreview /> {/* Needs composer state */}
|
||||
<DialogActions>
|
||||
<CancelButton />
|
||||
<ForwardButton /> {/* Needs to call submit */}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Incorrect: useEffect to sync state up**
|
||||
|
||||
```tsx
|
||||
function ForwardMessageDialog() {
|
||||
const [input, setInput] = useState("");
|
||||
return (
|
||||
<Dialog>
|
||||
<ForwardMessageComposer onInputChange={setInput} />
|
||||
<MessagePreview input={input} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardMessageComposer({ onInputChange }) {
|
||||
const [state, setState] = useState(initialState);
|
||||
useEffect(() => {
|
||||
onInputChange(state.input); // Sync on every change 😬
|
||||
}, [state.input]);
|
||||
}
|
||||
```
|
||||
|
||||
**Incorrect: reading state from ref on submit**
|
||||
|
||||
```tsx
|
||||
function ForwardMessageDialog() {
|
||||
const stateRef = useRef(null);
|
||||
return (
|
||||
<Dialog>
|
||||
<ForwardMessageComposer stateRef={stateRef} />
|
||||
<ForwardButton onPress={() => submit(stateRef.current)} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct: state lifted to provider**
|
||||
|
||||
```tsx
|
||||
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, setState] = useState(initialState);
|
||||
const forwardMessage = useForwardMessage();
|
||||
const inputRef = useRef(null);
|
||||
|
||||
return (
|
||||
<Composer.Provider
|
||||
state={state}
|
||||
actions={{ update: setState, submit: forwardMessage }}
|
||||
meta={{ inputRef }}
|
||||
>
|
||||
{children}
|
||||
</Composer.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardMessageDialog() {
|
||||
return (
|
||||
<ForwardMessageProvider>
|
||||
<Dialog>
|
||||
<ForwardMessageComposer />
|
||||
<MessagePreview /> {/* Custom components can access state and actions */}
|
||||
<DialogActions>
|
||||
<CancelButton />
|
||||
<ForwardButton /> {/* Custom components can access state and actions */}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</ForwardMessageProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardButton() {
|
||||
const { actions } = use(Composer.Context);
|
||||
return <Button onPress={actions.submit}>Forward</Button>;
|
||||
}
|
||||
```
|
||||
|
||||
The ForwardButton lives outside the Composer.Frame but still has access to the
|
||||
|
||||
submit action because it's within the provider. Even though it's a one-off
|
||||
|
||||
component, it can still access the composer's state and actions from outside the
|
||||
|
||||
UI itself.
|
||||
|
||||
**Key insight:** Components that need shared state don't have to be visually
|
||||
|
||||
nested inside each other—they just need to be within the same provider.
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation Patterns
|
||||
|
||||
**Impact: MEDIUM**
|
||||
|
||||
Specific techniques for implementing compound components and
|
||||
context providers.
|
||||
|
||||
### 3.1 Create Explicit Component Variants
|
||||
|
||||
**Impact: MEDIUM (self-documenting code, no hidden conditionals)**
|
||||
|
||||
Instead of one component with many boolean props, create explicit variant
|
||||
|
||||
components. Each variant composes the pieces it needs. The code documents
|
||||
|
||||
itself.
|
||||
|
||||
**Incorrect: one component, many modes**
|
||||
|
||||
```tsx
|
||||
// What does this component actually render?
|
||||
<Composer isThread isEditing={false} channelId="abc" showAttachments showFormatting={false} />
|
||||
```
|
||||
|
||||
**Correct: explicit variants**
|
||||
|
||||
```tsx
|
||||
// Immediately clear what this renders
|
||||
<ThreadComposer channelId="abc" />
|
||||
|
||||
// Or
|
||||
<EditMessageComposer messageId="xyz" />
|
||||
|
||||
// Or
|
||||
<ForwardMessageComposer messageId="123" />
|
||||
```
|
||||
|
||||
Each implementation is unique, explicit and self-contained. Yet they can each
|
||||
|
||||
use shared parts.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```tsx
|
||||
function ThreadComposer({ channelId }: { channelId: string }) {
|
||||
return (
|
||||
<ThreadProvider channelId={channelId}>
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<AlsoSendToChannelField channelId={channelId} />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</ThreadProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function EditMessageComposer({ messageId }: { messageId: string }) {
|
||||
return (
|
||||
<EditMessageProvider messageId={messageId}>
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.CancelEdit />
|
||||
<Composer.SaveEdit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</EditMessageProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardMessageComposer({ messageId }: { messageId: string }) {
|
||||
return (
|
||||
<ForwardMessageProvider messageId={messageId}>
|
||||
<Composer.Frame>
|
||||
<Composer.Input placeholder="Add a message, if you'd like." />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.Mentions />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</ForwardMessageProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Each variant is explicit about:
|
||||
|
||||
- What provider/state it uses
|
||||
|
||||
- What UI elements it includes
|
||||
|
||||
- What actions are available
|
||||
|
||||
No boolean prop combinations to reason about. No impossible states.
|
||||
|
||||
### 3.2 Prefer Composing Children Over Render Props
|
||||
|
||||
**Impact: MEDIUM (cleaner composition, better readability)**
|
||||
|
||||
Use `children` for composition instead of `renderX` props. Children are more
|
||||
|
||||
readable, compose naturally, and don't require understanding callback
|
||||
|
||||
signatures.
|
||||
|
||||
**Incorrect: render props**
|
||||
|
||||
```tsx
|
||||
function Composer({
|
||||
renderHeader,
|
||||
renderFooter,
|
||||
renderActions,
|
||||
}: {
|
||||
renderHeader?: () => React.ReactNode;
|
||||
renderFooter?: () => React.ReactNode;
|
||||
renderActions?: () => React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<form>
|
||||
{renderHeader?.()}
|
||||
<Input />
|
||||
{renderFooter ? renderFooter() : <DefaultFooter />}
|
||||
{renderActions?.()}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage is awkward and inflexible
|
||||
return (
|
||||
<Composer
|
||||
renderHeader={() => <CustomHeader />}
|
||||
renderFooter={() => (
|
||||
<>
|
||||
<Formatting />
|
||||
<Emojis />
|
||||
</>
|
||||
)}
|
||||
renderActions={() => <SubmitButton />}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
**Correct: compound components with children**
|
||||
|
||||
```tsx
|
||||
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
||||
return <form>{children}</form>;
|
||||
}
|
||||
|
||||
function ComposerFooter({ children }: { children: React.ReactNode }) {
|
||||
return <footer className="flex">{children}</footer>;
|
||||
}
|
||||
|
||||
// Usage is flexible
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<CustomHeader />
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<SubmitButton />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
);
|
||||
```
|
||||
|
||||
**When render props are appropriate:**
|
||||
|
||||
```tsx
|
||||
// Render props work well when you need to pass data back
|
||||
<List data={items} renderItem={({ item, index }) => <Item item={item} index={index} />} />
|
||||
```
|
||||
|
||||
Use render props when the parent needs to provide data or state to the child.
|
||||
|
||||
Use children when composing static structure.
|
||||
|
||||
---
|
||||
|
||||
## 4. React 19 APIs
|
||||
|
||||
**Impact: MEDIUM**
|
||||
|
||||
React 19+ only. Don't use `forwardRef`; use `use()` instead of `useContext()`.
|
||||
|
||||
### 4.1 React 19 API Changes
|
||||
|
||||
**Impact: MEDIUM (cleaner component definitions and context usage)**
|
||||
|
||||
> **⚠️ React 19+ only.** Skip this if you're on React 18 or earlier.
|
||||
|
||||
In React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `use()` replaces `useContext()`.
|
||||
|
||||
**Incorrect: forwardRef in React 19**
|
||||
|
||||
```tsx
|
||||
const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
|
||||
return <TextInput ref={ref} {...props} />;
|
||||
});
|
||||
```
|
||||
|
||||
**Correct: ref as a regular prop**
|
||||
|
||||
```tsx
|
||||
function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
|
||||
return <TextInput ref={ref} {...props} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Incorrect: useContext in React 19**
|
||||
|
||||
```tsx
|
||||
const value = useContext(MyContext);
|
||||
```
|
||||
|
||||
**Correct: use instead of useContext**
|
||||
|
||||
```tsx
|
||||
const value = use(MyContext);
|
||||
```
|
||||
|
||||
`use()` can also be called conditionally, unlike `useContext()`.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
1. [https://react.dev](https://react.dev)
|
||||
2. [https://react.dev/learn/passing-data-deeply-with-context](https://react.dev/learn/passing-data-deeply-with-context)
|
||||
3. [https://react.dev/reference/react/use](https://react.dev/reference/react/use)
|
||||
60
.agents/skills/vercel-composition-patterns/README.md
Normal file
60
.agents/skills/vercel-composition-patterns/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# React Composition Patterns
|
||||
|
||||
A structured repository for React composition patterns that scale. These
|
||||
patterns help avoid boolean prop proliferation by using compound components,
|
||||
lifting state, and composing internals.
|
||||
|
||||
## Structure
|
||||
|
||||
- `rules/` - Individual rule files (one per rule)
|
||||
- `_sections.md` - Section metadata (titles, impacts, descriptions)
|
||||
- `_template.md` - Template for creating new rules
|
||||
- `area-description.md` - Individual rule files
|
||||
- `metadata.json` - Document metadata (version, organization, abstract)
|
||||
- **`AGENTS.md`** - Compiled output (generated)
|
||||
|
||||
## Rules
|
||||
|
||||
### Component Architecture (CRITICAL)
|
||||
|
||||
- `architecture-avoid-boolean-props.md` - Don't add boolean props to customize
|
||||
behavior
|
||||
- `architecture-compound-components.md` - Structure as compound components with
|
||||
shared context
|
||||
|
||||
### State Management (HIGH)
|
||||
|
||||
- `state-lift-state.md` - Lift state into provider components
|
||||
- `state-context-interface.md` - Define clear context interfaces
|
||||
(state/actions/meta)
|
||||
- `state-decouple-implementation.md` - Decouple state management from UI
|
||||
|
||||
### Implementation Patterns (MEDIUM)
|
||||
|
||||
- `patterns-children-over-render-props.md` - Prefer children over renderX props
|
||||
- `patterns-explicit-variants.md` - Create explicit component variants
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Composition over configuration** — Instead of adding props, let consumers
|
||||
compose
|
||||
2. **Lift your state** — State in providers, not trapped in components
|
||||
3. **Compose your internals** — Subcomponents access context, not props
|
||||
4. **Explicit variants** — Create ThreadComposer, EditComposer, not Composer
|
||||
with isThread
|
||||
|
||||
## Creating a New Rule
|
||||
|
||||
1. Copy `rules/_template.md` to `rules/area-description.md`
|
||||
2. Choose the appropriate area prefix:
|
||||
- `architecture-` for Component Architecture
|
||||
- `state-` for State Management
|
||||
- `patterns-` for Implementation Patterns
|
||||
3. Fill in the frontmatter and content
|
||||
4. Ensure you have clear examples with explanations
|
||||
|
||||
## Impact Levels
|
||||
|
||||
- `CRITICAL` - Foundational patterns, prevents unmaintainable code
|
||||
- `HIGH` - Significant maintainability improvements
|
||||
- `MEDIUM` - Good practices for cleaner code
|
||||
88
.agents/skills/vercel-composition-patterns/SKILL.md
Normal file
88
.agents/skills/vercel-composition-patterns/SKILL.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: vercel-composition-patterns
|
||||
description: React composition patterns that scale. Use when refactoring components with
|
||||
boolean prop proliferation, building flexible component libraries, or
|
||||
designing reusable APIs. Triggers on tasks involving compound components,
|
||||
render props, context providers, or component architecture. Includes React 19
|
||||
API changes.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: vercel
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# React Composition Patterns
|
||||
|
||||
Composition patterns for building flexible, maintainable React components. Avoid
|
||||
boolean prop proliferation by using compound components, lifting state, and
|
||||
composing internals. These patterns make codebases easier for both humans and AI
|
||||
agents to work with as they scale.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Reference these guidelines when:
|
||||
|
||||
- Refactoring components with many boolean props
|
||||
- Building reusable component libraries
|
||||
- Designing flexible component APIs
|
||||
- Reviewing component architecture
|
||||
- Working with compound components or context providers
|
||||
|
||||
## Rule Categories by Priority
|
||||
|
||||
| Priority | Category | Impact | Prefix |
|
||||
| -------- | ----------------------- | ------ | --------------- |
|
||||
| 1 | Component Architecture | HIGH | `architecture-` |
|
||||
| 2 | State Management | MEDIUM | `state-` |
|
||||
| 3 | Implementation Patterns | MEDIUM | `patterns-` |
|
||||
| 4 | React 19 APIs | MEDIUM | `react19-` |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### 1. Component Architecture (HIGH)
|
||||
|
||||
- `architecture-avoid-boolean-props` - Don't add boolean props to customize
|
||||
behavior; use composition
|
||||
- `architecture-compound-components` - Structure complex components with shared
|
||||
context
|
||||
|
||||
### 2. State Management (MEDIUM)
|
||||
|
||||
- `state-decouple-implementation` - Provider is the only place that knows how
|
||||
state is managed
|
||||
- `state-context-interface` - Define generic interface with state, actions, meta
|
||||
for dependency injection
|
||||
- `state-lift-state` - Move state into provider components for sibling access
|
||||
|
||||
### 3. Implementation Patterns (MEDIUM)
|
||||
|
||||
- `patterns-explicit-variants` - Create explicit variant components instead of
|
||||
boolean modes
|
||||
- `patterns-children-over-render-props` - Use children for composition instead
|
||||
of renderX props
|
||||
|
||||
### 4. React 19 APIs (MEDIUM)
|
||||
|
||||
> **⚠️ React 19+ only.** Skip this section if using React 18 or earlier.
|
||||
|
||||
- `react19-no-forwardref` - Don't use `forwardRef`; use `use()` instead of `useContext()`
|
||||
|
||||
## How to Use
|
||||
|
||||
Read individual rule files for detailed explanations and code examples:
|
||||
|
||||
```
|
||||
rules/architecture-avoid-boolean-props.md
|
||||
rules/state-context-interface.md
|
||||
```
|
||||
|
||||
Each rule file contains:
|
||||
|
||||
- Brief explanation of why it matters
|
||||
- Incorrect code example with explanation
|
||||
- Correct code example with explanation
|
||||
- Additional context and references
|
||||
|
||||
## Full Compiled Document
|
||||
|
||||
For the complete guide with all rules expanded: `AGENTS.md`
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: Avoid Boolean Prop Proliferation
|
||||
impact: CRITICAL
|
||||
impactDescription: prevents unmaintainable component variants
|
||||
tags: composition, props, architecture
|
||||
---
|
||||
|
||||
## Avoid Boolean Prop Proliferation
|
||||
|
||||
Don't add boolean props like `isThread`, `isEditing`, `isDMThread` to customize
|
||||
component behavior. Each boolean doubles possible states and creates
|
||||
unmaintainable conditional logic. Use composition instead.
|
||||
|
||||
**Incorrect (boolean props create exponential complexity):**
|
||||
|
||||
```tsx
|
||||
function Composer({
|
||||
onSubmit,
|
||||
isThread,
|
||||
channelId,
|
||||
isDMThread,
|
||||
dmId,
|
||||
isEditing,
|
||||
isForwarding,
|
||||
}: Props) {
|
||||
return (
|
||||
<form>
|
||||
<Header />
|
||||
<Input />
|
||||
{isDMThread ? (
|
||||
<AlsoSendToDMField id={dmId} />
|
||||
) : isThread ? (
|
||||
<AlsoSendToChannelField id={channelId} />
|
||||
) : null}
|
||||
{isEditing ? <EditActions /> : isForwarding ? <ForwardActions /> : <DefaultActions />}
|
||||
<Footer onSubmit={onSubmit} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (composition eliminates conditionals):**
|
||||
|
||||
```tsx
|
||||
// Channel composer
|
||||
function ChannelComposer() {
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Header />
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Attachments />
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
);
|
||||
}
|
||||
|
||||
// Thread composer - adds "also send to channel" field
|
||||
function ThreadComposer({ channelId }: { channelId: string }) {
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Header />
|
||||
<Composer.Input />
|
||||
<AlsoSendToChannelField id={channelId} />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
);
|
||||
}
|
||||
|
||||
// Edit composer - different footer actions
|
||||
function EditComposer() {
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.CancelEdit />
|
||||
<Composer.SaveEdit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Each variant is explicit about what it renders. We can share internals without
|
||||
sharing a single monolithic parent.
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: Use Compound Components
|
||||
impact: HIGH
|
||||
impactDescription: enables flexible composition without prop drilling
|
||||
tags: composition, compound-components, architecture
|
||||
---
|
||||
|
||||
## Use Compound Components
|
||||
|
||||
Structure complex components as compound components with a shared context. Each
|
||||
subcomponent accesses shared state via context, not props. Consumers compose the
|
||||
pieces they need.
|
||||
|
||||
**Incorrect (monolithic component with render props):**
|
||||
|
||||
```tsx
|
||||
function Composer({
|
||||
renderHeader,
|
||||
renderFooter,
|
||||
renderActions,
|
||||
showAttachments,
|
||||
showFormatting,
|
||||
showEmojis,
|
||||
}: Props) {
|
||||
return (
|
||||
<form>
|
||||
{renderHeader?.()}
|
||||
<Input />
|
||||
{showAttachments && <Attachments />}
|
||||
{renderFooter ? (
|
||||
renderFooter()
|
||||
) : (
|
||||
<Footer>
|
||||
{showFormatting && <Formatting />}
|
||||
{showEmojis && <Emojis />}
|
||||
{renderActions?.()}
|
||||
</Footer>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (compound components with shared context):**
|
||||
|
||||
```tsx
|
||||
const ComposerContext = createContext<ComposerContextValue | null>(null);
|
||||
|
||||
function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
|
||||
return <ComposerContext value={{ state, actions, meta }}>{children}</ComposerContext>;
|
||||
}
|
||||
|
||||
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
||||
return <form>{children}</form>;
|
||||
}
|
||||
|
||||
function ComposerInput() {
|
||||
const {
|
||||
state,
|
||||
actions: { update },
|
||||
meta: { inputRef },
|
||||
} = use(ComposerContext);
|
||||
return (
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
value={state.input}
|
||||
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ComposerSubmit() {
|
||||
const {
|
||||
actions: { submit },
|
||||
} = use(ComposerContext);
|
||||
return <Button onPress={submit}>Send</Button>;
|
||||
}
|
||||
|
||||
// Export as compound component
|
||||
const Composer = {
|
||||
Provider: ComposerProvider,
|
||||
Frame: ComposerFrame,
|
||||
Input: ComposerInput,
|
||||
Submit: ComposerSubmit,
|
||||
Header: ComposerHeader,
|
||||
Footer: ComposerFooter,
|
||||
Attachments: ComposerAttachments,
|
||||
Formatting: ComposerFormatting,
|
||||
Emojis: ComposerEmojis,
|
||||
};
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
<Composer.Provider state={state} actions={actions} meta={meta}>
|
||||
<Composer.Frame>
|
||||
<Composer.Header />
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</Composer.Provider>
|
||||
```
|
||||
|
||||
Consumers explicitly compose exactly what they need. No hidden conditionals. And the state, actions and meta are dependency-injected by a parent provider, allowing multiple usages of the same component structure.
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: Prefer Composing Children Over Render Props
|
||||
impact: MEDIUM
|
||||
impactDescription: cleaner composition, better readability
|
||||
tags: composition, children, render-props
|
||||
---
|
||||
|
||||
## Prefer Children Over Render Props
|
||||
|
||||
Use `children` for composition instead of `renderX` props. Children are more
|
||||
readable, compose naturally, and don't require understanding callback
|
||||
signatures.
|
||||
|
||||
**Incorrect (render props):**
|
||||
|
||||
```tsx
|
||||
function Composer({
|
||||
renderHeader,
|
||||
renderFooter,
|
||||
renderActions,
|
||||
}: {
|
||||
renderHeader?: () => React.ReactNode;
|
||||
renderFooter?: () => React.ReactNode;
|
||||
renderActions?: () => React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<form>
|
||||
{renderHeader?.()}
|
||||
<Input />
|
||||
{renderFooter ? renderFooter() : <DefaultFooter />}
|
||||
{renderActions?.()}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage is awkward and inflexible
|
||||
return (
|
||||
<Composer
|
||||
renderHeader={() => <CustomHeader />}
|
||||
renderFooter={() => (
|
||||
<>
|
||||
<Formatting />
|
||||
<Emojis />
|
||||
</>
|
||||
)}
|
||||
renderActions={() => <SubmitButton />}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
**Correct (compound components with children):**
|
||||
|
||||
```tsx
|
||||
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
||||
return <form>{children}</form>;
|
||||
}
|
||||
|
||||
function ComposerFooter({ children }: { children: React.ReactNode }) {
|
||||
return <footer className="flex">{children}</footer>;
|
||||
}
|
||||
|
||||
// Usage is flexible
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<CustomHeader />
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<SubmitButton />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
);
|
||||
```
|
||||
|
||||
**When render props are appropriate:**
|
||||
|
||||
```tsx
|
||||
// Render props work well when you need to pass data back
|
||||
<List data={items} renderItem={({ item, index }) => <Item item={item} index={index} />} />
|
||||
```
|
||||
|
||||
Use render props when the parent needs to provide data or state to the child.
|
||||
Use children when composing static structure.
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: Create Explicit Component Variants
|
||||
impact: MEDIUM
|
||||
impactDescription: self-documenting code, no hidden conditionals
|
||||
tags: composition, variants, architecture
|
||||
---
|
||||
|
||||
## Create Explicit Component Variants
|
||||
|
||||
Instead of one component with many boolean props, create explicit variant
|
||||
components. Each variant composes the pieces it needs. The code documents
|
||||
itself.
|
||||
|
||||
**Incorrect (one component, many modes):**
|
||||
|
||||
```tsx
|
||||
// What does this component actually render?
|
||||
<Composer isThread isEditing={false} channelId="abc" showAttachments showFormatting={false} />
|
||||
```
|
||||
|
||||
**Correct (explicit variants):**
|
||||
|
||||
```tsx
|
||||
// Immediately clear what this renders
|
||||
<ThreadComposer channelId="abc" />
|
||||
|
||||
// Or
|
||||
<EditMessageComposer messageId="xyz" />
|
||||
|
||||
// Or
|
||||
<ForwardMessageComposer messageId="123" />
|
||||
```
|
||||
|
||||
Each implementation is unique, explicit and self-contained. Yet they can each
|
||||
use shared parts.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```tsx
|
||||
function ThreadComposer({ channelId }: { channelId: string }) {
|
||||
return (
|
||||
<ThreadProvider channelId={channelId}>
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<AlsoSendToChannelField channelId={channelId} />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</ThreadProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function EditMessageComposer({ messageId }: { messageId: string }) {
|
||||
return (
|
||||
<EditMessageProvider messageId={messageId}>
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.CancelEdit />
|
||||
<Composer.SaveEdit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</EditMessageProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardMessageComposer({ messageId }: { messageId: string }) {
|
||||
return (
|
||||
<ForwardMessageProvider messageId={messageId}>
|
||||
<Composer.Frame>
|
||||
<Composer.Input placeholder="Add a message, if you'd like." />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.Mentions />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</ForwardMessageProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Each variant is explicit about:
|
||||
|
||||
- What provider/state it uses
|
||||
- What UI elements it includes
|
||||
- What actions are available
|
||||
|
||||
No boolean prop combinations to reason about. No impossible states.
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: React 19 API Changes
|
||||
impact: MEDIUM
|
||||
impactDescription: cleaner component definitions and context usage
|
||||
tags: react19, refs, context, hooks
|
||||
---
|
||||
|
||||
## React 19 API Changes
|
||||
|
||||
> **⚠️ React 19+ only.** Skip this if you're on React 18 or earlier.
|
||||
|
||||
In React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `use()` replaces `useContext()`.
|
||||
|
||||
**Incorrect (forwardRef in React 19):**
|
||||
|
||||
```tsx
|
||||
const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
|
||||
return <TextInput ref={ref} {...props} />;
|
||||
});
|
||||
```
|
||||
|
||||
**Correct (ref as a regular prop):**
|
||||
|
||||
```tsx
|
||||
function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
|
||||
return <TextInput ref={ref} {...props} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Incorrect (useContext in React 19):**
|
||||
|
||||
```tsx
|
||||
const value = useContext(MyContext);
|
||||
```
|
||||
|
||||
**Correct (use instead of useContext):**
|
||||
|
||||
```tsx
|
||||
const value = use(MyContext);
|
||||
```
|
||||
|
||||
`use()` can also be called conditionally, unlike `useContext()`.
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
title: Define Generic Context Interfaces for Dependency Injection
|
||||
impact: HIGH
|
||||
impactDescription: enables dependency-injectable state across use-cases
|
||||
tags: composition, context, state, typescript, dependency-injection
|
||||
---
|
||||
|
||||
## Define Generic Context Interfaces for Dependency Injection
|
||||
|
||||
Define a **generic interface** for your component context with three parts:
|
||||
`state`, `actions`, and `meta`. This interface is a contract that any provider
|
||||
can implement—enabling the same UI components to work with completely different
|
||||
state implementations.
|
||||
|
||||
**Core principle:** Lift state, compose internals, make state
|
||||
dependency-injectable.
|
||||
|
||||
**Incorrect (UI coupled to specific state implementation):**
|
||||
|
||||
```tsx
|
||||
function ComposerInput() {
|
||||
// Tightly coupled to a specific hook
|
||||
const { input, setInput } = useChannelComposerState();
|
||||
return <TextInput value={input} onChangeText={setInput} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (generic interface enables dependency injection):**
|
||||
|
||||
```tsx
|
||||
// Define a GENERIC interface that any provider can implement
|
||||
interface ComposerState {
|
||||
input: string;
|
||||
attachments: Attachment[];
|
||||
isSubmitting: boolean;
|
||||
}
|
||||
|
||||
interface ComposerActions {
|
||||
update: (updater: (state: ComposerState) => ComposerState) => void;
|
||||
submit: () => void;
|
||||
}
|
||||
|
||||
interface ComposerMeta {
|
||||
inputRef: React.RefObject<TextInput>;
|
||||
}
|
||||
|
||||
interface ComposerContextValue {
|
||||
state: ComposerState;
|
||||
actions: ComposerActions;
|
||||
meta: ComposerMeta;
|
||||
}
|
||||
|
||||
const ComposerContext = createContext<ComposerContextValue | null>(null);
|
||||
```
|
||||
|
||||
**UI components consume the interface, not the implementation:**
|
||||
|
||||
```tsx
|
||||
function ComposerInput() {
|
||||
const {
|
||||
state,
|
||||
actions: { update },
|
||||
meta,
|
||||
} = use(ComposerContext);
|
||||
|
||||
// This component works with ANY provider that implements the interface
|
||||
return (
|
||||
<TextInput
|
||||
ref={meta.inputRef}
|
||||
value={state.input}
|
||||
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Different providers implement the same interface:**
|
||||
|
||||
```tsx
|
||||
// Provider A: Local state for ephemeral forms
|
||||
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, setState] = useState(initialState);
|
||||
const inputRef = useRef(null);
|
||||
const submit = useForwardMessage();
|
||||
|
||||
return (
|
||||
<ComposerContext
|
||||
value={{
|
||||
state,
|
||||
actions: { update: setState, submit },
|
||||
meta: { inputRef },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ComposerContext>
|
||||
);
|
||||
}
|
||||
|
||||
// Provider B: Global synced state for channels
|
||||
function ChannelProvider({ channelId, children }: Props) {
|
||||
const { state, update, submit } = useGlobalChannel(channelId);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
return (
|
||||
<ComposerContext
|
||||
value={{
|
||||
state,
|
||||
actions: { update, submit },
|
||||
meta: { inputRef },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ComposerContext>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**The same composed UI works with both:**
|
||||
|
||||
```tsx
|
||||
// Works with ForwardMessageProvider (local state)
|
||||
<ForwardMessageProvider>
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<Composer.Submit />
|
||||
</Composer.Frame>
|
||||
</ForwardMessageProvider>
|
||||
|
||||
// Works with ChannelProvider (global synced state)
|
||||
<ChannelProvider channelId="abc">
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<Composer.Submit />
|
||||
</Composer.Frame>
|
||||
</ChannelProvider>
|
||||
```
|
||||
|
||||
**Custom UI outside the component can access state and actions:**
|
||||
|
||||
The provider boundary is what matters—not the visual nesting. Components that
|
||||
need shared state don't have to be inside the `Composer.Frame`. They just need
|
||||
to be within the provider.
|
||||
|
||||
```tsx
|
||||
function ForwardMessageDialog() {
|
||||
return (
|
||||
<ForwardMessageProvider>
|
||||
<Dialog>
|
||||
{/* The composer UI */}
|
||||
<Composer.Frame>
|
||||
<Composer.Input placeholder="Add a message, if you'd like." />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
|
||||
{/* Custom UI OUTSIDE the composer, but INSIDE the provider */}
|
||||
<MessagePreview />
|
||||
|
||||
{/* Actions at the bottom of the dialog */}
|
||||
<DialogActions>
|
||||
<CancelButton />
|
||||
<ForwardButton />
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</ForwardMessageProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// This button lives OUTSIDE Composer.Frame but can still submit based on its context!
|
||||
function ForwardButton() {
|
||||
const {
|
||||
actions: { submit },
|
||||
} = use(ComposerContext);
|
||||
return <Button onPress={submit}>Forward</Button>;
|
||||
}
|
||||
|
||||
// This preview lives OUTSIDE Composer.Frame but can read composer's state!
|
||||
function MessagePreview() {
|
||||
const { state } = use(ComposerContext);
|
||||
return <Preview message={state.input} attachments={state.attachments} />;
|
||||
}
|
||||
```
|
||||
|
||||
The `ForwardButton` and `MessagePreview` are not visually inside the composer
|
||||
box, but they can still access its state and actions. This is the power of
|
||||
lifting state into providers.
|
||||
|
||||
The UI is reusable bits you compose together. The state is dependency-injected
|
||||
by the provider. Swap the provider, keep the UI.
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: Decouple State Management from UI
|
||||
impact: MEDIUM
|
||||
impactDescription: enables swapping state implementations without changing UI
|
||||
tags: composition, state, architecture
|
||||
---
|
||||
|
||||
## Decouple State Management from UI
|
||||
|
||||
The provider component should be the only place that knows how state is managed.
|
||||
UI components consume the context interface—they don't know if state comes from
|
||||
useState, Zustand, or a server sync.
|
||||
|
||||
**Incorrect (UI coupled to state implementation):**
|
||||
|
||||
```tsx
|
||||
function ChannelComposer({ channelId }: { channelId: string }) {
|
||||
// UI component knows about global state implementation
|
||||
const state = useGlobalChannelState(channelId);
|
||||
const { submit, updateInput } = useChannelSync(channelId);
|
||||
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Input value={state.input} onChange={(text) => sync.updateInput(text)} />
|
||||
<Composer.Submit onPress={() => sync.submit()} />
|
||||
</Composer.Frame>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (state management isolated in provider):**
|
||||
|
||||
```tsx
|
||||
// Provider handles all state management details
|
||||
function ChannelProvider({
|
||||
channelId,
|
||||
children,
|
||||
}: {
|
||||
channelId: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { state, update, submit } = useGlobalChannel(channelId);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
return (
|
||||
<Composer.Provider state={state} actions={{ update, submit }} meta={{ inputRef }}>
|
||||
{children}
|
||||
</Composer.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// UI component only knows about the context interface
|
||||
function ChannelComposer() {
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Header />
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
function Channel({ channelId }: { channelId: string }) {
|
||||
return (
|
||||
<ChannelProvider channelId={channelId}>
|
||||
<ChannelComposer />
|
||||
</ChannelProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Different providers, same UI:**
|
||||
|
||||
```tsx
|
||||
// Local state for ephemeral forms
|
||||
function ForwardMessageProvider({ children }) {
|
||||
const [state, setState] = useState(initialState);
|
||||
const forwardMessage = useForwardMessage();
|
||||
|
||||
return (
|
||||
<Composer.Provider state={state} actions={{ update: setState, submit: forwardMessage }}>
|
||||
{children}
|
||||
</Composer.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Global synced state for channels
|
||||
function ChannelProvider({ channelId, children }) {
|
||||
const { state, update, submit } = useGlobalChannel(channelId);
|
||||
|
||||
return (
|
||||
<Composer.Provider state={state} actions={{ update, submit }}>
|
||||
{children}
|
||||
</Composer.Provider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The same `Composer.Input` component works with both providers because it only
|
||||
depends on the context interface, not the implementation.
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
title: Lift State into Provider Components
|
||||
impact: HIGH
|
||||
impactDescription: enables state sharing outside component boundaries
|
||||
tags: composition, state, context, providers
|
||||
---
|
||||
|
||||
## Lift State into Provider Components
|
||||
|
||||
Move state management into dedicated provider components. This allows sibling
|
||||
components outside the main UI to access and modify state without prop drilling
|
||||
or awkward refs.
|
||||
|
||||
**Incorrect (state trapped inside component):**
|
||||
|
||||
```tsx
|
||||
function ForwardMessageComposer() {
|
||||
const [state, setState] = useState(initialState);
|
||||
const forwardMessage = useForwardMessage();
|
||||
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<Composer.Footer />
|
||||
</Composer.Frame>
|
||||
);
|
||||
}
|
||||
|
||||
// Problem: How does this button access composer state?
|
||||
function ForwardMessageDialog() {
|
||||
return (
|
||||
<Dialog>
|
||||
<ForwardMessageComposer />
|
||||
<MessagePreview /> {/* Needs composer state */}
|
||||
<DialogActions>
|
||||
<CancelButton />
|
||||
<ForwardButton /> {/* Needs to call submit */}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Incorrect (useEffect to sync state up):**
|
||||
|
||||
```tsx
|
||||
function ForwardMessageDialog() {
|
||||
const [input, setInput] = useState("");
|
||||
return (
|
||||
<Dialog>
|
||||
<ForwardMessageComposer onInputChange={setInput} />
|
||||
<MessagePreview input={input} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardMessageComposer({ onInputChange }) {
|
||||
const [state, setState] = useState(initialState);
|
||||
useEffect(() => {
|
||||
onInputChange(state.input); // Sync on every change 😬
|
||||
}, [state.input]);
|
||||
}
|
||||
```
|
||||
|
||||
**Incorrect (reading state from ref on submit):**
|
||||
|
||||
```tsx
|
||||
function ForwardMessageDialog() {
|
||||
const stateRef = useRef(null);
|
||||
return (
|
||||
<Dialog>
|
||||
<ForwardMessageComposer stateRef={stateRef} />
|
||||
<ForwardButton onPress={() => submit(stateRef.current)} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (state lifted to provider):**
|
||||
|
||||
```tsx
|
||||
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, setState] = useState(initialState);
|
||||
const forwardMessage = useForwardMessage();
|
||||
const inputRef = useRef(null);
|
||||
|
||||
return (
|
||||
<Composer.Provider
|
||||
state={state}
|
||||
actions={{ update: setState, submit: forwardMessage }}
|
||||
meta={{ inputRef }}
|
||||
>
|
||||
{children}
|
||||
</Composer.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardMessageDialog() {
|
||||
return (
|
||||
<ForwardMessageProvider>
|
||||
<Dialog>
|
||||
<ForwardMessageComposer />
|
||||
<MessagePreview /> {/* Custom components can access state and actions */}
|
||||
<DialogActions>
|
||||
<CancelButton />
|
||||
<ForwardButton /> {/* Custom components can access state and actions */}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</ForwardMessageProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardButton() {
|
||||
const { actions } = use(Composer.Context);
|
||||
return <Button onPress={actions.submit}>Forward</Button>;
|
||||
}
|
||||
```
|
||||
|
||||
The ForwardButton lives outside the Composer.Frame but still has access to the
|
||||
submit action because it's within the provider. Even though it's a one-off
|
||||
component, it can still access the composer's state and actions from outside the
|
||||
UI itself.
|
||||
|
||||
**Key insight:** Components that need shared state don't have to be visually
|
||||
nested inside each other—they just need to be within the same provider.
|
||||
2883
.agents/skills/vercel-react-best-practices/AGENTS.md
Normal file
2883
.agents/skills/vercel-react-best-practices/AGENTS.md
Normal file
File diff suppressed because it is too large
Load Diff
127
.agents/skills/vercel-react-best-practices/README.md
Normal file
127
.agents/skills/vercel-react-best-practices/README.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# React Best Practices
|
||||
|
||||
A structured repository for creating and maintaining React Best Practices optimized for agents and LLMs.
|
||||
|
||||
## Structure
|
||||
|
||||
- `rules/` - Individual rule files (one per rule)
|
||||
- `_sections.md` - Section metadata (titles, impacts, descriptions)
|
||||
- `_template.md` - Template for creating new rules
|
||||
- `area-description.md` - Individual rule files
|
||||
- `src/` - Build scripts and utilities
|
||||
- `metadata.json` - Document metadata (version, organization, abstract)
|
||||
- **`AGENTS.md`** - Compiled output (generated)
|
||||
- **`test-cases.json`** - Test cases for LLM evaluation (generated)
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. Build AGENTS.md from rules:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
3. Validate rule files:
|
||||
|
||||
```bash
|
||||
pnpm validate
|
||||
```
|
||||
|
||||
4. Extract test cases:
|
||||
```bash
|
||||
pnpm extract-tests
|
||||
```
|
||||
|
||||
## Creating a New Rule
|
||||
|
||||
1. Copy `rules/_template.md` to `rules/area-description.md`
|
||||
2. Choose the appropriate area prefix:
|
||||
- `async-` for Eliminating Waterfalls (Section 1)
|
||||
- `bundle-` for Bundle Size Optimization (Section 2)
|
||||
- `server-` for Server-Side Performance (Section 3)
|
||||
- `client-` for Client-Side Data Fetching (Section 4)
|
||||
- `rerender-` for Re-render Optimization (Section 5)
|
||||
- `rendering-` for Rendering Performance (Section 6)
|
||||
- `js-` for JavaScript Performance (Section 7)
|
||||
- `advanced-` for Advanced Patterns (Section 8)
|
||||
3. Fill in the frontmatter and content
|
||||
4. Ensure you have clear examples with explanations
|
||||
5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
|
||||
|
||||
## Rule File Structure
|
||||
|
||||
Each rule file should follow this structure:
|
||||
|
||||
````markdown
|
||||
---
|
||||
title: Rule Title Here
|
||||
impact: MEDIUM
|
||||
impactDescription: Optional description
|
||||
tags: tag1, tag2, tag3
|
||||
---
|
||||
|
||||
## Rule Title Here
|
||||
|
||||
Brief explanation of the rule and why it matters.
|
||||
|
||||
**Incorrect (description of what's wrong):**
|
||||
|
||||
```typescript
|
||||
// Bad code example
|
||||
```
|
||||
````
|
||||
|
||||
**Correct (description of what's right):**
|
||||
|
||||
```typescript
|
||||
// Good code example
|
||||
```
|
||||
|
||||
Optional explanatory text after examples.
|
||||
|
||||
Reference: [Link](https://example.com)
|
||||
|
||||
## File Naming Convention
|
||||
|
||||
- Files starting with `_` are special (excluded from build)
|
||||
- Rule files: `area-description.md` (e.g., `async-parallel.md`)
|
||||
- Section is automatically inferred from filename prefix
|
||||
- Rules are sorted alphabetically by title within each section
|
||||
- IDs (e.g., 1.1, 1.2) are auto-generated during build
|
||||
|
||||
## Impact Levels
|
||||
|
||||
- `CRITICAL` - Highest priority, major performance gains
|
||||
- `HIGH` - Significant performance improvements
|
||||
- `MEDIUM-HIGH` - Moderate-high gains
|
||||
- `MEDIUM` - Moderate performance improvements
|
||||
- `LOW-MEDIUM` - Low-medium gains
|
||||
- `LOW` - Incremental improvements
|
||||
|
||||
## Scripts
|
||||
|
||||
- `pnpm build` - Compile rules into AGENTS.md
|
||||
- `pnpm validate` - Validate all rule files
|
||||
- `pnpm extract-tests` - Extract test cases for LLM evaluation
|
||||
- `pnpm dev` - Build and validate
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding or modifying rules:
|
||||
|
||||
1. Use the correct filename prefix for your section
|
||||
2. Follow the `_template.md` structure
|
||||
3. Include clear bad/good examples with explanations
|
||||
4. Add appropriate tags
|
||||
5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
|
||||
6. Rules are automatically sorted by title - no need to manage numbers!
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Originally created by [@shuding](https://x.com/shuding) at [Vercel](https://vercel.com).
|
||||
138
.agents/skills/vercel-react-best-practices/SKILL.md
Normal file
138
.agents/skills/vercel-react-best-practices/SKILL.md
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: vercel-react-best-practices
|
||||
description: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: vercel
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Vercel React Best Practices
|
||||
|
||||
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 57 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Reference these guidelines when:
|
||||
|
||||
- Writing new React components or Next.js pages
|
||||
- Implementing data fetching (client or server-side)
|
||||
- Reviewing code for performance issues
|
||||
- Refactoring existing React/Next.js code
|
||||
- Optimizing bundle size or load times
|
||||
|
||||
## Rule Categories by Priority
|
||||
|
||||
| Priority | Category | Impact | Prefix |
|
||||
| -------- | ------------------------- | ----------- | ------------ |
|
||||
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
|
||||
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
|
||||
| 3 | Server-Side Performance | HIGH | `server-` |
|
||||
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
|
||||
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
|
||||
| 6 | Rendering Performance | MEDIUM | `rendering-` |
|
||||
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
|
||||
| 8 | Advanced Patterns | LOW | `advanced-` |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### 1. Eliminating Waterfalls (CRITICAL)
|
||||
|
||||
- `async-defer-await` - Move await into branches where actually used
|
||||
- `async-parallel` - Use Promise.all() for independent operations
|
||||
- `async-dependencies` - Use better-all for partial dependencies
|
||||
- `async-api-routes` - Start promises early, await late in API routes
|
||||
- `async-suspense-boundaries` - Use Suspense to stream content
|
||||
|
||||
### 2. Bundle Size Optimization (CRITICAL)
|
||||
|
||||
- `bundle-barrel-imports` - Import directly, avoid barrel files
|
||||
- `bundle-dynamic-imports` - Use next/dynamic for heavy components
|
||||
- `bundle-defer-third-party` - Load analytics/logging after hydration
|
||||
- `bundle-conditional` - Load modules only when feature is activated
|
||||
- `bundle-preload` - Preload on hover/focus for perceived speed
|
||||
|
||||
### 3. Server-Side Performance (HIGH)
|
||||
|
||||
- `server-auth-actions` - Authenticate server actions like API routes
|
||||
- `server-cache-react` - Use React.cache() for per-request deduplication
|
||||
- `server-cache-lru` - Use LRU cache for cross-request caching
|
||||
- `server-dedup-props` - Avoid duplicate serialization in RSC props
|
||||
- `server-serialization` - Minimize data passed to client components
|
||||
- `server-parallel-fetching` - Restructure components to parallelize fetches
|
||||
- `server-after-nonblocking` - Use after() for non-blocking operations
|
||||
|
||||
### 4. Client-Side Data Fetching (MEDIUM-HIGH)
|
||||
|
||||
- `client-swr-dedup` - Use SWR for automatic request deduplication
|
||||
- `client-event-listeners` - Deduplicate global event listeners
|
||||
- `client-passive-event-listeners` - Use passive listeners for scroll
|
||||
- `client-localstorage-schema` - Version and minimize localStorage data
|
||||
|
||||
### 5. Re-render Optimization (MEDIUM)
|
||||
|
||||
- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
|
||||
- `rerender-memo` - Extract expensive work into memoized components
|
||||
- `rerender-memo-with-default-value` - Hoist default non-primitive props
|
||||
- `rerender-dependencies` - Use primitive dependencies in effects
|
||||
- `rerender-derived-state` - Subscribe to derived booleans, not raw values
|
||||
- `rerender-derived-state-no-effect` - Derive state during render, not effects
|
||||
- `rerender-functional-setstate` - Use functional setState for stable callbacks
|
||||
- `rerender-lazy-state-init` - Pass function to useState for expensive values
|
||||
- `rerender-simple-expression-in-memo` - Avoid memo for simple primitives
|
||||
- `rerender-move-effect-to-event` - Put interaction logic in event handlers
|
||||
- `rerender-transitions` - Use startTransition for non-urgent updates
|
||||
- `rerender-use-ref-transient-values` - Use refs for transient frequent values
|
||||
|
||||
### 6. Rendering Performance (MEDIUM)
|
||||
|
||||
- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element
|
||||
- `rendering-content-visibility` - Use content-visibility for long lists
|
||||
- `rendering-hoist-jsx` - Extract static JSX outside components
|
||||
- `rendering-svg-precision` - Reduce SVG coordinate precision
|
||||
- `rendering-hydration-no-flicker` - Use inline script for client-only data
|
||||
- `rendering-hydration-suppress-warning` - Suppress expected mismatches
|
||||
- `rendering-activity` - Use Activity component for show/hide
|
||||
- `rendering-conditional-render` - Use ternary, not && for conditionals
|
||||
- `rendering-usetransition-loading` - Prefer useTransition for loading state
|
||||
|
||||
### 7. JavaScript Performance (LOW-MEDIUM)
|
||||
|
||||
- `js-batch-dom-css` - Group CSS changes via classes or cssText
|
||||
- `js-index-maps` - Build Map for repeated lookups
|
||||
- `js-cache-property-access` - Cache object properties in loops
|
||||
- `js-cache-function-results` - Cache function results in module-level Map
|
||||
- `js-cache-storage` - Cache localStorage/sessionStorage reads
|
||||
- `js-combine-iterations` - Combine multiple filter/map into one loop
|
||||
- `js-length-check-first` - Check array length before expensive comparison
|
||||
- `js-early-exit` - Return early from functions
|
||||
- `js-hoist-regexp` - Hoist RegExp creation outside loops
|
||||
- `js-min-max-loop` - Use loop for min/max instead of sort
|
||||
- `js-set-map-lookups` - Use Set/Map for O(1) lookups
|
||||
- `js-tosorted-immutable` - Use toSorted() for immutability
|
||||
|
||||
### 8. Advanced Patterns (LOW)
|
||||
|
||||
- `advanced-event-handler-refs` - Store event handlers in refs
|
||||
- `advanced-init-once` - Initialize app once per app load
|
||||
- `advanced-use-latest` - useLatest for stable callback refs
|
||||
|
||||
## How to Use
|
||||
|
||||
Read individual rule files for detailed explanations and code examples:
|
||||
|
||||
```
|
||||
rules/async-parallel.md
|
||||
rules/bundle-barrel-imports.md
|
||||
```
|
||||
|
||||
Each rule file contains:
|
||||
|
||||
- Brief explanation of why it matters
|
||||
- Incorrect code example with explanation
|
||||
- Correct code example with explanation
|
||||
- Additional context and references
|
||||
|
||||
## Full Compiled Document
|
||||
|
||||
For the complete guide with all rules expanded: `AGENTS.md`
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: Store Event Handlers in Refs
|
||||
impact: LOW
|
||||
impactDescription: stable subscriptions
|
||||
tags: advanced, hooks, refs, event-handlers, optimization
|
||||
---
|
||||
|
||||
## Store Event Handlers in Refs
|
||||
|
||||
Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
|
||||
|
||||
**Incorrect (re-subscribes on every render):**
|
||||
|
||||
```tsx
|
||||
function useWindowEvent(event: string, handler: (e) => void) {
|
||||
useEffect(() => {
|
||||
window.addEventListener(event, handler);
|
||||
return () => window.removeEventListener(event, handler);
|
||||
}, [event, handler]);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (stable subscription):**
|
||||
|
||||
```tsx
|
||||
function useWindowEvent(event: string, handler: (e) => void) {
|
||||
const handlerRef = useRef(handler);
|
||||
useEffect(() => {
|
||||
handlerRef.current = handler;
|
||||
}, [handler]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (e) => handlerRef.current(e);
|
||||
window.addEventListener(event, listener);
|
||||
return () => window.removeEventListener(event, listener);
|
||||
}, [event]);
|
||||
}
|
||||
```
|
||||
|
||||
**Alternative: use `useEffectEvent` if you're on latest React:**
|
||||
|
||||
```tsx
|
||||
import { useEffectEvent } from "react";
|
||||
|
||||
function useWindowEvent(event: string, handler: (e) => void) {
|
||||
const onEvent = useEffectEvent(handler);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener(event, onEvent);
|
||||
return () => window.removeEventListener(event, onEvent);
|
||||
}, [event]);
|
||||
}
|
||||
```
|
||||
|
||||
`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: Initialize App Once, Not Per Mount
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: avoids duplicate init in development
|
||||
tags: initialization, useEffect, app-startup, side-effects
|
||||
---
|
||||
|
||||
## Initialize App Once, Not Per Mount
|
||||
|
||||
Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
|
||||
|
||||
**Incorrect (runs twice in dev, re-runs on remount):**
|
||||
|
||||
```tsx
|
||||
function Comp() {
|
||||
useEffect(() => {
|
||||
loadFromStorage();
|
||||
checkAuthToken();
|
||||
}, []);
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (once per app load):**
|
||||
|
||||
```tsx
|
||||
let didInit = false;
|
||||
|
||||
function Comp() {
|
||||
useEffect(() => {
|
||||
if (didInit) return;
|
||||
didInit = true;
|
||||
loadFromStorage();
|
||||
checkAuthToken();
|
||||
}, []);
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Reference: [Initializing the application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: useEffectEvent for Stable Callback Refs
|
||||
impact: LOW
|
||||
impactDescription: prevents effect re-runs
|
||||
tags: advanced, hooks, useEffectEvent, refs, optimization
|
||||
---
|
||||
|
||||
## useEffectEvent for Stable Callback Refs
|
||||
|
||||
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
|
||||
|
||||
**Incorrect (effect re-runs on every callback change):**
|
||||
|
||||
```tsx
|
||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => onSearch(query), 300);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [query, onSearch]);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (using React's useEffectEvent):**
|
||||
|
||||
```tsx
|
||||
import { useEffectEvent } from "react";
|
||||
|
||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||
const [query, setQuery] = useState("");
|
||||
const onSearchEvent = useEffectEvent(onSearch);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => onSearchEvent(query), 300);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [query]);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Prevent Waterfall Chains in API Routes
|
||||
impact: CRITICAL
|
||||
impactDescription: 2-10× improvement
|
||||
tags: api-routes, server-actions, waterfalls, parallelization
|
||||
---
|
||||
|
||||
## Prevent Waterfall Chains in API Routes
|
||||
|
||||
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).
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Defer Await Until Needed
|
||||
impact: HIGH
|
||||
impactDescription: avoids blocking unused code paths
|
||||
tags: async, await, conditional, optimization
|
||||
---
|
||||
|
||||
## Defer Await Until Needed
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: Dependency-Based Parallelization
|
||||
impact: CRITICAL
|
||||
impactDescription: 2-10× improvement
|
||||
tags: async, parallelization, dependencies, better-all
|
||||
---
|
||||
|
||||
## Dependency-Based Parallelization
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Alternative without extra dependencies:**
|
||||
|
||||
We can also create all the promises first, and do `Promise.all()` at the end.
|
||||
|
||||
```typescript
|
||||
const userPromise = fetchUser();
|
||||
const profilePromise = userPromise.then((user) => fetchProfile(user.id));
|
||||
|
||||
const [user, config, profile] = await Promise.all([userPromise, fetchConfig(), profilePromise]);
|
||||
```
|
||||
|
||||
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Promise.all() for Independent Operations
|
||||
impact: CRITICAL
|
||||
impactDescription: 2-10× improvement
|
||||
tags: async, parallelization, promises, waterfalls
|
||||
---
|
||||
|
||||
## Promise.all() for Independent Operations
|
||||
|
||||
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()]);
|
||||
```
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
title: Strategic Suspense Boundaries
|
||||
impact: HIGH
|
||||
impactDescription: faster initial paint
|
||||
tags: async, suspense, streaming, layout-shift
|
||||
---
|
||||
|
||||
## Strategic Suspense Boundaries
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<div>Sidebar</div>
|
||||
<div>Header</div>
|
||||
<div>
|
||||
<DataDisplay data={data} />
|
||||
</div>
|
||||
<div>Footer</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<div>Sidebar</div>
|
||||
<div>Header</div>
|
||||
<div>
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<DataDisplay />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div>Footer</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function DataDisplay() {
|
||||
const data = await fetchData(); // Only blocks this component
|
||||
return <div>{data.content}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<div>Sidebar</div>
|
||||
<div>Header</div>
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<DataDisplay dataPromise={dataPromise} />
|
||||
<DataSummary dataPromise={dataPromise} />
|
||||
</Suspense>
|
||||
<div>Footer</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
|
||||
const data = use(dataPromise); // Unwraps the promise
|
||||
return <div>{data.content}</div>;
|
||||
}
|
||||
|
||||
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
|
||||
const data = use(dataPromise); // Reuses the same promise
|
||||
return <div>{data.summary}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: Avoid Barrel File Imports
|
||||
impact: CRITICAL
|
||||
impactDescription: 200-800ms import cost, slow builds
|
||||
tags: bundle, imports, tree-shaking, barrel-files, performance
|
||||
---
|
||||
|
||||
## Avoid Barrel File Imports
|
||||
|
||||
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: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Conditional Module Loading
|
||||
impact: HIGH
|
||||
impactDescription: loads large data only when needed
|
||||
tags: bundle, conditional-loading, lazy-loading
|
||||
---
|
||||
|
||||
## Conditional Module Loading
|
||||
|
||||
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<React.SetStateAction<boolean>>;
|
||||
}) {
|
||||
const [frames, setFrames] = useState<Frame[] | null>(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 <Skeleton />;
|
||||
return <Canvas frames={frames} />;
|
||||
}
|
||||
```
|
||||
|
||||
The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: Defer Non-Critical Third-Party Libraries
|
||||
impact: MEDIUM
|
||||
impactDescription: loads after hydration
|
||||
tags: bundle, third-party, analytics, defer
|
||||
---
|
||||
|
||||
## Defer Non-Critical Third-Party Libraries
|
||||
|
||||
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 (
|
||||
<html>
|
||||
<body>
|
||||
{children}
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**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 (
|
||||
<html>
|
||||
<body>
|
||||
{children}
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Dynamic Imports for Heavy Components
|
||||
impact: CRITICAL
|
||||
impactDescription: directly affects TTI and LCP
|
||||
tags: bundle, dynamic-import, code-splitting, next-dynamic
|
||||
---
|
||||
|
||||
## Dynamic Imports for Heavy Components
|
||||
|
||||
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 <MonacoEditor value={code} />;
|
||||
}
|
||||
```
|
||||
|
||||
**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 <MonacoEditor value={code} />;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Preload Based on User Intent
|
||||
impact: MEDIUM
|
||||
impactDescription: reduces perceived latency
|
||||
tags: bundle, preload, user-intent, hover
|
||||
---
|
||||
|
||||
## Preload Based on User Intent
|
||||
|
||||
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 (
|
||||
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
|
||||
Open Editor
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**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 <FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>;
|
||||
}
|
||||
```
|
||||
|
||||
The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: Deduplicate Global Event Listeners
|
||||
impact: LOW
|
||||
impactDescription: single listener for N components
|
||||
tags: client, swr, event-listeners, subscription
|
||||
---
|
||||
|
||||
## Deduplicate Global Event Listeners
|
||||
|
||||
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<string, Set<() => 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", () => {
|
||||
/* ... */
|
||||
});
|
||||
// ...
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Version and Minimize localStorage Data
|
||||
impact: MEDIUM
|
||||
impactDescription: prevents schema conflicts, reduces storage size
|
||||
tags: client, localStorage, storage, versioning, data-minimization
|
||||
---
|
||||
|
||||
## Version and Minimize localStorage Data
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: Use Passive Event Listeners for Scrolling Performance
|
||||
impact: MEDIUM
|
||||
impactDescription: eliminates scroll delay caused by event listeners
|
||||
tags: client, event-listeners, scrolling, performance, touch, wheel
|
||||
---
|
||||
|
||||
## Use Passive Event Listeners for Scrolling Performance
|
||||
|
||||
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()`.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: Use SWR for Automatic Deduplication
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: automatic deduplication
|
||||
tags: client, swr, deduplication, data-fetching
|
||||
---
|
||||
|
||||
## Use SWR for 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 <button onClick={() => trigger()}>Update</button>;
|
||||
}
|
||||
```
|
||||
|
||||
Reference: [https://swr.vercel.app](https://swr.vercel.app)
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
title: Avoid Layout Thrashing
|
||||
impact: MEDIUM
|
||||
impactDescription: prevents forced synchronous layouts and reduces performance bottlenecks
|
||||
tags: javascript, dom, css, performance, reflow, layout-thrashing
|
||||
---
|
||||
|
||||
## Avoid Layout Thrashing
|
||||
|
||||
Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
|
||||
|
||||
**This is OK (browser batches style changes):**
|
||||
|
||||
```typescript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
// Each line invalidates style, but browser batches the recalculation
|
||||
element.style.width = "100px";
|
||||
element.style.height = "200px";
|
||||
element.style.backgroundColor = "blue";
|
||||
element.style.border = "1px solid black";
|
||||
}
|
||||
```
|
||||
|
||||
**Incorrect (interleaved reads and writes force reflows):**
|
||||
|
||||
```typescript
|
||||
function layoutThrashing(element: HTMLElement) {
|
||||
element.style.width = "100px";
|
||||
const width = element.offsetWidth; // Forces reflow
|
||||
element.style.height = "200px";
|
||||
const height = element.offsetHeight; // Forces another reflow
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (batch writes, then read once):**
|
||||
|
||||
```typescript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
// Batch all writes together
|
||||
element.style.width = "100px";
|
||||
element.style.height = "200px";
|
||||
element.style.backgroundColor = "blue";
|
||||
element.style.border = "1px solid black";
|
||||
|
||||
// Read after all writes are done (single reflow)
|
||||
const { width, height } = element.getBoundingClientRect();
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (batch reads, then writes):**
|
||||
|
||||
```typescript
|
||||
function avoidThrashing(element: HTMLElement) {
|
||||
// Read phase - all layout queries first
|
||||
const rect1 = element.getBoundingClientRect();
|
||||
const offsetWidth = element.offsetWidth;
|
||||
const offsetHeight = element.offsetHeight;
|
||||
|
||||
// Write phase - all style changes after
|
||||
element.style.width = "100px";
|
||||
element.style.height = "200px";
|
||||
}
|
||||
```
|
||||
|
||||
**Better: use CSS classes**
|
||||
|
||||
```css
|
||||
.highlighted-box {
|
||||
width: 100px;
|
||||
height: 200px;
|
||||
background-color: blue;
|
||||
border: 1px solid black;
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
element.classList.add("highlighted-box");
|
||||
|
||||
const { width, height } = element.getBoundingClientRect();
|
||||
}
|
||||
```
|
||||
|
||||
**React example:**
|
||||
|
||||
```tsx
|
||||
// Incorrect: interleaving style changes with layout queries
|
||||
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && isHighlighted) {
|
||||
ref.current.style.width = "100px";
|
||||
const width = ref.current.offsetWidth; // Forces layout
|
||||
ref.current.style.height = "200px";
|
||||
}
|
||||
}, [isHighlighted]);
|
||||
|
||||
return <div ref={ref}>Content</div>;
|
||||
}
|
||||
|
||||
// Correct: toggle class
|
||||
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
||||
return <div className={isHighlighted ? "highlighted-box" : ""}>Content</div>;
|
||||
}
|
||||
```
|
||||
|
||||
Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
|
||||
|
||||
See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Cache Repeated Function Calls
|
||||
impact: MEDIUM
|
||||
impactDescription: avoid redundant computation
|
||||
tags: javascript, cache, memoization, performance
|
||||
---
|
||||
|
||||
## Cache Repeated Function Calls
|
||||
|
||||
Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
|
||||
|
||||
**Incorrect (redundant computation):**
|
||||
|
||||
```typescript
|
||||
function ProjectList({ projects }: { projects: Project[] }) {
|
||||
return (
|
||||
<div>
|
||||
{projects.map(project => {
|
||||
// slugify() called 100+ times for same project names
|
||||
const slug = slugify(project.name)
|
||||
|
||||
return <ProjectCard key={project.id} slug={slug} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (cached results):**
|
||||
|
||||
```typescript
|
||||
// Module-level cache
|
||||
const slugifyCache = new Map<string, string>()
|
||||
|
||||
function cachedSlugify(text: string): string {
|
||||
if (slugifyCache.has(text)) {
|
||||
return slugifyCache.get(text)!
|
||||
}
|
||||
const result = slugify(text)
|
||||
slugifyCache.set(text, result)
|
||||
return result
|
||||
}
|
||||
|
||||
function ProjectList({ projects }: { projects: Project[] }) {
|
||||
return (
|
||||
<div>
|
||||
{projects.map(project => {
|
||||
// Computed only once per unique project name
|
||||
const slug = cachedSlugify(project.name)
|
||||
|
||||
return <ProjectCard key={project.id} slug={slug} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Simpler pattern for single-value functions:**
|
||||
|
||||
```typescript
|
||||
let isLoggedInCache: boolean | null = null;
|
||||
|
||||
function isLoggedIn(): boolean {
|
||||
if (isLoggedInCache !== null) {
|
||||
return isLoggedInCache;
|
||||
}
|
||||
|
||||
isLoggedInCache = document.cookie.includes("auth=");
|
||||
return isLoggedInCache;
|
||||
}
|
||||
|
||||
// Clear cache when auth changes
|
||||
function onAuthChange() {
|
||||
isLoggedInCache = null;
|
||||
}
|
||||
```
|
||||
|
||||
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
|
||||
|
||||
Reference: [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Cache Property Access in Loops
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: reduces lookups
|
||||
tags: javascript, loops, optimization, caching
|
||||
---
|
||||
|
||||
## Cache Property Access in Loops
|
||||
|
||||
Cache object property lookups in hot paths.
|
||||
|
||||
**Incorrect (3 lookups × N iterations):**
|
||||
|
||||
```typescript
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
process(obj.config.settings.value);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (1 lookup total):**
|
||||
|
||||
```typescript
|
||||
const value = obj.config.settings.value;
|
||||
const len = arr.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
process(value);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Cache Storage API Calls
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: reduces expensive I/O
|
||||
tags: javascript, localStorage, storage, caching, performance
|
||||
---
|
||||
|
||||
## Cache Storage API Calls
|
||||
|
||||
`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
|
||||
|
||||
**Incorrect (reads storage on every call):**
|
||||
|
||||
```typescript
|
||||
function getTheme() {
|
||||
return localStorage.getItem("theme") ?? "light";
|
||||
}
|
||||
// Called 10 times = 10 storage reads
|
||||
```
|
||||
|
||||
**Correct (Map cache):**
|
||||
|
||||
```typescript
|
||||
const storageCache = new Map<string, string | null>();
|
||||
|
||||
function getLocalStorage(key: string) {
|
||||
if (!storageCache.has(key)) {
|
||||
storageCache.set(key, localStorage.getItem(key));
|
||||
}
|
||||
return storageCache.get(key);
|
||||
}
|
||||
|
||||
function setLocalStorage(key: string, value: string) {
|
||||
localStorage.setItem(key, value);
|
||||
storageCache.set(key, value); // keep cache in sync
|
||||
}
|
||||
```
|
||||
|
||||
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
|
||||
|
||||
**Cookie caching:**
|
||||
|
||||
```typescript
|
||||
let cookieCache: Record<string, string> | null = null;
|
||||
|
||||
function getCookie(name: string) {
|
||||
if (!cookieCache) {
|
||||
cookieCache = Object.fromEntries(document.cookie.split("; ").map((c) => c.split("=")));
|
||||
}
|
||||
return cookieCache[name];
|
||||
}
|
||||
```
|
||||
|
||||
**Important (invalidate on external changes):**
|
||||
|
||||
If storage can change externally (another tab, server-set cookies), invalidate cache:
|
||||
|
||||
```typescript
|
||||
window.addEventListener("storage", (e) => {
|
||||
if (e.key) storageCache.delete(e.key);
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
storageCache.clear();
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Combine Multiple Array Iterations
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: reduces iterations
|
||||
tags: javascript, arrays, loops, performance
|
||||
---
|
||||
|
||||
## Combine Multiple Array Iterations
|
||||
|
||||
Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
|
||||
|
||||
**Incorrect (3 iterations):**
|
||||
|
||||
```typescript
|
||||
const admins = users.filter((u) => u.isAdmin);
|
||||
const testers = users.filter((u) => u.isTester);
|
||||
const inactive = users.filter((u) => !u.isActive);
|
||||
```
|
||||
|
||||
**Correct (1 iteration):**
|
||||
|
||||
```typescript
|
||||
const admins: User[] = [];
|
||||
const testers: User[] = [];
|
||||
const inactive: User[] = [];
|
||||
|
||||
for (const user of users) {
|
||||
if (user.isAdmin) admins.push(user);
|
||||
if (user.isTester) testers.push(user);
|
||||
if (!user.isActive) inactive.push(user);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Early Return from Functions
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: avoids unnecessary computation
|
||||
tags: javascript, functions, optimization, early-return
|
||||
---
|
||||
|
||||
## Early Return from Functions
|
||||
|
||||
Return early when result is determined to skip unnecessary processing.
|
||||
|
||||
**Incorrect (processes all items even after finding answer):**
|
||||
|
||||
```typescript
|
||||
function validateUsers(users: User[]) {
|
||||
let hasError = false;
|
||||
let errorMessage = "";
|
||||
|
||||
for (const user of users) {
|
||||
if (!user.email) {
|
||||
hasError = true;
|
||||
errorMessage = "Email required";
|
||||
}
|
||||
if (!user.name) {
|
||||
hasError = true;
|
||||
errorMessage = "Name required";
|
||||
}
|
||||
// Continues checking all users even after error found
|
||||
}
|
||||
|
||||
return hasError ? { valid: false, error: errorMessage } : { valid: true };
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (returns immediately on first error):**
|
||||
|
||||
```typescript
|
||||
function validateUsers(users: User[]) {
|
||||
for (const user of users) {
|
||||
if (!user.email) {
|
||||
return { valid: false, error: "Email required" };
|
||||
}
|
||||
if (!user.name) {
|
||||
return { valid: false, error: "Name required" };
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Hoist RegExp Creation
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: avoids recreation
|
||||
tags: javascript, regexp, optimization, memoization
|
||||
---
|
||||
|
||||
## Hoist RegExp Creation
|
||||
|
||||
Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
|
||||
|
||||
**Incorrect (new RegExp every render):**
|
||||
|
||||
```tsx
|
||||
function Highlighter({ text, query }: Props) {
|
||||
const regex = new RegExp(`(${query})`, 'gi')
|
||||
const parts = text.split(regex)
|
||||
return <>{parts.map((part, i) => ...)}</>
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (memoize or hoist):**
|
||||
|
||||
```tsx
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
|
||||
function Highlighter({ text, query }: Props) {
|
||||
const regex = useMemo(
|
||||
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
|
||||
[query]
|
||||
)
|
||||
const parts = text.split(regex)
|
||||
return <>{parts.map((part, i) => ...)}</>
|
||||
}
|
||||
```
|
||||
|
||||
**Warning (global regex has mutable state):**
|
||||
|
||||
Global regex (`/g`) has mutable `lastIndex` state:
|
||||
|
||||
```typescript
|
||||
const regex = /foo/g;
|
||||
regex.test("foo"); // true, lastIndex = 3
|
||||
regex.test("foo"); // false, lastIndex = 0
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Build Index Maps for Repeated Lookups
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: 1M ops to 2K ops
|
||||
tags: javascript, map, indexing, optimization, performance
|
||||
---
|
||||
|
||||
## Build Index Maps for Repeated Lookups
|
||||
|
||||
Multiple `.find()` calls by the same key should use a Map.
|
||||
|
||||
**Incorrect (O(n) per lookup):**
|
||||
|
||||
```typescript
|
||||
function processOrders(orders: Order[], users: User[]) {
|
||||
return orders.map((order) => ({
|
||||
...order,
|
||||
user: users.find((u) => u.id === order.userId),
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (O(1) per lookup):**
|
||||
|
||||
```typescript
|
||||
function processOrders(orders: Order[], users: User[]) {
|
||||
const userById = new Map(users.map((u) => [u.id, u]));
|
||||
|
||||
return orders.map((order) => ({
|
||||
...order,
|
||||
user: userById.get(order.userId),
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
Build map once (O(n)), then all lookups are O(1).
|
||||
For 1000 orders × 1000 users: 1M ops → 2K ops.
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Early Length Check for Array Comparisons
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: avoids expensive operations when lengths differ
|
||||
tags: javascript, arrays, performance, optimization, comparison
|
||||
---
|
||||
|
||||
## Early Length Check for Array Comparisons
|
||||
|
||||
When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
|
||||
|
||||
In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
|
||||
|
||||
**Incorrect (always runs expensive comparison):**
|
||||
|
||||
```typescript
|
||||
function hasChanges(current: string[], original: string[]) {
|
||||
// Always sorts and joins, even when lengths differ
|
||||
return current.sort().join() !== original.sort().join();
|
||||
}
|
||||
```
|
||||
|
||||
Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.
|
||||
|
||||
**Correct (O(1) length check first):**
|
||||
|
||||
```typescript
|
||||
function hasChanges(current: string[], original: string[]) {
|
||||
// Early return if lengths differ
|
||||
if (current.length !== original.length) {
|
||||
return true;
|
||||
}
|
||||
// Only sort when lengths match
|
||||
const currentSorted = current.toSorted();
|
||||
const originalSorted = original.toSorted();
|
||||
for (let i = 0; i < currentSorted.length; i++) {
|
||||
if (currentSorted[i] !== originalSorted[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
This new approach is more efficient because:
|
||||
|
||||
- It avoids the overhead of sorting and joining the arrays when lengths differ
|
||||
- It avoids consuming memory for the joined strings (especially important for large arrays)
|
||||
- It avoids mutating the original arrays
|
||||
- It returns early when a difference is found
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Use Loop for Min/Max Instead of Sort
|
||||
impact: LOW
|
||||
impactDescription: O(n) instead of O(n log n)
|
||||
tags: javascript, arrays, performance, sorting, algorithms
|
||||
---
|
||||
|
||||
## Use Loop for Min/Max Instead of Sort
|
||||
|
||||
Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
|
||||
|
||||
**Incorrect (O(n log n) - sort to find latest):**
|
||||
|
||||
```typescript
|
||||
interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
function getLatestProject(projects: Project[]) {
|
||||
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
return sorted[0];
|
||||
}
|
||||
```
|
||||
|
||||
Sorts the entire array just to find the maximum value.
|
||||
|
||||
**Incorrect (O(n log n) - sort for oldest and newest):**
|
||||
|
||||
```typescript
|
||||
function getOldestAndNewest(projects: Project[]) {
|
||||
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt);
|
||||
return { oldest: sorted[0], newest: sorted[sorted.length - 1] };
|
||||
}
|
||||
```
|
||||
|
||||
Still sorts unnecessarily when only min/max are needed.
|
||||
|
||||
**Correct (O(n) - single loop):**
|
||||
|
||||
```typescript
|
||||
function getLatestProject(projects: Project[]) {
|
||||
if (projects.length === 0) return null;
|
||||
|
||||
let latest = projects[0];
|
||||
|
||||
for (let i = 1; i < projects.length; i++) {
|
||||
if (projects[i].updatedAt > latest.updatedAt) {
|
||||
latest = projects[i];
|
||||
}
|
||||
}
|
||||
|
||||
return latest;
|
||||
}
|
||||
|
||||
function getOldestAndNewest(projects: Project[]) {
|
||||
if (projects.length === 0) return { oldest: null, newest: null };
|
||||
|
||||
let oldest = projects[0];
|
||||
let newest = projects[0];
|
||||
|
||||
for (let i = 1; i < projects.length; i++) {
|
||||
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i];
|
||||
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i];
|
||||
}
|
||||
|
||||
return { oldest, newest };
|
||||
}
|
||||
```
|
||||
|
||||
Single pass through the array, no copying, no sorting.
|
||||
|
||||
**Alternative (Math.min/Math.max for small arrays):**
|
||||
|
||||
```typescript
|
||||
const numbers = [5, 2, 8, 1, 9];
|
||||
const min = Math.min(...numbers);
|
||||
const max = Math.max(...numbers);
|
||||
```
|
||||
|
||||
This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Use Set/Map for O(1) Lookups
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: O(n) to O(1)
|
||||
tags: javascript, set, map, data-structures, performance
|
||||
---
|
||||
|
||||
## Use Set/Map for O(1) Lookups
|
||||
|
||||
Convert arrays to Set/Map for repeated membership checks.
|
||||
|
||||
**Incorrect (O(n) per check):**
|
||||
|
||||
```typescript
|
||||
const allowedIds = ['a', 'b', 'c', ...]
|
||||
items.filter(item => allowedIds.includes(item.id))
|
||||
```
|
||||
|
||||
**Correct (O(1) per check):**
|
||||
|
||||
```typescript
|
||||
const allowedIds = new Set(['a', 'b', 'c', ...])
|
||||
items.filter(item => allowedIds.has(item.id))
|
||||
```
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: Use toSorted() Instead of sort() for Immutability
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: prevents mutation bugs in React state
|
||||
tags: javascript, arrays, immutability, react, state, mutation
|
||||
---
|
||||
|
||||
## Use toSorted() Instead of sort() for Immutability
|
||||
|
||||
`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.
|
||||
|
||||
**Incorrect (mutates original array):**
|
||||
|
||||
```typescript
|
||||
function UserList({ users }: { users: User[] }) {
|
||||
// Mutates the users prop array!
|
||||
const sorted = useMemo(
|
||||
() => users.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
[users]
|
||||
)
|
||||
return <div>{sorted.map(renderUser)}</div>
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (creates new array):**
|
||||
|
||||
```typescript
|
||||
function UserList({ users }: { users: User[] }) {
|
||||
// Creates new sorted array, original unchanged
|
||||
const sorted = useMemo(
|
||||
() => users.toSorted((a, b) => a.name.localeCompare(b.name)),
|
||||
[users]
|
||||
)
|
||||
return <div>{sorted.map(renderUser)}</div>
|
||||
}
|
||||
```
|
||||
|
||||
**Why this matters in React:**
|
||||
|
||||
1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
|
||||
2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
|
||||
|
||||
**Browser support (fallback for older browsers):**
|
||||
|
||||
`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
|
||||
|
||||
```typescript
|
||||
// Fallback for older browsers
|
||||
const sorted = [...items].sort((a, b) => a.value - b.value);
|
||||
```
|
||||
|
||||
**Other immutable array methods:**
|
||||
|
||||
- `.toSorted()` - immutable sort
|
||||
- `.toReversed()` - immutable reverse
|
||||
- `.toSpliced()` - immutable splice
|
||||
- `.with()` - immutable element replacement
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Use Activity Component for Show/Hide
|
||||
impact: MEDIUM
|
||||
impactDescription: preserves state/DOM
|
||||
tags: rendering, activity, visibility, state-preservation
|
||||
---
|
||||
|
||||
## Use Activity Component for Show/Hide
|
||||
|
||||
Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
import { Activity } from "react";
|
||||
|
||||
function Dropdown({ isOpen }: Props) {
|
||||
return (
|
||||
<Activity mode={isOpen ? "visible" : "hidden"}>
|
||||
<ExpensiveMenu />
|
||||
</Activity>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Avoids expensive re-renders and state loss.
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Animate SVG Wrapper Instead of SVG Element
|
||||
impact: LOW
|
||||
impactDescription: enables hardware acceleration
|
||||
tags: rendering, svg, css, animation, performance
|
||||
---
|
||||
|
||||
## Animate SVG Wrapper Instead of SVG Element
|
||||
|
||||
Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.
|
||||
|
||||
**Incorrect (animating SVG directly - no hardware acceleration):**
|
||||
|
||||
```tsx
|
||||
function LoadingSpinner() {
|
||||
return (
|
||||
<svg className="animate-spin" width="24" height="24" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (animating wrapper div - hardware accelerated):**
|
||||
|
||||
```tsx
|
||||
function LoadingSpinner() {
|
||||
return (
|
||||
<div className="animate-spin">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Use Explicit Conditional Rendering
|
||||
impact: LOW
|
||||
impactDescription: prevents rendering 0 or NaN
|
||||
tags: rendering, conditional, jsx, falsy-values
|
||||
---
|
||||
|
||||
## Use Explicit Conditional Rendering
|
||||
|
||||
Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
|
||||
|
||||
**Incorrect (renders "0" when count is 0):**
|
||||
|
||||
```tsx
|
||||
function Badge({ count }: { count: number }) {
|
||||
return <div>{count && <span className="badge">{count}</span>}</div>;
|
||||
}
|
||||
|
||||
// When count = 0, renders: <div>0</div>
|
||||
// When count = 5, renders: <div><span class="badge">5</span></div>
|
||||
```
|
||||
|
||||
**Correct (renders nothing when count is 0):**
|
||||
|
||||
```tsx
|
||||
function Badge({ count }: { count: number }) {
|
||||
return <div>{count > 0 ? <span className="badge">{count}</span> : null}</div>;
|
||||
}
|
||||
|
||||
// When count = 0, renders: <div></div>
|
||||
// When count = 5, renders: <div><span class="badge">5</span></div>
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: CSS content-visibility for Long Lists
|
||||
impact: HIGH
|
||||
impactDescription: faster initial render
|
||||
tags: rendering, css, content-visibility, long-lists
|
||||
---
|
||||
|
||||
## CSS content-visibility for Long Lists
|
||||
|
||||
Apply `content-visibility: auto` to defer off-screen rendering.
|
||||
|
||||
**CSS:**
|
||||
|
||||
```css
|
||||
.message-item {
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: 0 80px;
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
function MessageList({ messages }: { messages: Message[] }) {
|
||||
return (
|
||||
<div className="overflow-y-auto h-screen">
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className="message-item">
|
||||
<Avatar user={msg.author} />
|
||||
<div>{msg.content}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Hoist Static JSX Elements
|
||||
impact: LOW
|
||||
impactDescription: avoids re-creation
|
||||
tags: rendering, jsx, static, optimization
|
||||
---
|
||||
|
||||
## Hoist Static JSX Elements
|
||||
|
||||
Extract static JSX outside components to avoid re-creation.
|
||||
|
||||
**Incorrect (recreates element every render):**
|
||||
|
||||
```tsx
|
||||
function LoadingSkeleton() {
|
||||
return <div className="animate-pulse h-20 bg-gray-200" />;
|
||||
}
|
||||
|
||||
function Container() {
|
||||
return <div>{loading && <LoadingSkeleton />}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (reuses same element):**
|
||||
|
||||
```tsx
|
||||
const loadingSkeleton = <div className="animate-pulse h-20 bg-gray-200" />;
|
||||
|
||||
function Container() {
|
||||
return <div>{loading && loadingSkeleton}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
|
||||
|
||||
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Prevent Hydration Mismatch Without Flickering
|
||||
impact: MEDIUM
|
||||
impactDescription: avoids visual flicker and hydration errors
|
||||
tags: rendering, ssr, hydration, localStorage, flicker
|
||||
---
|
||||
|
||||
## Prevent Hydration Mismatch Without Flickering
|
||||
|
||||
When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
|
||||
|
||||
**Incorrect (breaks SSR):**
|
||||
|
||||
```tsx
|
||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||
// localStorage is not available on server - throws error
|
||||
const theme = localStorage.getItem("theme") || "light";
|
||||
|
||||
return <div className={theme}>{children}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
Server-side rendering will fail because `localStorage` is undefined.
|
||||
|
||||
**Incorrect (visual flickering):**
|
||||
|
||||
```tsx
|
||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||
const [theme, setTheme] = useState("light");
|
||||
|
||||
useEffect(() => {
|
||||
// Runs after hydration - causes visible flash
|
||||
const stored = localStorage.getItem("theme");
|
||||
if (stored) {
|
||||
setTheme(stored);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <div className={theme}>{children}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
|
||||
|
||||
**Correct (no flicker, no hydration mismatch):**
|
||||
|
||||
```tsx
|
||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<div id="theme-wrapper">{children}</div>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
try {
|
||||
var theme = localStorage.getItem('theme') || 'light';
|
||||
var el = document.getElementById('theme-wrapper');
|
||||
if (el) el.className = theme;
|
||||
} catch (e) {}
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
|
||||
|
||||
This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Suppress Expected Hydration Mismatches
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: avoids noisy hydration warnings for known differences
|
||||
tags: rendering, hydration, ssr, nextjs
|
||||
---
|
||||
|
||||
## Suppress Expected Hydration Mismatches
|
||||
|
||||
In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these _expected_ mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
|
||||
|
||||
**Incorrect (known mismatch warnings):**
|
||||
|
||||
```tsx
|
||||
function Timestamp() {
|
||||
return <span>{new Date().toLocaleString()}</span>;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (suppress expected mismatch only):**
|
||||
|
||||
```tsx
|
||||
function Timestamp() {
|
||||
return <span suppressHydrationWarning>{new Date().toLocaleString()}</span>;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Optimize SVG Precision
|
||||
impact: LOW
|
||||
impactDescription: reduces file size
|
||||
tags: rendering, svg, optimization, svgo
|
||||
---
|
||||
|
||||
## Optimize SVG Precision
|
||||
|
||||
Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
|
||||
|
||||
**Incorrect (excessive precision):**
|
||||
|
||||
```svg
|
||||
<path d="M 10.293847 20.847362 L 30.938472 40.192837" />
|
||||
```
|
||||
|
||||
**Correct (1 decimal place):**
|
||||
|
||||
```svg
|
||||
<path d="M 10.3 20.8 L 30.9 40.2" />
|
||||
```
|
||||
|
||||
**Automate with SVGO:**
|
||||
|
||||
```bash
|
||||
npx svgo --precision=1 --multipass icon.svg
|
||||
```
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: Use useTransition Over Manual Loading States
|
||||
impact: LOW
|
||||
impactDescription: reduces re-renders and improves code clarity
|
||||
tags: rendering, transitions, useTransition, loading, state
|
||||
---
|
||||
|
||||
## Use useTransition Over Manual Loading States
|
||||
|
||||
Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
|
||||
|
||||
**Incorrect (manual loading state):**
|
||||
|
||||
```tsx
|
||||
function SearchResults() {
|
||||
const [query, setQuery] = useState("");
|
||||
const [results, setResults] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSearch = async (value: string) => {
|
||||
setIsLoading(true);
|
||||
setQuery(value);
|
||||
const data = await fetchResults(value);
|
||||
setResults(data);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input onChange={(e) => handleSearch(e.target.value)} />
|
||||
{isLoading && <Spinner />}
|
||||
<ResultsList results={results} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (useTransition with built-in pending state):**
|
||||
|
||||
```tsx
|
||||
import { useTransition, useState } from "react";
|
||||
|
||||
function SearchResults() {
|
||||
const [query, setQuery] = useState("");
|
||||
const [results, setResults] = useState([]);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setQuery(value); // Update input immediately
|
||||
|
||||
startTransition(async () => {
|
||||
// Fetch and update results
|
||||
const data = await fetchResults(value);
|
||||
setResults(data);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input onChange={(e) => handleSearch(e.target.value)} />
|
||||
{isPending && <Spinner />}
|
||||
<ResultsList results={results} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
|
||||
- **Error resilience**: Pending state correctly resets even if the transition throws
|
||||
- **Better responsiveness**: Keeps the UI responsive during updates
|
||||
- **Interrupt handling**: New transitions automatically cancel pending ones
|
||||
|
||||
Reference: [useTransition](https://react.dev/reference/react/useTransition)
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: Defer State Reads to Usage Point
|
||||
impact: MEDIUM
|
||||
impactDescription: avoids unnecessary subscriptions
|
||||
tags: rerender, searchParams, localStorage, optimization
|
||||
---
|
||||
|
||||
## Defer State Reads to Usage Point
|
||||
|
||||
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 <button onClick={handleShare}>Share</button>;
|
||||
}
|
||||
```
|
||||
|
||||
**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 <button onClick={handleShare}>Share</button>;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Narrow Effect Dependencies
|
||||
impact: LOW
|
||||
impactDescription: minimizes effect re-runs
|
||||
tags: rerender, useEffect, dependencies, optimization
|
||||
---
|
||||
|
||||
## Narrow Effect Dependencies
|
||||
|
||||
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]);
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Calculate Derived State During Rendering
|
||||
impact: MEDIUM
|
||||
impactDescription: avoids redundant renders and state drift
|
||||
tags: rerender, derived-state, useEffect, state
|
||||
---
|
||||
|
||||
## Calculate Derived State During Rendering
|
||||
|
||||
If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead.
|
||||
|
||||
**Incorrect (redundant state and effect):**
|
||||
|
||||
```tsx
|
||||
function Form() {
|
||||
const [firstName, setFirstName] = useState("First");
|
||||
const [lastName, setLastName] = useState("Last");
|
||||
const [fullName, setFullName] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + " " + lastName);
|
||||
}, [firstName, lastName]);
|
||||
|
||||
return <p>{fullName}</p>;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (derive during render):**
|
||||
|
||||
```tsx
|
||||
function Form() {
|
||||
const [firstName, setFirstName] = useState("First");
|
||||
const [lastName, setLastName] = useState("Last");
|
||||
const fullName = firstName + " " + lastName;
|
||||
|
||||
return <p>{fullName}</p>;
|
||||
}
|
||||
```
|
||||
|
||||
References: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Subscribe to Derived State
|
||||
impact: MEDIUM
|
||||
impactDescription: reduces re-render frequency
|
||||
tags: rerender, derived-state, media-query, optimization
|
||||
---
|
||||
|
||||
## Subscribe to Derived State
|
||||
|
||||
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 <nav className={isMobile ? "mobile" : "desktop"} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (re-renders only when boolean changes):**
|
||||
|
||||
```tsx
|
||||
function Sidebar() {
|
||||
const isMobile = useMediaQuery("(max-width: 767px)");
|
||||
return <nav className={isMobile ? "mobile" : "desktop"} />;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Use Functional setState Updates
|
||||
impact: MEDIUM
|
||||
impactDescription: prevents stale closures and unnecessary callback recreations
|
||||
tags: react, hooks, useState, useCallback, callbacks, closures
|
||||
---
|
||||
|
||||
## Use Functional setState Updates
|
||||
|
||||
When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
|
||||
|
||||
**Incorrect (requires state as dependency):**
|
||||
|
||||
```tsx
|
||||
function TodoList() {
|
||||
const [items, setItems] = useState(initialItems);
|
||||
|
||||
// Callback must depend on items, recreated on every items change
|
||||
const addItems = useCallback(
|
||||
(newItems: Item[]) => {
|
||||
setItems([...items, ...newItems]);
|
||||
},
|
||||
[items],
|
||||
); // ❌ items dependency causes recreations
|
||||
|
||||
// Risk of stale closure if dependency is forgotten
|
||||
const removeItem = useCallback((id: string) => {
|
||||
setItems(items.filter((item) => item.id !== id));
|
||||
}, []); // ❌ Missing items dependency - will use stale items!
|
||||
|
||||
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
|
||||
}
|
||||
```
|
||||
|
||||
The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
|
||||
|
||||
**Correct (stable callbacks, no stale closures):**
|
||||
|
||||
```tsx
|
||||
function TodoList() {
|
||||
const [items, setItems] = useState(initialItems);
|
||||
|
||||
// Stable callback, never recreated
|
||||
const addItems = useCallback((newItems: Item[]) => {
|
||||
setItems((curr) => [...curr, ...newItems]);
|
||||
}, []); // ✅ No dependencies needed
|
||||
|
||||
// Always uses latest state, no stale closure risk
|
||||
const removeItem = useCallback((id: string) => {
|
||||
setItems((curr) => curr.filter((item) => item.id !== id));
|
||||
}, []); // ✅ Safe and stable
|
||||
|
||||
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
1. **Stable callback references** - Callbacks don't need to be recreated when state changes
|
||||
2. **No stale closures** - Always operates on the latest state value
|
||||
3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
|
||||
4. **Prevents bugs** - Eliminates the most common source of React closure bugs
|
||||
|
||||
**When to use functional updates:**
|
||||
|
||||
- Any setState that depends on the current state value
|
||||
- Inside useCallback/useMemo when state is needed
|
||||
- Event handlers that reference state
|
||||
- Async operations that update state
|
||||
|
||||
**When direct updates are fine:**
|
||||
|
||||
- Setting state to a static value: `setCount(0)`
|
||||
- Setting state from props/arguments only: `setName(newName)`
|
||||
- State doesn't depend on previous value
|
||||
|
||||
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: Use Lazy State Initialization
|
||||
impact: MEDIUM
|
||||
impactDescription: wasted computation on every render
|
||||
tags: react, hooks, useState, performance, initialization
|
||||
---
|
||||
|
||||
## Use Lazy State Initialization
|
||||
|
||||
Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
|
||||
|
||||
**Incorrect (runs on every render):**
|
||||
|
||||
```tsx
|
||||
function FilteredList({ items }: { items: Item[] }) {
|
||||
// buildSearchIndex() runs on EVERY render, even after initialization
|
||||
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items));
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
// When query changes, buildSearchIndex runs again unnecessarily
|
||||
return <SearchResults index={searchIndex} query={query} />;
|
||||
}
|
||||
|
||||
function UserProfile() {
|
||||
// JSON.parse runs on every render
|
||||
const [settings, setSettings] = useState(JSON.parse(localStorage.getItem("settings") || "{}"));
|
||||
|
||||
return <SettingsForm settings={settings} onChange={setSettings} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (runs only once):**
|
||||
|
||||
```tsx
|
||||
function FilteredList({ items }: { items: Item[] }) {
|
||||
// buildSearchIndex() runs ONLY on initial render
|
||||
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items));
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
return <SearchResults index={searchIndex} query={query} />;
|
||||
}
|
||||
|
||||
function UserProfile() {
|
||||
// JSON.parse runs only on initial render
|
||||
const [settings, setSettings] = useState(() => {
|
||||
const stored = localStorage.getItem("settings");
|
||||
return stored ? JSON.parse(stored) : {};
|
||||
});
|
||||
|
||||
return <SettingsForm settings={settings} onChange={setSettings} />;
|
||||
}
|
||||
```
|
||||
|
||||
Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
|
||||
|
||||
For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Extract Default Non-primitive Parameter Value from Memoized Component to Constant
|
||||
impact: MEDIUM
|
||||
impactDescription: restores memoization by using a constant for default value
|
||||
tags: rerender, memo, optimization
|
||||
---
|
||||
|
||||
## Extract Default Non-primitive Parameter Value from Memoized Component to Constant
|
||||
|
||||
When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.
|
||||
|
||||
To address this issue, extract the default value into a constant.
|
||||
|
||||
**Incorrect (`onClick` has different values on every rerender):**
|
||||
|
||||
```tsx
|
||||
const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
|
||||
// ...
|
||||
})
|
||||
|
||||
// Used without optional onClick
|
||||
<UserAvatar />
|
||||
```
|
||||
|
||||
**Correct (stable default value):**
|
||||
|
||||
```tsx
|
||||
const NOOP = () => {};
|
||||
|
||||
const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
|
||||
// ...
|
||||
})
|
||||
|
||||
// Used without optional onClick
|
||||
<UserAvatar />
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Extract to Memoized Components
|
||||
impact: MEDIUM
|
||||
impactDescription: enables early returns
|
||||
tags: rerender, memo, useMemo, optimization
|
||||
---
|
||||
|
||||
## Extract to Memoized Components
|
||||
|
||||
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 <Avatar id={id} />;
|
||||
}, [user]);
|
||||
|
||||
if (loading) return <Skeleton />;
|
||||
return <div>{avatar}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (skips computation when loading):**
|
||||
|
||||
```tsx
|
||||
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
|
||||
const id = useMemo(() => computeAvatarId(user), [user]);
|
||||
return <Avatar id={id} />;
|
||||
});
|
||||
|
||||
function Profile({ user, loading }: Props) {
|
||||
if (loading) return <Skeleton />;
|
||||
return (
|
||||
<div>
|
||||
<UserAvatar user={user} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**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.
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Put Interaction Logic in Event Handlers
|
||||
impact: MEDIUM
|
||||
impactDescription: avoids effect re-runs and duplicate side effects
|
||||
tags: rerender, useEffect, events, side-effects, dependencies
|
||||
---
|
||||
|
||||
## Put Interaction Logic in Event Handlers
|
||||
|
||||
If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action.
|
||||
|
||||
**Incorrect (event modeled as state + effect):**
|
||||
|
||||
```tsx
|
||||
function Form() {
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (submitted) {
|
||||
post("/api/register");
|
||||
showToast("Registered", theme);
|
||||
}
|
||||
}, [submitted, theme]);
|
||||
|
||||
return <button onClick={() => setSubmitted(true)}>Submit</button>;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (do it in the handler):**
|
||||
|
||||
```tsx
|
||||
function Form() {
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
function handleSubmit() {
|
||||
post("/api/register");
|
||||
showToast("Registered", theme);
|
||||
}
|
||||
|
||||
return <button onClick={handleSubmit}>Submit</button>;
|
||||
}
|
||||
```
|
||||
|
||||
Reference: [Should this code move to an event handler?](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Do not wrap a simple expression with a primitive result type in useMemo
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: wasted computation on every render
|
||||
tags: rerender, useMemo, optimization
|
||||
---
|
||||
|
||||
## Do not wrap a simple expression with a primitive result type in useMemo
|
||||
|
||||
When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.
|
||||
Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
function Header({ user, notifications }: Props) {
|
||||
const isLoading = useMemo(() => {
|
||||
return user.isLoading || notifications.isLoading;
|
||||
}, [user.isLoading, notifications.isLoading]);
|
||||
|
||||
if (isLoading) return <Skeleton />;
|
||||
// return some markup
|
||||
}
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
function Header({ user, notifications }: Props) {
|
||||
const isLoading = user.isLoading || notifications.isLoading;
|
||||
|
||||
if (isLoading) return <Skeleton />;
|
||||
// return some markup
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Use Transitions for Non-Urgent Updates
|
||||
impact: MEDIUM
|
||||
impactDescription: maintains UI responsiveness
|
||||
tags: rerender, transitions, startTransition, performance
|
||||
---
|
||||
|
||||
## Use Transitions for Non-Urgent Updates
|
||||
|
||||
Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
|
||||
|
||||
**Incorrect (blocks UI on every scroll):**
|
||||
|
||||
```tsx
|
||||
function ScrollTracker() {
|
||||
const [scrollY, setScrollY] = useState(0);
|
||||
useEffect(() => {
|
||||
const handler = () => setScrollY(window.scrollY);
|
||||
window.addEventListener("scroll", handler, { passive: true });
|
||||
return () => window.removeEventListener("scroll", handler);
|
||||
}, []);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (non-blocking updates):**
|
||||
|
||||
```tsx
|
||||
import { startTransition } from "react";
|
||||
|
||||
function ScrollTracker() {
|
||||
const [scrollY, setScrollY] = useState(0);
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
startTransition(() => setScrollY(window.scrollY));
|
||||
};
|
||||
window.addEventListener("scroll", handler, { passive: true });
|
||||
return () => window.removeEventListener("scroll", handler);
|
||||
}, []);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: Use useRef for Transient Values
|
||||
impact: MEDIUM
|
||||
impactDescription: avoids unnecessary re-renders on frequent updates
|
||||
tags: rerender, useref, state, performance
|
||||
---
|
||||
|
||||
## Use useRef for Transient Values
|
||||
|
||||
When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
|
||||
|
||||
**Incorrect (renders every update):**
|
||||
|
||||
```tsx
|
||||
function Tracker() {
|
||||
const [lastX, setLastX] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const onMove = (e: MouseEvent) => setLastX(e.clientX);
|
||||
window.addEventListener("mousemove", onMove);
|
||||
return () => window.removeEventListener("mousemove", onMove);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: lastX,
|
||||
width: 8,
|
||||
height: 8,
|
||||
background: "black",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (no re-render for tracking):**
|
||||
|
||||
```tsx
|
||||
function Tracker() {
|
||||
const lastXRef = useRef(0);
|
||||
const dotRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const onMove = (e: MouseEvent) => {
|
||||
lastXRef.current = e.clientX;
|
||||
const node = dotRef.current;
|
||||
if (node) {
|
||||
node.style.transform = `translateX(${e.clientX}px)`;
|
||||
}
|
||||
};
|
||||
window.addEventListener("mousemove", onMove);
|
||||
return () => window.removeEventListener("mousemove", onMove);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={dotRef}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 8,
|
||||
height: 8,
|
||||
background: "black",
|
||||
transform: "translateX(0px)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: Use after() for Non-Blocking Operations
|
||||
impact: MEDIUM
|
||||
impactDescription: faster response times
|
||||
tags: server, async, logging, analytics, side-effects
|
||||
---
|
||||
|
||||
## Use after() for Non-Blocking Operations
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Authenticate Server Actions Like API Routes
|
||||
impact: CRITICAL
|
||||
impactDescription: prevents unauthorized access to server mutations
|
||||
tags: server, server-actions, authentication, security, authorization
|
||||
---
|
||||
|
||||
## Authenticate Server Actions Like API Routes
|
||||
|
||||
**Impact: CRITICAL (prevents unauthorized access to server mutations)**
|
||||
|
||||
Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.
|
||||
|
||||
Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."
|
||||
|
||||
**Incorrect (no authentication check):**
|
||||
|
||||
```typescript
|
||||
"use server";
|
||||
|
||||
export async function deleteUser(userId: string) {
|
||||
// Anyone can call this! No auth check
|
||||
await db.user.delete({ where: { id: userId } });
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (authentication inside the action):**
|
||||
|
||||
```typescript
|
||||
"use server";
|
||||
|
||||
import { verifySession } from "@/lib/auth";
|
||||
import { unauthorized } from "@/lib/errors";
|
||||
|
||||
export async function deleteUser(userId: string) {
|
||||
// Always check auth inside the action
|
||||
const session = await verifySession();
|
||||
|
||||
if (!session) {
|
||||
throw unauthorized("Must be logged in");
|
||||
}
|
||||
|
||||
// Check authorization too
|
||||
if (session.user.role !== "admin" && session.user.id !== userId) {
|
||||
throw unauthorized("Cannot delete other users");
|
||||
}
|
||||
|
||||
await db.user.delete({ where: { id: userId } });
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
|
||||
**With input validation:**
|
||||
|
||||
```typescript
|
||||
"use server";
|
||||
|
||||
import { verifySession } from "@/lib/auth";
|
||||
import { z } from "zod";
|
||||
|
||||
const updateProfileSchema = z.object({
|
||||
userId: z.string().uuid(),
|
||||
name: z.string().min(1).max(100),
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
export async function updateProfile(data: unknown) {
|
||||
// Validate input first
|
||||
const validated = updateProfileSchema.parse(data);
|
||||
|
||||
// Then authenticate
|
||||
const session = await verifySession();
|
||||
if (!session) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
// Then authorize
|
||||
if (session.user.id !== validated.userId) {
|
||||
throw new Error("Can only update own profile");
|
||||
}
|
||||
|
||||
// Finally perform the mutation
|
||||
await db.user.update({
|
||||
where: { id: validated.userId },
|
||||
data: {
|
||||
name: validated.name,
|
||||
email: validated.email,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
|
||||
Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: Cross-Request LRU Caching
|
||||
impact: HIGH
|
||||
impactDescription: caches across requests
|
||||
tags: server, cache, lru, cross-request
|
||||
---
|
||||
|
||||
## Cross-Request LRU Caching
|
||||
|
||||
`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<string, any>({
|
||||
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)
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: Per-Request Deduplication with React.cache()
|
||||
impact: MEDIUM
|
||||
impactDescription: deduplicates within request
|
||||
tags: server, cache, react-cache, deduplication
|
||||
---
|
||||
|
||||
## Per-Request Deduplication with React.cache()
|
||||
|
||||
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 getUser = cache(async (uid: number) => {
|
||||
return await db.user.findUnique({ where: { id: uid } });
|
||||
});
|
||||
|
||||
// Primitive args use value equality
|
||||
getUser(1);
|
||||
getUser(1); // Cache hit, returns cached result
|
||||
```
|
||||
|
||||
If you must pass objects, pass the same reference:
|
||||
|
||||
```typescript
|
||||
const params = { uid: 1 };
|
||||
getUser(params); // Query runs
|
||||
getUser(params); // Cache hit (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: [React.cache documentation](https://react.dev/reference/react/cache)
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: Avoid Duplicate Serialization in RSC Props
|
||||
impact: LOW
|
||||
impactDescription: reduces network payload by avoiding duplicate serialization
|
||||
tags: server, rsc, serialization, props, client-components
|
||||
---
|
||||
|
||||
## Avoid Duplicate Serialization in RSC Props
|
||||
|
||||
**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
|
||||
|
||||
RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.
|
||||
|
||||
**Incorrect (duplicates array):**
|
||||
|
||||
```tsx
|
||||
// RSC: sends 6 strings (2 arrays × 3 items)
|
||||
<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
|
||||
```
|
||||
|
||||
**Correct (sends 3 strings):**
|
||||
|
||||
```tsx
|
||||
// RSC: send once
|
||||
<ClientList usernames={usernames} />;
|
||||
|
||||
// Client: transform there
|
||||
("use client");
|
||||
const sorted = useMemo(() => [...usernames].sort(), [usernames]);
|
||||
```
|
||||
|
||||
**Nested deduplication behavior:**
|
||||
|
||||
Deduplication works recursively. Impact varies by data type:
|
||||
|
||||
- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
|
||||
- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
|
||||
|
||||
```tsx
|
||||
// string[] - duplicates everything
|
||||
usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
|
||||
|
||||
// object[] - duplicates array structure only
|
||||
users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
|
||||
```
|
||||
|
||||
**Operations breaking deduplication (create new references):**
|
||||
|
||||
- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
|
||||
- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
|
||||
|
||||
**More examples:**
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
<C users={users} active={users.filter(u => u.active)} />
|
||||
<C product={product} productName={product.name} />
|
||||
|
||||
// ✅ Good
|
||||
<C users={users} />
|
||||
<C product={product} />
|
||||
// Do filtering/destructuring in client
|
||||
```
|
||||
|
||||
**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: Parallel Data Fetching with Component Composition
|
||||
impact: CRITICAL
|
||||
impactDescription: eliminates server-side waterfalls
|
||||
tags: server, rsc, parallel-fetching, composition
|
||||
---
|
||||
|
||||
## Parallel Data Fetching with Component Composition
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<div>{header}</div>
|
||||
<Sidebar />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function Sidebar() {
|
||||
const items = await fetchSidebarItems();
|
||||
return <nav>{items.map(renderItem)}</nav>;
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (both fetch simultaneously):**
|
||||
|
||||
```tsx
|
||||
async function Header() {
|
||||
const data = await fetchHeader();
|
||||
return <div>{data}</div>;
|
||||
}
|
||||
|
||||
async function Sidebar() {
|
||||
const items = await fetchSidebarItems();
|
||||
return <nav>{items.map(renderItem)}</nav>;
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<Sidebar />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Alternative with children prop:**
|
||||
|
||||
```tsx
|
||||
async function Header() {
|
||||
const data = await fetchHeader();
|
||||
return <div>{data}</div>;
|
||||
}
|
||||
|
||||
async function Sidebar() {
|
||||
const items = await fetchSidebarItems();
|
||||
return <nav>{items.map(renderItem)}</nav>;
|
||||
}
|
||||
|
||||
function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Layout>
|
||||
<Sidebar />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
title: Minimize Serialization at RSC Boundaries
|
||||
impact: HIGH
|
||||
impactDescription: reduces data transfer size
|
||||
tags: server, rsc, serialization, props
|
||||
---
|
||||
|
||||
## Minimize Serialization at RSC Boundaries
|
||||
|
||||
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 <Profile user={user} />;
|
||||
}
|
||||
|
||||
("use client");
|
||||
function Profile({ user }: { user: User }) {
|
||||
return <div>{user.name}</div>; // uses 1 field
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (serializes only 1 field):**
|
||||
|
||||
```tsx
|
||||
async function Page() {
|
||||
const user = await fetchUser();
|
||||
return <Profile name={user.name} />;
|
||||
}
|
||||
|
||||
("use client");
|
||||
function Profile({ name }: { name: string }) {
|
||||
return <div>{name}</div>;
|
||||
}
|
||||
```
|
||||
40
.agents/skills/web-design-guidelines/SKILL.md
Normal file
40
.agents/skills/web-design-guidelines/SKILL.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: web-design-guidelines
|
||||
description: Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
|
||||
metadata:
|
||||
author: vercel
|
||||
version: "1.0.0"
|
||||
argument-hint: <file-or-pattern>
|
||||
---
|
||||
|
||||
# Web Interface Guidelines
|
||||
|
||||
Review files for compliance with Web Interface Guidelines.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. Fetch the latest guidelines from the source URL below
|
||||
2. Read the specified files (or prompt user for files/pattern)
|
||||
3. Check against all rules in the fetched guidelines
|
||||
4. Output findings in the terse `file:line` format
|
||||
|
||||
## Guidelines Source
|
||||
|
||||
Fetch fresh guidelines before each review:
|
||||
|
||||
```
|
||||
https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md
|
||||
```
|
||||
|
||||
Use WebFetch to retrieve the latest rules. The fetched content contains all the rules and output format instructions.
|
||||
|
||||
## Usage
|
||||
|
||||
When a user provides a file or pattern argument:
|
||||
|
||||
1. Fetch guidelines from the source URL above
|
||||
2. Read the specified files
|
||||
3. Apply all rules from the fetched guidelines
|
||||
4. Output findings using the format specified in the guidelines
|
||||
|
||||
If no files specified, ask the user which files to review.
|
||||
1
.claude/skills/better-auth-best-practices
Symbolic link
1
.claude/skills/better-auth-best-practices
Symbolic link
@@ -0,0 +1 @@
|
||||
../../.agents/skills/better-auth-best-practices
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user