mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-31 12:23:33 -06:00
Compare commits
1 Commits
4.6.0-rc.1
...
chore-agen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4eb96b491e |
71
.agent/scripts/README.md
Normal file
71
.agent/scripts/README.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Skill Filter
|
||||
|
||||
Automatically filters Vercel React best practices to reduce AI token costs while keeping high-impact performance patterns.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Fetch and filter (first time)
|
||||
pnpm filter-skills --fetch
|
||||
|
||||
# Re-filter after config changes
|
||||
pnpm filter-skills
|
||||
```
|
||||
|
||||
**Result:** ~50% reduction in skill files (keeps CRITICAL/HIGH/MEDIUM priorities, removes LOW priority rules)
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `.agent/skills/react-best-practices/skill-filter-config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"featureFlags": {
|
||||
"keepCriticalPriority": true, // async-*, bundle-*
|
||||
"keepHighPriority": true, // server-*
|
||||
"keepMediumPriority": true, // rerender-*
|
||||
"keepLowPriority": false, // js-*, rendering-*, advanced-*
|
||||
"removeJsOptimizations": true,
|
||||
"removeRenderingOptimizations": true,
|
||||
"removeAdvancedPatterns": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Toggle LOW priority rules:** Set `keepLowPriority: true`
|
||||
|
||||
## What It Does
|
||||
|
||||
1. Downloads latest skills from GitHub (with `--fetch`)
|
||||
2. Filters based on priority and used technologies
|
||||
3. Archives unused rules to `.archived/` (not tracked in git)
|
||||
4. Formats markdown with Prettier to match project style
|
||||
|
||||
## Why This Works
|
||||
|
||||
- **AI Skills = Proactive:** Guide developers to write correct code from the start
|
||||
- **Linting = Reactive:** Catch mistakes after code is written
|
||||
- **Together:** AI prevents issues, linting catches what slips through
|
||||
|
||||
Token costs are an investment in preventing technical debt rather than fixing it later.
|
||||
|
||||
## Restore Archived Rules
|
||||
|
||||
```bash
|
||||
mv .agent/skills/react-best-practices/.archived/rule-name.md \
|
||||
.agent/skills/react-best-practices/rules/
|
||||
```
|
||||
|
||||
Then re-run: `pnpm filter-skills`
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
pnpm filter-skills # Filter with current config
|
||||
pnpm filter-skills:dry-run # Preview changes
|
||||
pnpm filter-skills --fetch # Fetch latest + filter
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Source:** [vercel-labs/agent-skills](https://github.com/vercel-labs/agent-skills)
|
||||
476
.agent/scripts/filter-skills.ts
Executable file
476
.agent/scripts/filter-skills.ts
Executable file
@@ -0,0 +1,476 @@
|
||||
#!/usr/bin/env tsx
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
interface FilterConfig {
|
||||
featureFlags: {
|
||||
keepCriticalPriority: boolean;
|
||||
keepHighPriority: boolean;
|
||||
keepMediumPriority: boolean;
|
||||
keepLowPriority: boolean;
|
||||
removeJsOptimizations: boolean;
|
||||
removeRenderingOptimizations: boolean;
|
||||
removeAdvancedPatterns: boolean;
|
||||
};
|
||||
priorities: {
|
||||
keep: string[];
|
||||
conditionalKeep: string[];
|
||||
remove: string[];
|
||||
};
|
||||
technologyDetection: Record<string, {
|
||||
packageNames: string[];
|
||||
codePatterns: string[];
|
||||
relatedRules: string[];
|
||||
}>;
|
||||
alwaysKeep: string[];
|
||||
alwaysRemove: string[];
|
||||
}
|
||||
|
||||
interface FilterReport {
|
||||
kept: { file: string; reason: string }[];
|
||||
archived: { file: string; reason: string }[];
|
||||
technologiesDetected: string[];
|
||||
summary: {
|
||||
totalRules: number;
|
||||
keptRules: number;
|
||||
archivedRules: number;
|
||||
reductionPercent: number;
|
||||
};
|
||||
}
|
||||
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
||||
const SKILLS_DIR = path.join(PROJECT_ROOT, '.agent/skills/react-best-practices');
|
||||
const RULES_DIR = path.join(SKILLS_DIR, 'rules');
|
||||
const ARCHIVE_DIR = path.join(SKILLS_DIR, '.archived');
|
||||
const CONFIG_PATH = path.join(SKILLS_DIR, 'skill-filter-config.json');
|
||||
const PACKAGE_JSON_PATH = path.join(PROJECT_ROOT, 'package.json');
|
||||
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const isDryRun = args.includes('--dry-run');
|
||||
const validateOnly = args.includes('--validate-config');
|
||||
|
||||
function loadConfig(): FilterConfig {
|
||||
const configContent = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
||||
return JSON.parse(configContent);
|
||||
}
|
||||
|
||||
function validateConfig(config: FilterConfig): void {
|
||||
console.log('✓ Configuration is valid');
|
||||
console.log(` - ${config.alwaysKeep.length} rules marked as always keep`);
|
||||
console.log(` - ${config.alwaysRemove.length} rules marked as always remove`);
|
||||
console.log(` - ${Object.keys(config.technologyDetection).length} technologies configured for detection`);
|
||||
}
|
||||
|
||||
function detectTechnologies(config: FilterConfig): Set<string> {
|
||||
const detected = new Set<string>();
|
||||
|
||||
// Check package.json dependencies
|
||||
const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf-8'));
|
||||
const allDeps = {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies,
|
||||
};
|
||||
|
||||
for (const [techName, techConfig] of Object.entries(config.technologyDetection)) {
|
||||
// Check for package dependencies
|
||||
const hasPackage = techConfig.packageNames.some(pkg => allDeps[pkg]);
|
||||
|
||||
if (hasPackage) {
|
||||
detected.add(techName);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for code patterns using ripgrep
|
||||
if (techConfig.codePatterns.length > 0) {
|
||||
for (const pattern of techConfig.codePatterns) {
|
||||
try {
|
||||
// Use ripgrep to search for patterns in TypeScript/JavaScript files
|
||||
execSync(
|
||||
`rg -q '${pattern.replace(/'/g, "\\'")}' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' "${PROJECT_ROOT}"`,
|
||||
{ stdio: 'pipe' }
|
||||
);
|
||||
detected.add(techName);
|
||||
break;
|
||||
} catch (e) {
|
||||
// Pattern not found, continue checking
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return detected;
|
||||
}
|
||||
|
||||
function getRulePriority(filename: string): string | null {
|
||||
const content = fs.readFileSync(path.join(RULES_DIR, filename), 'utf-8');
|
||||
const match = content.match(/impact:\s*([A-Z-]+)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function checkFeatureFlagOverride(
|
||||
filename: string,
|
||||
flags: FilterConfig['featureFlags']
|
||||
): { keep: boolean; reason: string } | null {
|
||||
if (flags.removeJsOptimizations && filename.startsWith('js-')) {
|
||||
return { keep: false, reason: 'Feature flag: removeJsOptimizations' };
|
||||
}
|
||||
if (flags.removeRenderingOptimizations && filename.startsWith('rendering-')) {
|
||||
return { keep: false, reason: 'Feature flag: removeRenderingOptimizations' };
|
||||
}
|
||||
if (flags.removeAdvancedPatterns && filename.startsWith('advanced-')) {
|
||||
return { keep: false, reason: 'Feature flag: removeAdvancedPatterns' };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkAlwaysKeepOrRemove(
|
||||
filename: string,
|
||||
config: FilterConfig
|
||||
): { keep: boolean; reason: string } | null {
|
||||
if (config.alwaysKeep.includes(filename)) {
|
||||
return { keep: true, reason: 'Always keep (critical pattern)' };
|
||||
}
|
||||
if (config.alwaysRemove.includes(filename)) {
|
||||
return { keep: false, reason: 'Always remove (low priority optimization)' };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkTechnologyDetection(
|
||||
filename: string,
|
||||
config: FilterConfig,
|
||||
detectedTechnologies: Set<string>
|
||||
): { keep: boolean; reason: string } | null {
|
||||
for (const [techName, techConfig] of Object.entries(config.technologyDetection)) {
|
||||
if (techConfig.relatedRules.includes(filename)) {
|
||||
if (detectedTechnologies.has(techName)) {
|
||||
return { keep: true, reason: `Technology detected: ${techName}` };
|
||||
} else {
|
||||
return { keep: false, reason: `Technology not used: ${techName}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkPriorityWithFlags(
|
||||
priority: string,
|
||||
flags: FilterConfig['featureFlags'],
|
||||
priorities: FilterConfig['priorities']
|
||||
): { keep: boolean; reason: string } | null {
|
||||
// Check feature flag overrides
|
||||
if (priority === 'CRITICAL' && !flags.keepCriticalPriority) {
|
||||
return { keep: false, reason: 'Feature flag: keepCriticalPriority disabled' };
|
||||
}
|
||||
if (priority === 'HIGH' && !flags.keepHighPriority) {
|
||||
return { keep: false, reason: 'Feature flag: keepHighPriority disabled' };
|
||||
}
|
||||
if ((priority === 'MEDIUM' || priority === 'MEDIUM-HIGH') && !flags.keepMediumPriority) {
|
||||
return { keep: false, reason: 'Feature flag: keepMediumPriority disabled' };
|
||||
}
|
||||
if ((priority === 'LOW' || priority === 'LOW-MEDIUM') && flags.keepLowPriority) {
|
||||
return { keep: true, reason: 'Feature flag: keepLowPriority enabled' };
|
||||
}
|
||||
|
||||
// Check priority configuration
|
||||
if (priorities.keep.includes(priority)) {
|
||||
return { keep: true, reason: `Priority: ${priority}` };
|
||||
}
|
||||
if (priorities.conditionalKeep.includes(priority)) {
|
||||
return { keep: true, reason: `Priority: ${priority} (conditional keep)` };
|
||||
}
|
||||
if (priorities.remove.includes(priority)) {
|
||||
return { keep: false, reason: `Priority: ${priority}` };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function shouldKeepRule(
|
||||
filename: string,
|
||||
config: FilterConfig,
|
||||
detectedTechnologies: Set<string>
|
||||
): { keep: boolean; reason: string } {
|
||||
const flags = config.featureFlags;
|
||||
|
||||
// Check feature flag overrides first
|
||||
const flagOverride = checkFeatureFlagOverride(filename, flags);
|
||||
if (flagOverride) return flagOverride;
|
||||
|
||||
// Check always keep/remove lists
|
||||
const alwaysRule = checkAlwaysKeepOrRemove(filename, config);
|
||||
if (alwaysRule) return alwaysRule;
|
||||
|
||||
// Check technology detection
|
||||
const techRule = checkTechnologyDetection(filename, config, detectedTechnologies);
|
||||
if (techRule) return techRule;
|
||||
|
||||
// Check priority with feature flags
|
||||
const priority = getRulePriority(filename);
|
||||
if (priority) {
|
||||
const priorityRule = checkPriorityWithFlags(priority, flags, config.priorities);
|
||||
if (priorityRule) return priorityRule;
|
||||
}
|
||||
|
||||
// Default: keep if uncertain
|
||||
return { keep: true, reason: 'Default (no matching rule)' };
|
||||
}
|
||||
|
||||
function filterRules(config: FilterConfig, detectedTechnologies: Set<string>): FilterReport {
|
||||
const report: FilterReport = {
|
||||
kept: [],
|
||||
archived: [],
|
||||
technologiesDetected: Array.from(detectedTechnologies),
|
||||
summary: {
|
||||
totalRules: 0,
|
||||
keptRules: 0,
|
||||
archivedRules: 0,
|
||||
reductionPercent: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const ruleFiles = fs.readdirSync(RULES_DIR).filter(f => f.endsWith('.md'));
|
||||
report.summary.totalRules = ruleFiles.length;
|
||||
|
||||
for (const filename of ruleFiles) {
|
||||
const decision = shouldKeepRule(filename, config, detectedTechnologies);
|
||||
|
||||
if (decision.keep) {
|
||||
report.kept.push({ file: filename, reason: decision.reason });
|
||||
report.summary.keptRules++;
|
||||
} else {
|
||||
report.archived.push({ file: filename, reason: decision.reason });
|
||||
report.summary.archivedRules++;
|
||||
|
||||
if (!isDryRun) {
|
||||
// Move to archive
|
||||
const sourcePath = path.join(RULES_DIR, filename);
|
||||
const archivePath = path.join(ARCHIVE_DIR, filename);
|
||||
fs.mkdirSync(ARCHIVE_DIR, { recursive: true });
|
||||
fs.renameSync(sourcePath, archivePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
report.summary.reductionPercent = Math.round(
|
||||
(report.summary.archivedRules / report.summary.totalRules) * 100
|
||||
);
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
function printReport(report: FilterReport): void {
|
||||
console.log('\n' + '='.repeat(80));
|
||||
console.log('SKILL FILTER REPORT');
|
||||
console.log('='.repeat(80) + '\n');
|
||||
|
||||
console.log('📊 SUMMARY');
|
||||
console.log(` Total rules: ${report.summary.totalRules}`);
|
||||
console.log(` Kept: ${report.summary.keptRules} (${100 - report.summary.reductionPercent}%)`);
|
||||
console.log(` Archived: ${report.summary.archivedRules} (${report.summary.reductionPercent}%)`);
|
||||
console.log('');
|
||||
|
||||
console.log('🔍 TECHNOLOGIES DETECTED');
|
||||
if (report.technologiesDetected.length > 0) {
|
||||
report.technologiesDetected.forEach(tech => console.log(` ✓ ${tech}`));
|
||||
} else {
|
||||
console.log(' (none detected)');
|
||||
}
|
||||
console.log('');
|
||||
|
||||
console.log('✅ KEPT RULES (' + report.kept.length + ')');
|
||||
report.kept.forEach(({ file, reason }) => {
|
||||
console.log(` • ${file.padEnd(45)} → ${reason}`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
console.log('📦 ARCHIVED RULES (' + report.archived.length + ')');
|
||||
report.archived.forEach(({ file, reason }) => {
|
||||
console.log(` • ${file.padEnd(45)} → ${reason}`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
if (isDryRun) {
|
||||
console.log('🔍 DRY RUN MODE - No files were modified');
|
||||
} else {
|
||||
console.log('✨ Filtering complete! Archived rules moved to .archived/');
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
function saveReport(report: FilterReport): void {
|
||||
const reportPath = path.join(SKILLS_DIR, 'filter-report.json');
|
||||
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
||||
console.log(`📝 Report saved to: ${reportPath}`);
|
||||
}
|
||||
|
||||
function fetchSkills(): void {
|
||||
console.log('📥 Fetching Vercel React best practices from GitHub...\n');
|
||||
|
||||
const tempDir = path.join(PROJECT_ROOT, '.agent/.temp-skills');
|
||||
const tarballUrl = 'https://github.com/vercel-labs/agent-skills/archive/refs/heads/main.tar.gz';
|
||||
|
||||
try {
|
||||
// Clean up temp directory if it exists
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
|
||||
console.log(' → Downloading tarball from GitHub...');
|
||||
// Download and extract the entire repo first
|
||||
execSync(
|
||||
`curl -sL ${tarballUrl} | tar -xz -C "${tempDir}"`,
|
||||
{ stdio: 'pipe' }
|
||||
);
|
||||
|
||||
// Find the extracted directory and move the skills subdirectory
|
||||
const extractedDir = path.join(tempDir, 'agent-skills-main/skills/react-best-practices');
|
||||
|
||||
if (!fs.existsSync(extractedDir)) {
|
||||
throw new Error(`Skills directory not found in tarball: ${extractedDir}`);
|
||||
}
|
||||
|
||||
// Move to final location
|
||||
if (fs.existsSync(SKILLS_DIR)) {
|
||||
console.log(' → Removing old skills...');
|
||||
fs.rmSync(SKILLS_DIR, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
console.log(' → Installing to .agent/skills/...');
|
||||
fs.mkdirSync(path.dirname(SKILLS_DIR), { recursive: true });
|
||||
fs.renameSync(extractedDir, SKILLS_DIR);
|
||||
|
||||
// Clean up temp directory
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
|
||||
// Create default config file if it doesn't exist
|
||||
if (!fs.existsSync(CONFIG_PATH)) {
|
||||
const defaultConfig = {
|
||||
featureFlags: {
|
||||
keepCriticalPriority: true,
|
||||
keepHighPriority: true,
|
||||
keepMediumPriority: true,
|
||||
keepLowPriority: false,
|
||||
removeJsOptimizations: true,
|
||||
removeRenderingOptimizations: true,
|
||||
removeAdvancedPatterns: true
|
||||
},
|
||||
priorities: {
|
||||
keep: ["CRITICAL", "HIGH"],
|
||||
conditionalKeep: ["MEDIUM", "MEDIUM-HIGH"],
|
||||
remove: ["LOW", "LOW-MEDIUM"]
|
||||
},
|
||||
technologyDetection: {},
|
||||
alwaysKeep: [
|
||||
"async-defer-await.md",
|
||||
"async-parallel.md",
|
||||
"async-dependencies.md",
|
||||
"async-api-routes.md",
|
||||
"bundle-barrel-imports.md",
|
||||
"bundle-dynamic-imports.md",
|
||||
"bundle-defer-third-party.md",
|
||||
"bundle-conditional.md",
|
||||
"bundle-preload.md",
|
||||
"rerender-functional-setstate.md",
|
||||
"rerender-memo.md",
|
||||
"rerender-dependencies.md",
|
||||
"rerender-defer-reads.md"
|
||||
],
|
||||
alwaysRemove: [
|
||||
"js-batch-dom-css.md",
|
||||
"js-cache-property-access.md",
|
||||
"js-combine-iterations.md",
|
||||
"js-early-exit.md",
|
||||
"js-hoist-regexp.md",
|
||||
"js-index-maps.md",
|
||||
"js-length-check-first.md",
|
||||
"js-min-max-loop.md",
|
||||
"js-set-map-lookups.md",
|
||||
"js-tosorted-immutable.md",
|
||||
"js-cache-function-results.md",
|
||||
"rendering-activity.md",
|
||||
"rendering-animate-svg-wrapper.md",
|
||||
"rendering-conditional-render.md",
|
||||
"rendering-content-visibility.md",
|
||||
"rendering-hoist-jsx.md",
|
||||
"rendering-hydration-no-flicker.md",
|
||||
"rendering-svg-precision.md",
|
||||
"advanced-event-handler-refs.md",
|
||||
"advanced-use-latest.md"
|
||||
]
|
||||
};
|
||||
fs.writeFileSync(CONFIG_PATH, JSON.stringify(defaultConfig, null, 2));
|
||||
}
|
||||
|
||||
console.log('✓ Skills fetched successfully\n');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to fetch skills:', error);
|
||||
// Clean up on error
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function formatSkills(): void {
|
||||
console.log('🎨 Formatting skill files to match project code style...\n');
|
||||
try {
|
||||
execSync(
|
||||
`prettier --write "${SKILLS_DIR}/**/*.md"`,
|
||||
{ stdio: 'inherit', cwd: PROJECT_ROOT }
|
||||
);
|
||||
console.log('✓ Formatting complete\n');
|
||||
} catch (error) {
|
||||
console.log('⚠️ Formatting failed (non-critical):', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
try {
|
||||
const args = process.argv.slice(2);
|
||||
const shouldFetch = args.includes('--fetch') || !fs.existsSync(SKILLS_DIR);
|
||||
|
||||
// Auto-fetch if skills don't exist
|
||||
if (shouldFetch) {
|
||||
fetchSkills();
|
||||
}
|
||||
|
||||
// Check if skills exist after potential fetch
|
||||
if (!fs.existsSync(SKILLS_DIR)) {
|
||||
console.error('❌ Skills directory not found!\n');
|
||||
console.error('Please run with --fetch flag:');
|
||||
console.error(' pnpm filter-skills --fetch\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const validateOnly = args.includes('--validate-config');
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
if (validateOnly) {
|
||||
validateConfig(config);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('🔍 Detecting technologies used in codebase...\n');
|
||||
const detectedTechnologies = detectTechnologies(config);
|
||||
|
||||
console.log('🎯 Filtering skills...\n');
|
||||
const report = filterRules(config, detectedTechnologies);
|
||||
|
||||
printReport(report);
|
||||
|
||||
if (!isDryRun) {
|
||||
saveReport(report);
|
||||
formatSkills();
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
2673
.agent/skills/react-best-practices/AGENTS.md
Normal file
2673
.agent/skills/react-best-practices/AGENTS.md
Normal file
File diff suppressed because it is too large
Load Diff
127
.agent/skills/react-best-practices/README.md
Normal file
127
.agent/skills/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).
|
||||
127
.agent/skills/react-best-practices/SKILL.md
Normal file
127
.agent/skills/react-best-practices/SKILL.md
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
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 45 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-cache-react` - Use React.cache() for per-request deduplication
|
||||
- `server-cache-lru` - Use LRU cache for cross-request caching
|
||||
- `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
|
||||
|
||||
### 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-dependencies` - Use primitive dependencies in effects
|
||||
- `rerender-derived-state` - Subscribe to derived booleans, not raw values
|
||||
- `rerender-functional-setstate` - Use functional setState for stable callbacks
|
||||
- `rerender-lazy-state-init` - Pass function to useState for expensive values
|
||||
- `rerender-transitions` - Use startTransition for non-urgent updates
|
||||
|
||||
### 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-activity` - Use Activity component for show/hide
|
||||
- `rendering-conditional-render` - Use ternary, not && for conditionals
|
||||
|
||||
### 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-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
|
||||
rules/_sections.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`
|
||||
229
.agent/skills/react-best-practices/filter-report.json
Normal file
229
.agent/skills/react-best-practices/filter-report.json
Normal file
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"kept": [
|
||||
{
|
||||
"file": "_sections.md",
|
||||
"reason": "Default (no matching rule)"
|
||||
},
|
||||
{
|
||||
"file": "_template.md",
|
||||
"reason": "Priority: MEDIUM (conditional keep)"
|
||||
},
|
||||
{
|
||||
"file": "async-api-routes.md",
|
||||
"reason": "Always keep (critical pattern)"
|
||||
},
|
||||
{
|
||||
"file": "async-defer-await.md",
|
||||
"reason": "Always keep (critical pattern)"
|
||||
},
|
||||
{
|
||||
"file": "async-dependencies.md",
|
||||
"reason": "Always keep (critical pattern)"
|
||||
},
|
||||
{
|
||||
"file": "async-parallel.md",
|
||||
"reason": "Always keep (critical pattern)"
|
||||
},
|
||||
{
|
||||
"file": "async-suspense-boundaries.md",
|
||||
"reason": "Priority: HIGH"
|
||||
},
|
||||
{
|
||||
"file": "bundle-barrel-imports.md",
|
||||
"reason": "Always keep (critical pattern)"
|
||||
},
|
||||
{
|
||||
"file": "bundle-conditional.md",
|
||||
"reason": "Always keep (critical pattern)"
|
||||
},
|
||||
{
|
||||
"file": "bundle-defer-third-party.md",
|
||||
"reason": "Always keep (critical pattern)"
|
||||
},
|
||||
{
|
||||
"file": "bundle-dynamic-imports.md",
|
||||
"reason": "Always keep (critical pattern)"
|
||||
},
|
||||
{
|
||||
"file": "bundle-preload.md",
|
||||
"reason": "Always keep (critical pattern)"
|
||||
},
|
||||
{
|
||||
"file": "client-localstorage-schema.md",
|
||||
"reason": "Priority: MEDIUM (conditional keep)"
|
||||
},
|
||||
{
|
||||
"file": "client-passive-event-listeners.md",
|
||||
"reason": "Priority: MEDIUM (conditional keep)"
|
||||
},
|
||||
{
|
||||
"file": "client-swr-dedup.md",
|
||||
"reason": "Priority: MEDIUM-HIGH (conditional keep)"
|
||||
},
|
||||
{
|
||||
"file": "rerender-defer-reads.md",
|
||||
"reason": "Always keep (critical pattern)"
|
||||
},
|
||||
{
|
||||
"file": "rerender-dependencies.md",
|
||||
"reason": "Always keep (critical pattern)"
|
||||
},
|
||||
{
|
||||
"file": "rerender-derived-state.md",
|
||||
"reason": "Priority: MEDIUM (conditional keep)"
|
||||
},
|
||||
{
|
||||
"file": "rerender-functional-setstate.md",
|
||||
"reason": "Always keep (critical pattern)"
|
||||
},
|
||||
{
|
||||
"file": "rerender-lazy-state-init.md",
|
||||
"reason": "Priority: MEDIUM (conditional keep)"
|
||||
},
|
||||
{
|
||||
"file": "rerender-memo-with-default-value.md",
|
||||
"reason": "Priority: MEDIUM (conditional keep)"
|
||||
},
|
||||
{
|
||||
"file": "rerender-memo.md",
|
||||
"reason": "Always keep (critical pattern)"
|
||||
},
|
||||
{
|
||||
"file": "rerender-transitions.md",
|
||||
"reason": "Priority: MEDIUM (conditional keep)"
|
||||
},
|
||||
{
|
||||
"file": "server-after-nonblocking.md",
|
||||
"reason": "Priority: MEDIUM (conditional keep)"
|
||||
},
|
||||
{
|
||||
"file": "server-auth-actions.md",
|
||||
"reason": "Priority: CRITICAL"
|
||||
},
|
||||
{
|
||||
"file": "server-cache-lru.md",
|
||||
"reason": "Priority: HIGH"
|
||||
},
|
||||
{
|
||||
"file": "server-cache-react.md",
|
||||
"reason": "Priority: MEDIUM (conditional keep)"
|
||||
},
|
||||
{
|
||||
"file": "server-parallel-fetching.md",
|
||||
"reason": "Priority: CRITICAL"
|
||||
},
|
||||
{
|
||||
"file": "server-serialization.md",
|
||||
"reason": "Priority: HIGH"
|
||||
}
|
||||
],
|
||||
"archived": [
|
||||
{
|
||||
"file": "advanced-event-handler-refs.md",
|
||||
"reason": "Feature flag: removeAdvancedPatterns"
|
||||
},
|
||||
{
|
||||
"file": "advanced-use-latest.md",
|
||||
"reason": "Feature flag: removeAdvancedPatterns"
|
||||
},
|
||||
{
|
||||
"file": "client-event-listeners.md",
|
||||
"reason": "Priority: LOW"
|
||||
},
|
||||
{
|
||||
"file": "js-batch-dom-css.md",
|
||||
"reason": "Feature flag: removeJsOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "js-cache-function-results.md",
|
||||
"reason": "Feature flag: removeJsOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "js-cache-property-access.md",
|
||||
"reason": "Feature flag: removeJsOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "js-cache-storage.md",
|
||||
"reason": "Feature flag: removeJsOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "js-combine-iterations.md",
|
||||
"reason": "Feature flag: removeJsOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "js-early-exit.md",
|
||||
"reason": "Feature flag: removeJsOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "js-hoist-regexp.md",
|
||||
"reason": "Feature flag: removeJsOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "js-index-maps.md",
|
||||
"reason": "Feature flag: removeJsOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "js-length-check-first.md",
|
||||
"reason": "Feature flag: removeJsOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "js-min-max-loop.md",
|
||||
"reason": "Feature flag: removeJsOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "js-set-map-lookups.md",
|
||||
"reason": "Feature flag: removeJsOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "js-tosorted-immutable.md",
|
||||
"reason": "Feature flag: removeJsOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "rendering-activity.md",
|
||||
"reason": "Feature flag: removeRenderingOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "rendering-animate-svg-wrapper.md",
|
||||
"reason": "Feature flag: removeRenderingOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "rendering-conditional-render.md",
|
||||
"reason": "Feature flag: removeRenderingOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "rendering-content-visibility.md",
|
||||
"reason": "Feature flag: removeRenderingOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "rendering-hoist-jsx.md",
|
||||
"reason": "Feature flag: removeRenderingOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "rendering-hydration-no-flicker.md",
|
||||
"reason": "Feature flag: removeRenderingOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "rendering-svg-precision.md",
|
||||
"reason": "Feature flag: removeRenderingOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "rendering-usetransition-loading.md",
|
||||
"reason": "Feature flag: removeRenderingOptimizations"
|
||||
},
|
||||
{
|
||||
"file": "rerender-simple-expression-in-memo.md",
|
||||
"reason": "Priority: LOW-MEDIUM"
|
||||
},
|
||||
{
|
||||
"file": "server-dedup-props.md",
|
||||
"reason": "Priority: LOW"
|
||||
}
|
||||
],
|
||||
"technologiesDetected": [],
|
||||
"summary": {
|
||||
"totalRules": 54,
|
||||
"keptRules": 29,
|
||||
"archivedRules": 25,
|
||||
"reductionPercent": 46
|
||||
}
|
||||
}
|
||||
15
.agent/skills/react-best-practices/metadata.json
Normal file
15
.agent/skills/react-best-practices/metadata.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"organization": "Vercel Engineering",
|
||||
"date": "January 2026",
|
||||
"abstract": "Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.",
|
||||
"references": [
|
||||
"https://react.dev",
|
||||
"https://nextjs.org",
|
||||
"https://swr.vercel.app",
|
||||
"https://github.com/shuding/better-all",
|
||||
"https://github.com/isaacs/node-lru-cache",
|
||||
"https://vercel.com/blog/how-we-optimized-package-imports-in-next-js",
|
||||
"https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast"
|
||||
]
|
||||
}
|
||||
46
.agent/skills/react-best-practices/rules/_sections.md
Normal file
46
.agent/skills/react-best-practices/rules/_sections.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Sections
|
||||
|
||||
This file defines all sections, their ordering, impact levels, and descriptions.
|
||||
The section ID (in parentheses) is the filename prefix used to group rules.
|
||||
|
||||
---
|
||||
|
||||
## 1. Eliminating Waterfalls (async)
|
||||
|
||||
**Impact:** CRITICAL
|
||||
**Description:** Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
|
||||
|
||||
## 2. Bundle Size Optimization (bundle)
|
||||
|
||||
**Impact:** CRITICAL
|
||||
**Description:** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
|
||||
|
||||
## 3. Server-Side Performance (server)
|
||||
|
||||
**Impact:** HIGH
|
||||
**Description:** Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
|
||||
|
||||
## 4. Client-Side Data Fetching (client)
|
||||
|
||||
**Impact:** MEDIUM-HIGH
|
||||
**Description:** Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
|
||||
|
||||
## 5. Re-render Optimization (rerender)
|
||||
|
||||
**Impact:** MEDIUM
|
||||
**Description:** Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
|
||||
|
||||
## 6. Rendering Performance (rendering)
|
||||
|
||||
**Impact:** MEDIUM
|
||||
**Description:** Optimizing the rendering process reduces the work the browser needs to do.
|
||||
|
||||
## 7. JavaScript Performance (js)
|
||||
|
||||
**Impact:** LOW-MEDIUM
|
||||
**Description:** Micro-optimizations for hot paths can add up to meaningful improvements.
|
||||
|
||||
## 8. Advanced Patterns (advanced)
|
||||
|
||||
**Impact:** LOW
|
||||
**Description:** Advanced patterns for specific cases that require careful implementation.
|
||||
28
.agent/skills/react-best-practices/rules/_template.md
Normal file
28
.agent/skills/react-best-practices/rules/_template.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Rule Title Here
|
||||
impact: MEDIUM
|
||||
impactDescription: Optional description of impact (e.g., "20-50% improvement")
|
||||
tags: tag1, tag2
|
||||
---
|
||||
|
||||
## Rule Title Here
|
||||
|
||||
**Impact: MEDIUM (optional impact description)**
|
||||
|
||||
Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.
|
||||
|
||||
**Incorrect (description of what's wrong):**
|
||||
|
||||
```typescript
|
||||
// Bad code example here
|
||||
const bad = example();
|
||||
```
|
||||
|
||||
**Correct (description of what's right):**
|
||||
|
||||
```typescript
|
||||
// Good code example here
|
||||
const good = example();
|
||||
```
|
||||
|
||||
Reference: [Link to documentation or resource](https://example.com)
|
||||
35
.agent/skills/react-best-practices/rules/async-api-routes.md
Normal file
35
.agent/skills/react-best-practices/rules/async-api-routes.md
Normal file
@@ -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)
|
||||
24
.agent/skills/react-best-practices/rules/async-parallel.md
Normal file
24
.agent/skills/react-best-practices/rules/async-parallel.md
Normal file
@@ -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,62 @@
|
||||
---
|
||||
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
|
||||
// Loads 1,583 modules, takes ~2.8s extra in dev
|
||||
// Runtime cost: 200-800ms on every cold start
|
||||
|
||||
import { Button, TextField } from "@mui/material";
|
||||
import { Check, Menu, X } from "lucide-react";
|
||||
|
||||
// Loads 2,225 modules, takes ~4.2s extra in dev
|
||||
```
|
||||
|
||||
**Correct (imports only what you need):**
|
||||
|
||||
```tsx
|
||||
// Loads only 3 modules (~2KB vs ~1MB)
|
||||
|
||||
import Button from "@mui/material/Button";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Check from "lucide-react/dist/esm/icons/check";
|
||||
import Menu from "lucide-react/dist/esm/icons/menu";
|
||||
import X from "lucide-react/dist/esm/icons/x";
|
||||
|
||||
// Loads only what you use
|
||||
```
|
||||
|
||||
**Alternative (Next.js 13.5+):**
|
||||
|
||||
```js
|
||||
// Then you can keep the ergonomic barrel imports:
|
||||
import { Check, Menu, X } from "lucide-react";
|
||||
|
||||
// next.config.js - use optimizePackageImports
|
||||
module.exports = {
|
||||
experimental: {
|
||||
optimizePackageImports: ["lucide-react", "@mui/material"],
|
||||
},
|
||||
};
|
||||
|
||||
// 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,35 @@
|
||||
---
|
||||
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,46 @@
|
||||
---
|
||||
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,32 @@
|
||||
---
|
||||
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} />;
|
||||
}
|
||||
```
|
||||
44
.agent/skills/react-best-practices/rules/bundle-preload.md
Normal file
44
.agent/skills/react-best-practices/rules/bundle-preload.md
Normal file
@@ -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,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()`.
|
||||
56
.agent/skills/react-best-practices/rules/client-swr-dedup.md
Normal file
56
.agent/skills/react-best-practices/rules/client-swr-dedup.md
Normal file
@@ -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,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,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 />
|
||||
```
|
||||
44
.agent/skills/react-best-practices/rules/rerender-memo.md
Normal file
44
.agent/skills/react-best-practices/rules/rerender-memo.md
Normal file
@@ -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,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 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 { cookies, headers } from "next/headers";
|
||||
import { after } from "next/server";
|
||||
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 { z } from "zod";
|
||||
import { verifySession } from "@/lib/auth";
|
||||
|
||||
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)
|
||||
41
.agent/skills/react-best-practices/rules/server-cache-lru.md
Normal file
41
.agent/skills/react-best-practices/rules/server-cache-lru.md
Normal file
@@ -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,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>;
|
||||
}
|
||||
```
|
||||
63
.agent/skills/react-best-practices/skill-filter-config.json
Normal file
63
.agent/skills/react-best-practices/skill-filter-config.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"featureFlags": {
|
||||
"keepCriticalPriority": true,
|
||||
"keepHighPriority": true,
|
||||
"keepMediumPriority": true,
|
||||
"keepLowPriority": false,
|
||||
"removeJsOptimizations": true,
|
||||
"removeRenderingOptimizations": true,
|
||||
"removeAdvancedPatterns": true
|
||||
},
|
||||
"priorities": {
|
||||
"keep": [
|
||||
"CRITICAL",
|
||||
"HIGH"
|
||||
],
|
||||
"conditionalKeep": [
|
||||
"MEDIUM",
|
||||
"MEDIUM-HIGH"
|
||||
],
|
||||
"remove": [
|
||||
"LOW",
|
||||
"LOW-MEDIUM"
|
||||
]
|
||||
},
|
||||
"technologyDetection": {},
|
||||
"alwaysKeep": [
|
||||
"async-defer-await.md",
|
||||
"async-parallel.md",
|
||||
"async-dependencies.md",
|
||||
"async-api-routes.md",
|
||||
"bundle-barrel-imports.md",
|
||||
"bundle-dynamic-imports.md",
|
||||
"bundle-defer-third-party.md",
|
||||
"bundle-conditional.md",
|
||||
"bundle-preload.md",
|
||||
"rerender-functional-setstate.md",
|
||||
"rerender-memo.md",
|
||||
"rerender-dependencies.md",
|
||||
"rerender-defer-reads.md"
|
||||
],
|
||||
"alwaysRemove": [
|
||||
"js-batch-dom-css.md",
|
||||
"js-cache-property-access.md",
|
||||
"js-combine-iterations.md",
|
||||
"js-early-exit.md",
|
||||
"js-hoist-regexp.md",
|
||||
"js-index-maps.md",
|
||||
"js-length-check-first.md",
|
||||
"js-min-max-loop.md",
|
||||
"js-set-map-lookups.md",
|
||||
"js-tosorted-immutable.md",
|
||||
"js-cache-function-results.md",
|
||||
"rendering-activity.md",
|
||||
"rendering-animate-svg-wrapper.md",
|
||||
"rendering-conditional-render.md",
|
||||
"rendering-content-visibility.md",
|
||||
"rendering-hoist-jsx.md",
|
||||
"rendering-hydration-no-flicker.md",
|
||||
"rendering-svg-precision.md",
|
||||
"advanced-event-handler-refs.md",
|
||||
"advanced-use-latest.md"
|
||||
]
|
||||
}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -63,3 +63,8 @@ packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.xcworkspace/xcuserdat
|
||||
.cursorrules
|
||||
i18n.cache
|
||||
stats.html
|
||||
|
||||
# Agent skill archives
|
||||
.agent/skills/**/.archived/
|
||||
.agent/.temp-skills/
|
||||
|
||||
|
||||
@@ -41,7 +41,9 @@
|
||||
"generate-translations": "pnpm i18n:web:generate && pnpm i18n:surveys:generate",
|
||||
"scan-translations": "pnpm --filter @formbricks/i18n-utils scan-translations",
|
||||
"i18n": "pnpm generate-translations && pnpm scan-translations",
|
||||
"i18n:validate": "pnpm scan-translations"
|
||||
"i18n:validate": "pnpm scan-translations",
|
||||
"filter-skills": "tsx .agent/scripts/filter-skills.ts",
|
||||
"filter-skills:dry-run": "tsx .agent/scripts/filter-skills.ts --dry-run"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "19.2.3",
|
||||
|
||||
Reference in New Issue
Block a user