Openstatus www.openstatus.dev

chore: more skills (#1809)

* chore: add vercel skills

* chore: add hono skill

* chore: more skills

* chore: copywriting skills

authored by

Maximilian Kaske and committed by
GitHub
bb7cd57f 2b342339

+10271
+251
.agents/skills/copywriting/SKILL.md
··· 1 + --- 2 + name: copywriting 3 + version: 1.0.0 4 + description: When the user wants to write, rewrite, or improve marketing copy for any page — including homepage, landing pages, pricing pages, feature pages, about pages, or product pages. Also use when the user says "write copy for," "improve this copy," "rewrite this page," "marketing copy," "headline help," or "CTA copy." For email copy, see email-sequence. For popup copy, see popup-cro. 5 + --- 6 + 7 + # Copywriting 8 + 9 + You are an expert conversion copywriter. Your goal is to write marketing copy that is clear, compelling, and drives action. 10 + 11 + ## Before Writing 12 + 13 + **Check for product marketing context first:** 14 + If `.claude/product-marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered or specific to this task. 15 + 16 + Gather this context (ask if not provided): 17 + 18 + ### 1. Page Purpose 19 + - What type of page? (homepage, landing page, pricing, feature, about) 20 + - What is the ONE primary action you want visitors to take? 21 + 22 + ### 2. Audience 23 + - Who is the ideal customer? 24 + - What problem are they trying to solve? 25 + - What objections or hesitations do they have? 26 + - What language do they use to describe their problem? 27 + 28 + ### 3. Product/Offer 29 + - What are you selling or offering? 30 + - What makes it different from alternatives? 31 + - What's the key transformation or outcome? 32 + - Any proof points (numbers, testimonials, case studies)? 33 + 34 + ### 4. Context 35 + - Where is traffic coming from? (ads, organic, email) 36 + - What do visitors already know before arriving? 37 + 38 + --- 39 + 40 + ## Copywriting Principles 41 + 42 + ### Clarity Over Cleverness 43 + If you have to choose between clear and creative, choose clear. 44 + 45 + ### Benefits Over Features 46 + Features: What it does. Benefits: What that means for the customer. 47 + 48 + ### Specificity Over Vagueness 49 + - Vague: "Save time on your workflow" 50 + - Specific: "Cut your weekly reporting from 4 hours to 15 minutes" 51 + 52 + ### Customer Language Over Company Language 53 + Use words your customers use. Mirror voice-of-customer from reviews, interviews, support tickets. 54 + 55 + ### One Idea Per Section 56 + Each section should advance one argument. Build a logical flow down the page. 57 + 58 + --- 59 + 60 + ## Writing Style Rules 61 + 62 + ### Core Principles 63 + 64 + 1. **Simple over complex** — "Use" not "utilize," "help" not "facilitate" 65 + 2. **Specific over vague** — Avoid "streamline," "optimize," "innovative" 66 + 3. **Active over passive** — "We generate reports" not "Reports are generated" 67 + 4. **Confident over qualified** — Remove "almost," "very," "really" 68 + 5. **Show over tell** — Describe the outcome instead of using adverbs 69 + 6. **Honest over sensational** — Never fabricate statistics or testimonials 70 + 71 + ### Quick Quality Check 72 + 73 + - Jargon that could confuse outsiders? 74 + - Sentences trying to do too much? 75 + - Passive voice constructions? 76 + - Exclamation points? (remove them) 77 + - Marketing buzzwords without substance? 78 + 79 + For thorough line-by-line review, use the **copy-editing** skill after your draft. 80 + 81 + --- 82 + 83 + ## Best Practices 84 + 85 + ### Be Direct 86 + Get to the point. Don't bury the value in qualifications. 87 + 88 + ❌ Slack lets you share files instantly, from documents to images, directly in your conversations 89 + 90 + ✅ Need to share a screenshot? Send as many documents, images, and audio files as your heart desires. 91 + 92 + ### Use Rhetorical Questions 93 + Questions engage readers and make them think about their own situation. 94 + - "Hate returning stuff to Amazon?" 95 + - "Tired of chasing approvals?" 96 + 97 + ### Use Analogies When Helpful 98 + Analogies make abstract concepts concrete and memorable. 99 + 100 + ### Pepper in Humor (When Appropriate) 101 + Puns and wit make copy memorable—but only if it fits the brand and doesn't undermine clarity. 102 + 103 + --- 104 + 105 + ## Page Structure Framework 106 + 107 + ### Above the Fold 108 + 109 + **Headline** 110 + - Your single most important message 111 + - Communicate core value proposition 112 + - Specific > generic 113 + 114 + **Example formulas:** 115 + - "{Achieve outcome} without {pain point}" 116 + - "The {category} for {audience}" 117 + - "Never {unpleasant event} again" 118 + - "{Question highlighting main pain point}" 119 + 120 + **For comprehensive headline formulas**: See [references/copy-frameworks.md](references/copy-frameworks.md) 121 + 122 + **For natural transition phrases**: See [references/natural-transitions.md](references/natural-transitions.md) 123 + 124 + **Subheadline** 125 + - Expands on headline 126 + - Adds specificity 127 + - 1-2 sentences max 128 + 129 + **Primary CTA** 130 + - Action-oriented button text 131 + - Communicate what they get: "Start Free Trial" > "Sign Up" 132 + 133 + ### Core Sections 134 + 135 + | Section | Purpose | 136 + |---------|---------| 137 + | Social Proof | Build credibility (logos, stats, testimonials) | 138 + | Problem/Pain | Show you understand their situation | 139 + | Solution/Benefits | Connect to outcomes (3-5 key benefits) | 140 + | How It Works | Reduce perceived complexity (3-4 steps) | 141 + | Objection Handling | FAQ, comparisons, guarantees | 142 + | Final CTA | Recap value, repeat CTA, risk reversal | 143 + 144 + **For detailed section types and page templates**: See [references/copy-frameworks.md](references/copy-frameworks.md) 145 + 146 + --- 147 + 148 + ## CTA Copy Guidelines 149 + 150 + **Weak CTAs (avoid):** 151 + - Submit, Sign Up, Learn More, Click Here, Get Started 152 + 153 + **Strong CTAs (use):** 154 + - Start Free Trial 155 + - Get [Specific Thing] 156 + - See [Product] in Action 157 + - Create Your First [Thing] 158 + - Download the Guide 159 + 160 + **Formula:** [Action Verb] + [What They Get] + [Qualifier if needed] 161 + 162 + Examples: 163 + - "Start My Free Trial" 164 + - "Get the Complete Checklist" 165 + - "See Pricing for My Team" 166 + 167 + --- 168 + 169 + ## Page-Specific Guidance 170 + 171 + ### Homepage 172 + - Serve multiple audiences without being generic 173 + - Lead with broadest value proposition 174 + - Provide clear paths for different visitor intents 175 + 176 + ### Landing Page 177 + - Single message, single CTA 178 + - Match headline to ad/traffic source 179 + - Complete argument on one page 180 + 181 + ### Pricing Page 182 + - Help visitors choose the right plan 183 + - Address "which is right for me?" anxiety 184 + - Make recommended plan obvious 185 + 186 + ### Feature Page 187 + - Connect feature → benefit → outcome 188 + - Show use cases and examples 189 + - Clear path to try or buy 190 + 191 + ### About Page 192 + - Tell the story of why you exist 193 + - Connect mission to customer benefit 194 + - Still include a CTA 195 + 196 + --- 197 + 198 + ## Voice and Tone 199 + 200 + Before writing, establish: 201 + 202 + **Formality level:** 203 + - Casual/conversational 204 + - Professional but friendly 205 + - Formal/enterprise 206 + 207 + **Brand personality:** 208 + - Playful or serious? 209 + - Bold or understated? 210 + - Technical or accessible? 211 + 212 + Maintain consistency, but adjust intensity: 213 + - Headlines can be bolder 214 + - Body copy should be clearer 215 + - CTAs should be action-oriented 216 + 217 + --- 218 + 219 + ## Output Format 220 + 221 + When writing copy, provide: 222 + 223 + ### Page Copy 224 + Organized by section: 225 + - Headline, Subheadline, CTA 226 + - Section headers and body copy 227 + - Secondary CTAs 228 + 229 + ### Annotations 230 + For key elements, explain: 231 + - Why you made this choice 232 + - What principle it applies 233 + 234 + ### Alternatives 235 + For headlines and CTAs, provide 2-3 options: 236 + - Option A: [copy] — [rationale] 237 + - Option B: [copy] — [rationale] 238 + 239 + ### Meta Content (if relevant) 240 + - Page title (for SEO) 241 + - Meta description 242 + 243 + --- 244 + 245 + ## Related Skills 246 + 247 + - **copy-editing**: For polishing existing copy (use after your draft) 248 + - **page-cro**: If page structure/strategy needs work, not just copy 249 + - **email-sequence**: For email copywriting 250 + - **popup-cro**: For popup and modal copy 251 + - **ab-test-setup**: To test copy variations
+338
.agents/skills/copywriting/references/copy-frameworks.md
··· 1 + # Copy Frameworks Reference 2 + 3 + Headline formulas, page section types, and structural templates. 4 + 5 + ## Headline Formulas 6 + 7 + ### Outcome-Focused 8 + 9 + **{Achieve desirable outcome} without {pain point}** 10 + > Understand how users are really experiencing your site without drowning in numbers 11 + 12 + **{Achieve desirable outcome} by {how product makes it possible}** 13 + > Generate more leads by seeing which companies visit your site 14 + 15 + **Turn {input} into {outcome}** 16 + > Turn your hard-earned sales into repeat customers 17 + 18 + **[Achieve outcome] in [timeframe]** 19 + > Get your tax refund in 10 days 20 + 21 + --- 22 + 23 + ### Problem-Focused 24 + 25 + **Never {unpleasant event} again** 26 + > Never miss a sales opportunity again 27 + 28 + **{Question highlighting the main pain point}** 29 + > Hate returning stuff to Amazon? 30 + 31 + **Stop [pain]. Start [pleasure].** 32 + > Stop chasing invoices. Start getting paid on time. 33 + 34 + --- 35 + 36 + ### Audience-Focused 37 + 38 + **{Key feature/product type} for {target audience}** 39 + > Advanced analytics for Shopify e-commerce 40 + 41 + **{Key feature/product type} for {target audience} to {what it's used for}** 42 + > An online whiteboard for teams to ideate and brainstorm together 43 + 44 + **You don't have to {skills or resources} to {achieve desirable outcome}** 45 + > With Ahrefs, you don't have to be an SEO pro to rank higher and get more traffic 46 + 47 + --- 48 + 49 + ### Differentiation-Focused 50 + 51 + **The {opposite of usual process} way to {achieve desirable outcome}** 52 + > The easiest way to turn your passion into income 53 + 54 + **The [category] that [key differentiator]** 55 + > The CRM that updates itself 56 + 57 + --- 58 + 59 + ### Proof-Focused 60 + 61 + **[Number] [people] use [product] to [outcome]** 62 + > 50,000 marketers use Drip to send better emails 63 + 64 + **{Key benefit of your product}** 65 + > Sound clear in online meetings 66 + 67 + --- 68 + 69 + ### Additional Formulas 70 + 71 + **The simple way to {outcome}** 72 + > The simple way to track your time 73 + 74 + **Finally, {category} that {benefit}** 75 + > Finally, accounting software that doesn't suck 76 + 77 + **{Outcome} without {common pain}** 78 + > Build your website without writing code 79 + 80 + **Get {benefit} from your {thing}** 81 + > Get more revenue from your existing traffic 82 + 83 + **{Action verb} your {thing} like {admirable example}** 84 + > Market your SaaS like a Fortune 500 85 + 86 + **What if you could {desirable outcome}?** 87 + > What if you could close deals 30% faster? 88 + 89 + **Everything you need to {outcome}** 90 + > Everything you need to launch your course 91 + 92 + **The {adjective} {category} built for {audience}** 93 + > The lightweight CRM built for startups 94 + 95 + --- 96 + 97 + ## Landing Page Section Types 98 + 99 + ### Core Sections 100 + 101 + **Hero (Above the Fold)** 102 + - Headline + subheadline 103 + - Primary CTA 104 + - Supporting visual (product screenshot, hero image) 105 + - Optional: Social proof bar 106 + 107 + **Social Proof Bar** 108 + - Customer logos (recognizable > many) 109 + - Key metric ("10,000+ teams") 110 + - Star rating with review count 111 + - Short testimonial snippet 112 + 113 + **Problem/Pain Section** 114 + - Articulate their problem better than they can 115 + - Create recognition ("that's exactly my situation") 116 + - Hint at cost of not solving it 117 + 118 + **Solution/Benefits Section** 119 + - Bridge from problem to your solution 120 + - 3-5 key benefits (not 10) 121 + - Each: headline + explanation + proof if available 122 + 123 + **How It Works** 124 + - 3-4 numbered steps 125 + - Reduces perceived complexity 126 + - Each step: action + outcome 127 + 128 + **Final CTA Section** 129 + - Recap value proposition 130 + - Repeat primary CTA 131 + - Risk reversal (guarantee, free trial) 132 + 133 + --- 134 + 135 + ### Supporting Sections 136 + 137 + **Testimonials** 138 + - Full quotes with names, roles, companies 139 + - Photos when possible 140 + - Specific results over vague praise 141 + - Formats: quote cards, video, tweet embeds 142 + 143 + **Case Studies** 144 + - Problem → Solution → Results 145 + - Specific metrics and outcomes 146 + - Customer name and context 147 + - Can be snippets with "Read more" links 148 + 149 + **Use Cases** 150 + - Different ways product is used 151 + - Helps visitors self-identify 152 + - "For marketers who need X" format 153 + 154 + **Personas / "Built For" Sections** 155 + - Explicitly call out target audience 156 + - "Perfect for [role]" blocks 157 + - Addresses "Is this for me?" question 158 + 159 + **FAQ Section** 160 + - Address common objections 161 + - Good for SEO 162 + - Reduces support burden 163 + - 5-10 most common questions 164 + 165 + **Comparison Section** 166 + - vs. competitors (name them or don't) 167 + - vs. status quo (spreadsheets, manual processes) 168 + - Tables or side-by-side format 169 + 170 + **Integrations / Partners** 171 + - Logos of tools you connect with 172 + - "Works with your stack" messaging 173 + - Builds credibility 174 + 175 + **Founder Story / Manifesto** 176 + - Why you built this 177 + - What you believe 178 + - Emotional connection 179 + - Differentiates from faceless competitors 180 + 181 + **Demo / Product Tour** 182 + - Interactive demos 183 + - Video walkthroughs 184 + - GIF previews 185 + - Shows product in action 186 + 187 + **Pricing Preview** 188 + - Teaser even on non-pricing pages 189 + - Starting price or "from $X/mo" 190 + - Moves decision-makers forward 191 + 192 + **Guarantee / Risk Reversal** 193 + - Money-back guarantee 194 + - Free trial terms 195 + - "Cancel anytime" 196 + - Reduces friction 197 + 198 + **Stats Section** 199 + - Key metrics that build credibility 200 + - "10,000+ customers" 201 + - "4.9/5 rating" 202 + - "$2M saved for customers" 203 + 204 + --- 205 + 206 + ## Page Structure Templates 207 + 208 + ### Feature-Heavy Page (Weak) 209 + 210 + ``` 211 + 1. Hero 212 + 2. Feature 1 213 + 3. Feature 2 214 + 4. Feature 3 215 + 5. Feature 4 216 + 6. CTA 217 + ``` 218 + 219 + This is a list, not a persuasive narrative. 220 + 221 + --- 222 + 223 + ### Varied, Engaging Page (Strong) 224 + 225 + ``` 226 + 1. Hero with clear value prop 227 + 2. Social proof bar (logos or stats) 228 + 3. Problem/pain section 229 + 4. How it works (3 steps) 230 + 5. Key benefits (2-3, not 10) 231 + 6. Testimonial 232 + 7. Use cases or personas 233 + 8. Comparison to alternatives 234 + 9. Case study snippet 235 + 10. FAQ 236 + 11. Final CTA with guarantee 237 + ``` 238 + 239 + This tells a story and addresses objections. 240 + 241 + --- 242 + 243 + ### Compact Landing Page 244 + 245 + ``` 246 + 1. Hero (headline, subhead, CTA, image) 247 + 2. Social proof bar 248 + 3. 3 key benefits with icons 249 + 4. Testimonial 250 + 5. How it works (3 steps) 251 + 6. Final CTA with guarantee 252 + ``` 253 + 254 + Good for ad landing pages where brevity matters. 255 + 256 + --- 257 + 258 + ### Enterprise/B2B Landing Page 259 + 260 + ``` 261 + 1. Hero (outcome-focused headline) 262 + 2. Logo bar (recognizable companies) 263 + 3. Problem section (business pain) 264 + 4. Solution overview 265 + 5. Use cases by role/department 266 + 6. Security/compliance section 267 + 7. Integration logos 268 + 8. Case study with metrics 269 + 9. ROI/value section 270 + 10. Contact/demo CTA 271 + ``` 272 + 273 + Addresses enterprise buyer concerns. 274 + 275 + --- 276 + 277 + ### Product Launch Page 278 + 279 + ``` 280 + 1. Hero with launch announcement 281 + 2. Video demo or walkthrough 282 + 3. Feature highlights (3-5) 283 + 4. Before/after comparison 284 + 5. Early testimonials 285 + 6. Launch pricing or early access offer 286 + 7. CTA with urgency 287 + ``` 288 + 289 + Good for ProductHunt, launches, or announcements. 290 + 291 + --- 292 + 293 + ## Section Writing Tips 294 + 295 + ### Problem Section 296 + 297 + Start with phrases like: 298 + - "You know the feeling..." 299 + - "If you're like most [role]..." 300 + - "Every day, [audience] struggles with..." 301 + - "We've all been there..." 302 + 303 + Then describe: 304 + - The specific frustration 305 + - The time/money wasted 306 + - The impact on their work/life 307 + 308 + ### Benefits Section 309 + 310 + For each benefit, include: 311 + - **Headline**: The outcome they get 312 + - **Body**: How it works (1-2 sentences) 313 + - **Proof**: Number, testimonial, or example (optional) 314 + 315 + ### How It Works Section 316 + 317 + Each step should be: 318 + - **Numbered**: Creates sense of progress 319 + - **Simple verb**: "Connect," "Set up," "Get" 320 + - **Outcome-oriented**: What they get from this step 321 + 322 + Example: 323 + 1. Connect your tools (takes 2 minutes) 324 + 2. Set your preferences 325 + 3. Get automated reports every Monday 326 + 327 + ### Testimonial Selection 328 + 329 + Best testimonials include: 330 + - Specific results ("increased conversions by 32%") 331 + - Before/after context ("We used to spend hours...") 332 + - Role + company for credibility 333 + - Something quotable and specific 334 + 335 + Avoid testimonials that just say: 336 + - "Great product!" 337 + - "Love it!" 338 + - "Easy to use!"
+252
.agents/skills/copywriting/references/natural-transitions.md
··· 1 + # Natural Transitions 2 + 3 + Transitional phrases to guide readers through your content. Good signposting improves readability, user engagement, and helps search engines understand content structure. 4 + 5 + Adapted from: University of Manchester Academic Phrasebank (2023), Plain English Campaign, web content best practices 6 + 7 + --- 8 + 9 + ## Previewing Content Structure 10 + 11 + Use to orient readers and set expectations: 12 + 13 + - Here's what we'll cover... 14 + - This guide walks you through... 15 + - Below, you'll find... 16 + - We'll start with X, then move to Y... 17 + - First, let's look at... 18 + - Let's break this down step by step. 19 + - The sections below explain... 20 + 21 + --- 22 + 23 + ## Introducing a New Topic 24 + 25 + - When it comes to X,... 26 + - Regarding X,... 27 + - Speaking of X,... 28 + - Now let's talk about X. 29 + - Another key factor is... 30 + - X is worth exploring because... 31 + 32 + --- 33 + 34 + ## Referring Back 35 + 36 + Use to connect ideas and reinforce key points: 37 + 38 + - As mentioned earlier,... 39 + - As we covered above,... 40 + - Remember when we discussed X? 41 + - Building on that point,... 42 + - Going back to X,... 43 + - Earlier, we explained that... 44 + 45 + --- 46 + 47 + ## Moving Between Sections 48 + 49 + - Now let's look at... 50 + - Next up:... 51 + - Moving on to... 52 + - With that covered, let's turn to... 53 + - Now that you understand X, here's Y. 54 + - That brings us to... 55 + 56 + --- 57 + 58 + ## Indicating Addition 59 + 60 + - Also,... 61 + - Plus,... 62 + - On top of that,... 63 + - What's more,... 64 + - Another benefit is... 65 + - Beyond that,... 66 + - In addition,... 67 + - There's also... 68 + 69 + **Note:** Use "moreover" and "furthermore" sparingly. They can sound AI-generated when overused. 70 + 71 + --- 72 + 73 + ## Indicating Contrast 74 + 75 + - However,... 76 + - But,... 77 + - That said,... 78 + - On the flip side,... 79 + - In contrast,... 80 + - Unlike X, Y... 81 + - While X is true, Y... 82 + - Despite this,... 83 + 84 + --- 85 + 86 + ## Indicating Similarity 87 + 88 + - Similarly,... 89 + - Likewise,... 90 + - In the same way,... 91 + - Just like X, Y also... 92 + - This mirrors... 93 + - The same applies to... 94 + 95 + --- 96 + 97 + ## Indicating Cause and Effect 98 + 99 + - So,... 100 + - This means... 101 + - As a result,... 102 + - That's why... 103 + - Because of this,... 104 + - This leads to... 105 + - The outcome?... 106 + - Here's what happens:... 107 + 108 + --- 109 + 110 + ## Giving Examples 111 + 112 + - For example,... 113 + - For instance,... 114 + - Here's an example:... 115 + - Take X, for instance. 116 + - Consider this:... 117 + - A good example is... 118 + - To illustrate,... 119 + - Like when... 120 + - Say you want to... 121 + 122 + --- 123 + 124 + ## Emphasising Key Points 125 + 126 + - Here's the key takeaway:... 127 + - The important thing is... 128 + - What matters most is... 129 + - Don't miss this:... 130 + - Pay attention to... 131 + - This is critical:... 132 + - The bottom line?... 133 + 134 + --- 135 + 136 + ## Providing Evidence 137 + 138 + Use when citing sources, data, or expert opinions: 139 + 140 + ### Neutral attribution 141 + - According to [Source],... 142 + - [Source] reports that... 143 + - Research shows that... 144 + - Data from [Source] indicates... 145 + - A study by [Source] found... 146 + 147 + ### Expert quotes 148 + - As [Expert] puts it,... 149 + - [Expert] explains,... 150 + - In the words of [Expert],... 151 + - [Expert] notes that... 152 + 153 + ### Supporting claims 154 + - This is backed by... 155 + - Evidence suggests... 156 + - The numbers confirm... 157 + - This aligns with findings from... 158 + 159 + --- 160 + 161 + ## Summarising Sections 162 + 163 + - To recap,... 164 + - Here's the short version:... 165 + - In short,... 166 + - The takeaway?... 167 + - So what does this mean?... 168 + - Let's pull this together:... 169 + - Quick summary:... 170 + 171 + --- 172 + 173 + ## Concluding Content 174 + 175 + - Wrapping up,... 176 + - The bottom line is... 177 + - Here's what to do next:... 178 + - To sum up,... 179 + - Final thoughts:... 180 + - Ready to get started?... 181 + - Now it's your turn. 182 + 183 + **Note:** Avoid "In conclusion" at the start of a paragraph. It's overused and signals AI writing. 184 + 185 + --- 186 + 187 + ## Question-Based Transitions 188 + 189 + Useful for conversational tone and featured snippet optimization: 190 + 191 + - So what does this mean for you? 192 + - But why does this matter? 193 + - How do you actually do this? 194 + - What's the catch? 195 + - Sound complicated? It's not. 196 + - Wondering where to start? 197 + - Still not sure? Here's the breakdown. 198 + 199 + --- 200 + 201 + ## List Introductions 202 + 203 + For numbered lists and step-by-step content: 204 + 205 + - Here's how to do it: 206 + - Follow these steps: 207 + - The process is straightforward: 208 + - Here's what you need to know: 209 + - Key things to consider: 210 + - The main factors are: 211 + 212 + --- 213 + 214 + ## Hedging Language 215 + 216 + For claims that need qualification or aren't absolute: 217 + 218 + - may, might, could 219 + - tends to, generally 220 + - often, usually, typically 221 + - in most cases 222 + - it appears that 223 + - evidence suggests 224 + - this can help 225 + - many experts believe 226 + 227 + --- 228 + 229 + ## Best Practice Guidelines 230 + 231 + 1. **Match tone to audience**: B2B content can be slightly more formal; B2C often benefits from conversational transitions 232 + 2. **Vary your transitions**: Repeating the same phrase gets noticed (and not in a good way) 233 + 3. **Don't over-signpost**: Trust your reader; every sentence doesn't need a transition 234 + 4. **Use for scannability**: Transitions at paragraph starts help skimmers navigate 235 + 5. **Keep it natural**: Read aloud; if it sounds forced, simplify 236 + 6. **Front-load key info**: Put the important word or phrase early in the transition 237 + 238 + --- 239 + 240 + ## Transitions to Avoid (AI Tells) 241 + 242 + These phrases are overused in AI-generated content: 243 + 244 + - "That being said,..." 245 + - "It's worth noting that..." 246 + - "At its core,..." 247 + - "In today's digital landscape,..." 248 + - "When it comes to the realm of..." 249 + - "This begs the question..." 250 + - "Let's delve into..." 251 + 252 + See the seo-audit skill's `references/ai-writing-detection.md` for a complete list of AI writing tells.
+133
.agents/skills/find-skills/SKILL.md
··· 1 + --- 2 + name: find-skills 3 + description: Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill. 4 + --- 5 + 6 + # Find Skills 7 + 8 + This skill helps you discover and install skills from the open agent skills ecosystem. 9 + 10 + ## When to Use This Skill 11 + 12 + Use this skill when the user: 13 + 14 + - Asks "how do I do X" where X might be a common task with an existing skill 15 + - Says "find a skill for X" or "is there a skill for X" 16 + - Asks "can you do X" where X is a specialized capability 17 + - Expresses interest in extending agent capabilities 18 + - Wants to search for tools, templates, or workflows 19 + - Mentions they wish they had help with a specific domain (design, testing, deployment, etc.) 20 + 21 + ## What is the Skills CLI? 22 + 23 + The Skills CLI (`npx skills`) is the package manager for the open agent skills ecosystem. Skills are modular packages that extend agent capabilities with specialized knowledge, workflows, and tools. 24 + 25 + **Key commands:** 26 + 27 + - `npx skills find [query]` - Search for skills interactively or by keyword 28 + - `npx skills add <package>` - Install a skill from GitHub or other sources 29 + - `npx skills check` - Check for skill updates 30 + - `npx skills update` - Update all installed skills 31 + 32 + **Browse skills at:** https://skills.sh/ 33 + 34 + ## How to Help Users Find Skills 35 + 36 + ### Step 1: Understand What They Need 37 + 38 + When a user asks for help with something, identify: 39 + 40 + 1. The domain (e.g., React, testing, design, deployment) 41 + 2. The specific task (e.g., writing tests, creating animations, reviewing PRs) 42 + 3. Whether this is a common enough task that a skill likely exists 43 + 44 + ### Step 2: Search for Skills 45 + 46 + Run the find command with a relevant query: 47 + 48 + ```bash 49 + npx skills find [query] 50 + ``` 51 + 52 + For example: 53 + 54 + - User asks "how do I make my React app faster?" → `npx skills find react performance` 55 + - User asks "can you help me with PR reviews?" → `npx skills find pr review` 56 + - User asks "I need to create a changelog" → `npx skills find changelog` 57 + 58 + The command will return results like: 59 + 60 + ``` 61 + Install with npx skills add <owner/repo@skill> 62 + 63 + vercel-labs/agent-skills@vercel-react-best-practices 64 + └ https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices 65 + ``` 66 + 67 + ### Step 3: Present Options to the User 68 + 69 + When you find relevant skills, present them to the user with: 70 + 71 + 1. The skill name and what it does 72 + 2. The install command they can run 73 + 3. A link to learn more at skills.sh 74 + 75 + Example response: 76 + 77 + ``` 78 + I found a skill that might help! The "vercel-react-best-practices" skill provides 79 + React and Next.js performance optimization guidelines from Vercel Engineering. 80 + 81 + To install it: 82 + npx skills add vercel-labs/agent-skills@vercel-react-best-practices 83 + 84 + Learn more: https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices 85 + ``` 86 + 87 + ### Step 4: Offer to Install 88 + 89 + If the user wants to proceed, you can install the skill for them: 90 + 91 + ```bash 92 + npx skills add <owner/repo@skill> -g -y 93 + ``` 94 + 95 + The `-g` flag installs globally (user-level) and `-y` skips confirmation prompts. 96 + 97 + ## Common Skill Categories 98 + 99 + When searching, consider these common categories: 100 + 101 + | Category | Example Queries | 102 + | --------------- | ---------------------------------------- | 103 + | Web Development | react, nextjs, typescript, css, tailwind | 104 + | Testing | testing, jest, playwright, e2e | 105 + | DevOps | deploy, docker, kubernetes, ci-cd | 106 + | Documentation | docs, readme, changelog, api-docs | 107 + | Code Quality | review, lint, refactor, best-practices | 108 + | Design | ui, ux, design-system, accessibility | 109 + | Productivity | workflow, automation, git | 110 + 111 + ## Tips for Effective Searches 112 + 113 + 1. **Use specific keywords**: "react testing" is better than just "testing" 114 + 2. **Try alternative terms**: If "deploy" doesn't work, try "deployment" or "ci-cd" 115 + 3. **Check popular sources**: Many skills come from `vercel-labs/agent-skills` or `ComposioHQ/awesome-claude-skills` 116 + 117 + ## When No Skills Are Found 118 + 119 + If no relevant skills exist: 120 + 121 + 1. Acknowledge that no existing skill was found 122 + 2. Offer to help with the task directly using your general capabilities 123 + 3. Suggest the user could create their own skill with `npx skills init` 124 + 125 + Example: 126 + 127 + ``` 128 + I searched for skills related to "xyz" but didn't find any matches. 129 + I can still help you with this task directly! Would you like me to proceed? 130 + 131 + If this is something you do often, you could create your own skill: 132 + npx skills init my-xyz-skill 133 + ```
+90
.agents/skills/hono/SKILL.md
··· 1 + --- 2 + name: hono 3 + description: Efficiently develop Hono applications using Hono CLI. Supports documentation search, API reference lookup, request testing, and bundle optimization. 4 + --- 5 + 6 + # Hono Skill 7 + 8 + Develop Hono applications efficiently using Hono CLI (`@hono/cli`). 9 + 10 + ## Setup 11 + 12 + You can use Hono CLI without global installation via npx: 13 + 14 + ```bash 15 + npx @hono/cli <command> 16 + ``` 17 + 18 + Or install globally (optional): 19 + 20 + ```bash 21 + npm install -g @hono/cli 22 + ``` 23 + 24 + ## Commands for AI 25 + 26 + ### 1. Search Documentation 27 + 28 + ```bash 29 + hono search "<query>" --pretty 30 + ``` 31 + 32 + Search for Hono APIs and features. Use `--pretty` for human-readable output. 33 + 34 + ### 2. View Documentation 35 + 36 + ```bash 37 + hono docs [path] 38 + ``` 39 + 40 + Display detailed documentation for a specific path found in search results. 41 + 42 + **Examples:** 43 + 44 + ```bash 45 + hono docs /docs/api/context 46 + hono docs /docs/api/hono 47 + hono docs /docs/helpers/factory 48 + ``` 49 + 50 + ### 3. Request Testing 51 + 52 + ```bash 53 + # GET request 54 + hono request [file] -P /path 55 + 56 + # POST request 57 + hono request [file] -X POST -P /api/users -d '{"name": "test"}' 58 + 59 + # Request with headers 60 + hono request [file] -H "Authorization: Bearer token" -P /api/protected 61 + ``` 62 + 63 + Uses `app.request()` internally, so no server startup required for testing. 64 + 65 + ### 4. Optimization & Bundling 66 + 67 + ```bash 68 + # Bundle optimization 69 + hono optimize [entry] -o dist/index.js 70 + 71 + # With minification 72 + hono optimize [entry] -o dist/index.js --minify 73 + 74 + # Specify target (cloudflare-workers, deno, etc.) 75 + hono optimize [entry] -t cloudflare-workers 76 + ``` 77 + 78 + ## Development Workflow 79 + 80 + 1. **Research**: Use `hono search` → `hono docs` to investigate APIs and features 81 + 2. **Implement**: Write the code 82 + 3. **Test**: Use `hono request` to test endpoints 83 + 4. **Optimize**: Use `hono optimize` for production builds when needed 84 + 85 + ## Guidelines 86 + 87 + - Always search with `hono search` before implementing unfamiliar APIs 88 + - Use `--pretty` flag with `hono search` (default output is JSON) 89 + - `hono request` works without starting an HTTP server 90 + - Search for middleware usage with `hono search "middleware name"`
+202
.agents/skills/skill-creator/LICENSE.txt
··· 1 + 2 + Apache License 3 + Version 2.0, January 2004 4 + http://www.apache.org/licenses/ 5 + 6 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 + 8 + 1. Definitions. 9 + 10 + "License" shall mean the terms and conditions for use, reproduction, 11 + and distribution as defined by Sections 1 through 9 of this document. 12 + 13 + "Licensor" shall mean the copyright owner or entity authorized by 14 + the copyright owner that is granting the License. 15 + 16 + "Legal Entity" shall mean the union of the acting entity and all 17 + other entities that control, are controlled by, or are under common 18 + control with that entity. For the purposes of this definition, 19 + "control" means (i) the power, direct or indirect, to cause the 20 + direction or management of such entity, whether by contract or 21 + otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 + outstanding shares, or (iii) beneficial ownership of such entity. 23 + 24 + "You" (or "Your") shall mean an individual or Legal Entity 25 + exercising permissions granted by this License. 26 + 27 + "Source" form shall mean the preferred form for making modifications, 28 + including but not limited to software source code, documentation 29 + source, and configuration files. 30 + 31 + "Object" form shall mean any form resulting from mechanical 32 + transformation or translation of a Source form, including but 33 + not limited to compiled object code, generated documentation, 34 + and conversions to other media types. 35 + 36 + "Work" shall mean the work of authorship, whether in Source or 37 + Object form, made available under the License, as indicated by a 38 + copyright notice that is included in or attached to the work 39 + (an example is provided in the Appendix below). 40 + 41 + "Derivative Works" shall mean any work, whether in Source or Object 42 + form, that is based on (or derived from) the Work and for which the 43 + editorial revisions, annotations, elaborations, or other modifications 44 + represent, as a whole, an original work of authorship. For the purposes 45 + of this License, Derivative Works shall not include works that remain 46 + separable from, or merely link (or bind by name) to the interfaces of, 47 + the Work and Derivative Works thereof. 48 + 49 + "Contribution" shall mean any work of authorship, including 50 + the original version of the Work and any modifications or additions 51 + to that Work or Derivative Works thereof, that is intentionally 52 + submitted to Licensor for inclusion in the Work by the copyright owner 53 + or by an individual or Legal Entity authorized to submit on behalf of 54 + the copyright owner. For the purposes of this definition, "submitted" 55 + means any form of electronic, verbal, or written communication sent 56 + to the Licensor or its representatives, including but not limited to 57 + communication on electronic mailing lists, source code control systems, 58 + and issue tracking systems that are managed by, or on behalf of, the 59 + Licensor for the purpose of discussing and improving the Work, but 60 + excluding communication that is conspicuously marked or otherwise 61 + designated in writing by the copyright owner as "Not a Contribution." 62 + 63 + "Contributor" shall mean Licensor and any individual or Legal Entity 64 + on behalf of whom a Contribution has been received by Licensor and 65 + subsequently incorporated within the Work. 66 + 67 + 2. Grant of Copyright License. Subject to the terms and conditions of 68 + this License, each Contributor hereby grants to You a perpetual, 69 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 + copyright license to reproduce, prepare Derivative Works of, 71 + publicly display, publicly perform, sublicense, and distribute the 72 + Work and such Derivative Works in Source or Object form. 73 + 74 + 3. Grant of Patent License. Subject to the terms and conditions of 75 + this License, each Contributor hereby grants to You a perpetual, 76 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 + (except as stated in this section) patent license to make, have made, 78 + use, offer to sell, sell, import, and otherwise transfer the Work, 79 + where such license applies only to those patent claims licensable 80 + by such Contributor that are necessarily infringed by their 81 + Contribution(s) alone or by combination of their Contribution(s) 82 + with the Work to which such Contribution(s) was submitted. If You 83 + institute patent litigation against any entity (including a 84 + cross-claim or counterclaim in a lawsuit) alleging that the Work 85 + or a Contribution incorporated within the Work constitutes direct 86 + or contributory patent infringement, then any patent licenses 87 + granted to You under this License for that Work shall terminate 88 + as of the date such litigation is filed. 89 + 90 + 4. Redistribution. You may reproduce and distribute copies of the 91 + Work or Derivative Works thereof in any medium, with or without 92 + modifications, and in Source or Object form, provided that You 93 + meet the following conditions: 94 + 95 + (a) You must give any other recipients of the Work or 96 + Derivative Works a copy of this License; and 97 + 98 + (b) You must cause any modified files to carry prominent notices 99 + stating that You changed the files; and 100 + 101 + (c) You must retain, in the Source form of any Derivative Works 102 + that You distribute, all copyright, patent, trademark, and 103 + attribution notices from the Source form of the Work, 104 + excluding those notices that do not pertain to any part of 105 + the Derivative Works; and 106 + 107 + (d) If the Work includes a "NOTICE" text file as part of its 108 + distribution, then any Derivative Works that You distribute must 109 + include a readable copy of the attribution notices contained 110 + within such NOTICE file, excluding those notices that do not 111 + pertain to any part of the Derivative Works, in at least one 112 + of the following places: within a NOTICE text file distributed 113 + as part of the Derivative Works; within the Source form or 114 + documentation, if provided along with the Derivative Works; or, 115 + within a display generated by the Derivative Works, if and 116 + wherever such third-party notices normally appear. The contents 117 + of the NOTICE file are for informational purposes only and 118 + do not modify the License. You may add Your own attribution 119 + notices within Derivative Works that You distribute, alongside 120 + or as an addendum to the NOTICE text from the Work, provided 121 + that such additional attribution notices cannot be construed 122 + as modifying the License. 123 + 124 + You may add Your own copyright statement to Your modifications and 125 + may provide additional or different license terms and conditions 126 + for use, reproduction, or distribution of Your modifications, or 127 + for any such Derivative Works as a whole, provided Your use, 128 + reproduction, and distribution of the Work otherwise complies with 129 + the conditions stated in this License. 130 + 131 + 5. Submission of Contributions. Unless You explicitly state otherwise, 132 + any Contribution intentionally submitted for inclusion in the Work 133 + by You to the Licensor shall be under the terms and conditions of 134 + this License, without any additional terms or conditions. 135 + Notwithstanding the above, nothing herein shall supersede or modify 136 + the terms of any separate license agreement you may have executed 137 + with Licensor regarding such Contributions. 138 + 139 + 6. Trademarks. This License does not grant permission to use the trade 140 + names, trademarks, service marks, or product names of the Licensor, 141 + except as required for reasonable and customary use in describing the 142 + origin of the Work and reproducing the content of the NOTICE file. 143 + 144 + 7. Disclaimer of Warranty. Unless required by applicable law or 145 + agreed to in writing, Licensor provides the Work (and each 146 + Contributor provides its Contributions) on an "AS IS" BASIS, 147 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 + implied, including, without limitation, any warranties or conditions 149 + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 + PARTICULAR PURPOSE. You are solely responsible for determining the 151 + appropriateness of using or redistributing the Work and assume any 152 + risks associated with Your exercise of permissions under this License. 153 + 154 + 8. Limitation of Liability. In no event and under no legal theory, 155 + whether in tort (including negligence), contract, or otherwise, 156 + unless required by applicable law (such as deliberate and grossly 157 + negligent acts) or agreed to in writing, shall any Contributor be 158 + liable to You for damages, including any direct, indirect, special, 159 + incidental, or consequential damages of any character arising as a 160 + result of this License or out of the use or inability to use the 161 + Work (including but not limited to damages for loss of goodwill, 162 + work stoppage, computer failure or malfunction, or any and all 163 + other commercial damages or losses), even if such Contributor 164 + has been advised of the possibility of such damages. 165 + 166 + 9. Accepting Warranty or Additional Liability. While redistributing 167 + the Work or Derivative Works thereof, You may choose to offer, 168 + and charge a fee for, acceptance of support, warranty, indemnity, 169 + or other liability obligations and/or rights consistent with this 170 + License. However, in accepting such obligations, You may act only 171 + on Your own behalf and on Your sole responsibility, not on behalf 172 + of any other Contributor, and only if You agree to indemnify, 173 + defend, and hold each Contributor harmless for any liability 174 + incurred by, or claims asserted against, such Contributor by reason 175 + of your accepting any such warranty or additional liability. 176 + 177 + END OF TERMS AND CONDITIONS 178 + 179 + APPENDIX: How to apply the Apache License to your work. 180 + 181 + To apply the Apache License to your work, attach the following 182 + boilerplate notice, with the fields enclosed by brackets "[]" 183 + replaced with your own identifying information. (Don't include 184 + the brackets!) The text should be enclosed in the appropriate 185 + comment syntax for the file format. We also recommend that a 186 + file or class name and description of purpose be included on the 187 + same "printed page" as the copyright notice for easier 188 + identification within third-party archives. 189 + 190 + Copyright [yyyy] [name of copyright owner] 191 + 192 + Licensed under the Apache License, Version 2.0 (the "License"); 193 + you may not use this file except in compliance with the License. 194 + You may obtain a copy of the License at 195 + 196 + http://www.apache.org/licenses/LICENSE-2.0 197 + 198 + Unless required by applicable law or agreed to in writing, software 199 + distributed under the License is distributed on an "AS IS" BASIS, 200 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 + See the License for the specific language governing permissions and 202 + limitations under the License.
+356
.agents/skills/skill-creator/SKILL.md
··· 1 + --- 2 + name: skill-creator 3 + description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations. 4 + license: Complete terms in LICENSE.txt 5 + --- 6 + 7 + # Skill Creator 8 + 9 + This skill provides guidance for creating effective skills. 10 + 11 + ## About Skills 12 + 13 + Skills are modular, self-contained packages that extend Claude's capabilities by providing 14 + specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific 15 + domains or tasks—they transform Claude from a general-purpose agent into a specialized agent 16 + equipped with procedural knowledge that no model can fully possess. 17 + 18 + ### What Skills Provide 19 + 20 + 1. Specialized workflows - Multi-step procedures for specific domains 21 + 2. Tool integrations - Instructions for working with specific file formats or APIs 22 + 3. Domain expertise - Company-specific knowledge, schemas, business logic 23 + 4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks 24 + 25 + ## Core Principles 26 + 27 + ### Concise is Key 28 + 29 + The context window is a public good. Skills share the context window with everything else Claude needs: system prompt, conversation history, other Skills' metadata, and the actual user request. 30 + 31 + **Default assumption: Claude is already very smart.** Only add context Claude doesn't already have. Challenge each piece of information: "Does Claude really need this explanation?" and "Does this paragraph justify its token cost?" 32 + 33 + Prefer concise examples over verbose explanations. 34 + 35 + ### Set Appropriate Degrees of Freedom 36 + 37 + Match the level of specificity to the task's fragility and variability: 38 + 39 + **High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach. 40 + 41 + **Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior. 42 + 43 + **Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed. 44 + 45 + Think of Claude as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom). 46 + 47 + ### Anatomy of a Skill 48 + 49 + Every skill consists of a required SKILL.md file and optional bundled resources: 50 + 51 + ``` 52 + skill-name/ 53 + ├── SKILL.md (required) 54 + │ ├── YAML frontmatter metadata (required) 55 + │ │ ├── name: (required) 56 + │ │ └── description: (required) 57 + │ └── Markdown instructions (required) 58 + └── Bundled Resources (optional) 59 + ├── scripts/ - Executable code (Python/Bash/etc.) 60 + ├── references/ - Documentation intended to be loaded into context as needed 61 + └── assets/ - Files used in output (templates, icons, fonts, etc.) 62 + ``` 63 + 64 + #### SKILL.md (required) 65 + 66 + Every SKILL.md consists of: 67 + 68 + - **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields that Claude reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used. 69 + - **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all). 70 + 71 + #### Bundled Resources (optional) 72 + 73 + ##### Scripts (`scripts/`) 74 + 75 + Executable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten. 76 + 77 + - **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed 78 + - **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks 79 + - **Benefits**: Token efficient, deterministic, may be executed without loading into context 80 + - **Note**: Scripts may still need to be read by Claude for patching or environment-specific adjustments 81 + 82 + ##### References (`references/`) 83 + 84 + Documentation and reference material intended to be loaded as needed into context to inform Claude's process and thinking. 85 + 86 + - **When to include**: For documentation that Claude should reference while working 87 + - **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications 88 + - **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides 89 + - **Benefits**: Keeps SKILL.md lean, loaded only when Claude determines it's needed 90 + - **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md 91 + - **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files. 92 + 93 + ##### Assets (`assets/`) 94 + 95 + Files not intended to be loaded into context, but rather used within the output Claude produces. 96 + 97 + - **When to include**: When the skill needs files that will be used in the final output 98 + - **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography 99 + - **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified 100 + - **Benefits**: Separates output resources from documentation, enables Claude to use files without loading them into context 101 + 102 + #### What to Not Include in a Skill 103 + 104 + A skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including: 105 + 106 + - README.md 107 + - INSTALLATION_GUIDE.md 108 + - QUICK_REFERENCE.md 109 + - CHANGELOG.md 110 + - etc. 111 + 112 + The skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxilary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion. 113 + 114 + ### Progressive Disclosure Design Principle 115 + 116 + Skills use a three-level loading system to manage context efficiently: 117 + 118 + 1. **Metadata (name + description)** - Always in context (~100 words) 119 + 2. **SKILL.md body** - When skill triggers (<5k words) 120 + 3. **Bundled resources** - As needed by Claude (Unlimited because scripts can be executed without reading into context window) 121 + 122 + #### Progressive Disclosure Patterns 123 + 124 + Keep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them. 125 + 126 + **Key principle:** When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files. 127 + 128 + **Pattern 1: High-level guide with references** 129 + 130 + ```markdown 131 + # PDF Processing 132 + 133 + ## Quick start 134 + 135 + Extract text with pdfplumber: 136 + [code example] 137 + 138 + ## Advanced features 139 + 140 + - **Form filling**: See [FORMS.md](FORMS.md) for complete guide 141 + - **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods 142 + - **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns 143 + ``` 144 + 145 + Claude loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed. 146 + 147 + **Pattern 2: Domain-specific organization** 148 + 149 + For Skills with multiple domains, organize content by domain to avoid loading irrelevant context: 150 + 151 + ``` 152 + bigquery-skill/ 153 + ├── SKILL.md (overview and navigation) 154 + └── reference/ 155 + ├── finance.md (revenue, billing metrics) 156 + ├── sales.md (opportunities, pipeline) 157 + ├── product.md (API usage, features) 158 + └── marketing.md (campaigns, attribution) 159 + ``` 160 + 161 + When a user asks about sales metrics, Claude only reads sales.md. 162 + 163 + Similarly, for skills supporting multiple frameworks or variants, organize by variant: 164 + 165 + ``` 166 + cloud-deploy/ 167 + ├── SKILL.md (workflow + provider selection) 168 + └── references/ 169 + ├── aws.md (AWS deployment patterns) 170 + ├── gcp.md (GCP deployment patterns) 171 + └── azure.md (Azure deployment patterns) 172 + ``` 173 + 174 + When the user chooses AWS, Claude only reads aws.md. 175 + 176 + **Pattern 3: Conditional details** 177 + 178 + Show basic content, link to advanced content: 179 + 180 + ```markdown 181 + # DOCX Processing 182 + 183 + ## Creating documents 184 + 185 + Use docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md). 186 + 187 + ## Editing documents 188 + 189 + For simple edits, modify the XML directly. 190 + 191 + **For tracked changes**: See [REDLINING.md](REDLINING.md) 192 + **For OOXML details**: See [OOXML.md](OOXML.md) 193 + ``` 194 + 195 + Claude reads REDLINING.md or OOXML.md only when the user needs those features. 196 + 197 + **Important guidelines:** 198 + 199 + - **Avoid deeply nested references** - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md. 200 + - **Structure longer reference files** - For files longer than 100 lines, include a table of contents at the top so Claude can see the full scope when previewing. 201 + 202 + ## Skill Creation Process 203 + 204 + Skill creation involves these steps: 205 + 206 + 1. Understand the skill with concrete examples 207 + 2. Plan reusable skill contents (scripts, references, assets) 208 + 3. Initialize the skill (run init_skill.py) 209 + 4. Edit the skill (implement resources and write SKILL.md) 210 + 5. Package the skill (run package_skill.py) 211 + 6. Iterate based on real usage 212 + 213 + Follow these steps in order, skipping only if there is a clear reason why they are not applicable. 214 + 215 + ### Step 1: Understanding the Skill with Concrete Examples 216 + 217 + Skip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill. 218 + 219 + To create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback. 220 + 221 + For example, when building an image-editor skill, relevant questions include: 222 + 223 + - "What functionality should the image-editor skill support? Editing, rotating, anything else?" 224 + - "Can you give some examples of how this skill would be used?" 225 + - "I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?" 226 + - "What would a user say that should trigger this skill?" 227 + 228 + To avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness. 229 + 230 + Conclude this step when there is a clear sense of the functionality the skill should support. 231 + 232 + ### Step 2: Planning the Reusable Skill Contents 233 + 234 + To turn concrete examples into an effective skill, analyze each example by: 235 + 236 + 1. Considering how to execute on the example from scratch 237 + 2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly 238 + 239 + Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows: 240 + 241 + 1. Rotating a PDF requires re-writing the same code each time 242 + 2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill 243 + 244 + Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows: 245 + 246 + 1. Writing a frontend webapp requires the same boilerplate HTML/React each time 247 + 2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill 248 + 249 + Example: When building a `big-query` skill to handle queries like "How many users have logged in today?" the analysis shows: 250 + 251 + 1. Querying BigQuery requires re-discovering the table schemas and relationships each time 252 + 2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill 253 + 254 + To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets. 255 + 256 + ### Step 3: Initializing the Skill 257 + 258 + At this point, it is time to actually create the skill. 259 + 260 + Skip this step only if the skill being developed already exists, and iteration or packaging is needed. In this case, continue to the next step. 261 + 262 + When creating a new skill from scratch, always run the `init_skill.py` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable. 263 + 264 + Usage: 265 + 266 + ```bash 267 + scripts/init_skill.py <skill-name> --path <output-directory> 268 + ``` 269 + 270 + The script: 271 + 272 + - Creates the skill directory at the specified path 273 + - Generates a SKILL.md template with proper frontmatter and TODO placeholders 274 + - Creates example resource directories: `scripts/`, `references/`, and `assets/` 275 + - Adds example files in each directory that can be customized or deleted 276 + 277 + After initialization, customize or remove the generated SKILL.md and example files as needed. 278 + 279 + ### Step 4: Edit the Skill 280 + 281 + When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Claude to use. Include information that would be beneficial and non-obvious to Claude. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Claude instance execute these tasks more effectively. 282 + 283 + #### Learn Proven Design Patterns 284 + 285 + Consult these helpful guides based on your skill's needs: 286 + 287 + - **Multi-step processes**: See references/workflows.md for sequential workflows and conditional logic 288 + - **Specific output formats or quality standards**: See references/output-patterns.md for template and example patterns 289 + 290 + These files contain established best practices for effective skill design. 291 + 292 + #### Start with Reusable Skill Contents 293 + 294 + To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`. 295 + 296 + Added scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion. 297 + 298 + Any example files and directories not needed for the skill should be deleted. The initialization script creates example files in `scripts/`, `references/`, and `assets/` to demonstrate structure, but most skills won't need all of them. 299 + 300 + #### Update SKILL.md 301 + 302 + **Writing Guidelines:** Always use imperative/infinitive form. 303 + 304 + ##### Frontmatter 305 + 306 + Write the YAML frontmatter with `name` and `description`: 307 + 308 + - `name`: The skill name 309 + - `description`: This is the primary triggering mechanism for your skill, and helps Claude understand when to use the skill. 310 + - Include both what the Skill does and specific triggers/contexts for when to use it. 311 + - Include all "when to use" information here - Not in the body. The body is only loaded after triggering, so "When to Use This Skill" sections in the body are not helpful to Claude. 312 + - Example description for a `docx` skill: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks" 313 + 314 + Do not include any other fields in YAML frontmatter. 315 + 316 + ##### Body 317 + 318 + Write instructions for using the skill and its bundled resources. 319 + 320 + ### Step 5: Packaging a Skill 321 + 322 + Once development of the skill is complete, it must be packaged into a distributable .skill file that gets shared with the user. The packaging process automatically validates the skill first to ensure it meets all requirements: 323 + 324 + ```bash 325 + scripts/package_skill.py <path/to/skill-folder> 326 + ``` 327 + 328 + Optional output directory specification: 329 + 330 + ```bash 331 + scripts/package_skill.py <path/to/skill-folder> ./dist 332 + ``` 333 + 334 + The packaging script will: 335 + 336 + 1. **Validate** the skill automatically, checking: 337 + 338 + - YAML frontmatter format and required fields 339 + - Skill naming conventions and directory structure 340 + - Description completeness and quality 341 + - File organization and resource references 342 + 343 + 2. **Package** the skill if validation passes, creating a .skill file named after the skill (e.g., `my-skill.skill`) that includes all files and maintains the proper directory structure for distribution. The .skill file is a zip file with a .skill extension. 344 + 345 + If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again. 346 + 347 + ### Step 6: Iterate 348 + 349 + After testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed. 350 + 351 + **Iteration workflow:** 352 + 353 + 1. Use the skill on real tasks 354 + 2. Notice struggles or inefficiencies 355 + 3. Identify how SKILL.md or bundled resources should be updated 356 + 4. Implement changes and test again
+82
.agents/skills/skill-creator/references/output-patterns.md
··· 1 + # Output Patterns 2 + 3 + Use these patterns when skills need to produce consistent, high-quality output. 4 + 5 + ## Template Pattern 6 + 7 + Provide templates for output format. Match the level of strictness to your needs. 8 + 9 + **For strict requirements (like API responses or data formats):** 10 + 11 + ```markdown 12 + ## Report structure 13 + 14 + ALWAYS use this exact template structure: 15 + 16 + # [Analysis Title] 17 + 18 + ## Executive summary 19 + [One-paragraph overview of key findings] 20 + 21 + ## Key findings 22 + - Finding 1 with supporting data 23 + - Finding 2 with supporting data 24 + - Finding 3 with supporting data 25 + 26 + ## Recommendations 27 + 1. Specific actionable recommendation 28 + 2. Specific actionable recommendation 29 + ``` 30 + 31 + **For flexible guidance (when adaptation is useful):** 32 + 33 + ```markdown 34 + ## Report structure 35 + 36 + Here is a sensible default format, but use your best judgment: 37 + 38 + # [Analysis Title] 39 + 40 + ## Executive summary 41 + [Overview] 42 + 43 + ## Key findings 44 + [Adapt sections based on what you discover] 45 + 46 + ## Recommendations 47 + [Tailor to the specific context] 48 + 49 + Adjust sections as needed for the specific analysis type. 50 + ``` 51 + 52 + ## Examples Pattern 53 + 54 + For skills where output quality depends on seeing examples, provide input/output pairs: 55 + 56 + ```markdown 57 + ## Commit message format 58 + 59 + Generate commit messages following these examples: 60 + 61 + **Example 1:** 62 + Input: Added user authentication with JWT tokens 63 + Output: 64 + ``` 65 + feat(auth): implement JWT-based authentication 66 + 67 + Add login endpoint and token validation middleware 68 + ``` 69 + 70 + **Example 2:** 71 + Input: Fixed bug where dates displayed incorrectly in reports 72 + Output: 73 + ``` 74 + fix(reports): correct date formatting in timezone conversion 75 + 76 + Use UTC timestamps consistently across report generation 77 + ``` 78 + 79 + Follow this style: type(scope): brief description, then detailed explanation. 80 + ``` 81 + 82 + Examples help Claude understand the desired style and level of detail more clearly than descriptions alone.
+28
.agents/skills/skill-creator/references/workflows.md
··· 1 + # Workflow Patterns 2 + 3 + ## Sequential Workflows 4 + 5 + For complex tasks, break operations into clear, sequential steps. It is often helpful to give Claude an overview of the process towards the beginning of SKILL.md: 6 + 7 + ```markdown 8 + Filling a PDF form involves these steps: 9 + 10 + 1. Analyze the form (run analyze_form.py) 11 + 2. Create field mapping (edit fields.json) 12 + 3. Validate mapping (run validate_fields.py) 13 + 4. Fill the form (run fill_form.py) 14 + 5. Verify output (run verify_output.py) 15 + ``` 16 + 17 + ## Conditional Workflows 18 + 19 + For tasks with branching logic, guide Claude through decision points: 20 + 21 + ```markdown 22 + 1. Determine the modification type: 23 + **Creating new content?** → Follow "Creation workflow" below 24 + **Editing existing content?** → Follow "Editing workflow" below 25 + 26 + 2. Creation workflow: [steps] 27 + 3. Editing workflow: [steps] 28 + ```
+303
.agents/skills/skill-creator/scripts/init_skill.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Skill Initializer - Creates a new skill from template 4 + 5 + Usage: 6 + init_skill.py <skill-name> --path <path> 7 + 8 + Examples: 9 + init_skill.py my-new-skill --path skills/public 10 + init_skill.py my-api-helper --path skills/private 11 + init_skill.py custom-skill --path /custom/location 12 + """ 13 + 14 + import sys 15 + from pathlib import Path 16 + 17 + 18 + SKILL_TEMPLATE = """--- 19 + name: {skill_name} 20 + description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.] 21 + --- 22 + 23 + # {skill_title} 24 + 25 + ## Overview 26 + 27 + [TODO: 1-2 sentences explaining what this skill enables] 28 + 29 + ## Structuring This Skill 30 + 31 + [TODO: Choose the structure that best fits this skill's purpose. Common patterns: 32 + 33 + **1. Workflow-Based** (best for sequential processes) 34 + - Works well when there are clear step-by-step procedures 35 + - Example: DOCX skill with "Workflow Decision Tree" → "Reading" → "Creating" → "Editing" 36 + - Structure: ## Overview → ## Workflow Decision Tree → ## Step 1 → ## Step 2... 37 + 38 + **2. Task-Based** (best for tool collections) 39 + - Works well when the skill offers different operations/capabilities 40 + - Example: PDF skill with "Quick Start" → "Merge PDFs" → "Split PDFs" → "Extract Text" 41 + - Structure: ## Overview → ## Quick Start → ## Task Category 1 → ## Task Category 2... 42 + 43 + **3. Reference/Guidelines** (best for standards or specifications) 44 + - Works well for brand guidelines, coding standards, or requirements 45 + - Example: Brand styling with "Brand Guidelines" → "Colors" → "Typography" → "Features" 46 + - Structure: ## Overview → ## Guidelines → ## Specifications → ## Usage... 47 + 48 + **4. Capabilities-Based** (best for integrated systems) 49 + - Works well when the skill provides multiple interrelated features 50 + - Example: Product Management with "Core Capabilities" → numbered capability list 51 + - Structure: ## Overview → ## Core Capabilities → ### 1. Feature → ### 2. Feature... 52 + 53 + Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations). 54 + 55 + Delete this entire "Structuring This Skill" section when done - it's just guidance.] 56 + 57 + ## [TODO: Replace with the first main section based on chosen structure] 58 + 59 + [TODO: Add content here. See examples in existing skills: 60 + - Code samples for technical skills 61 + - Decision trees for complex workflows 62 + - Concrete examples with realistic user requests 63 + - References to scripts/templates/references as needed] 64 + 65 + ## Resources 66 + 67 + This skill includes example resource directories that demonstrate how to organize different types of bundled resources: 68 + 69 + ### scripts/ 70 + Executable code (Python/Bash/etc.) that can be run directly to perform specific operations. 71 + 72 + **Examples from other skills:** 73 + - PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation 74 + - DOCX skill: `document.py`, `utilities.py` - Python modules for document processing 75 + 76 + **Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations. 77 + 78 + **Note:** Scripts may be executed without loading into context, but can still be read by Claude for patching or environment adjustments. 79 + 80 + ### references/ 81 + Documentation and reference material intended to be loaded into context to inform Claude's process and thinking. 82 + 83 + **Examples from other skills:** 84 + - Product management: `communication.md`, `context_building.md` - detailed workflow guides 85 + - BigQuery: API reference documentation and query examples 86 + - Finance: Schema documentation, company policies 87 + 88 + **Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Claude should reference while working. 89 + 90 + ### assets/ 91 + Files not intended to be loaded into context, but rather used within the output Claude produces. 92 + 93 + **Examples from other skills:** 94 + - Brand styling: PowerPoint template files (.pptx), logo files 95 + - Frontend builder: HTML/React boilerplate project directories 96 + - Typography: Font files (.ttf, .woff2) 97 + 98 + **Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output. 99 + 100 + --- 101 + 102 + **Any unneeded directories can be deleted.** Not every skill requires all three types of resources. 103 + """ 104 + 105 + EXAMPLE_SCRIPT = '''#!/usr/bin/env python3 106 + """ 107 + Example helper script for {skill_name} 108 + 109 + This is a placeholder script that can be executed directly. 110 + Replace with actual implementation or delete if not needed. 111 + 112 + Example real scripts from other skills: 113 + - pdf/scripts/fill_fillable_fields.py - Fills PDF form fields 114 + - pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images 115 + """ 116 + 117 + def main(): 118 + print("This is an example script for {skill_name}") 119 + # TODO: Add actual script logic here 120 + # This could be data processing, file conversion, API calls, etc. 121 + 122 + if __name__ == "__main__": 123 + main() 124 + ''' 125 + 126 + EXAMPLE_REFERENCE = """# Reference Documentation for {skill_title} 127 + 128 + This is a placeholder for detailed reference documentation. 129 + Replace with actual reference content or delete if not needed. 130 + 131 + Example real reference docs from other skills: 132 + - product-management/references/communication.md - Comprehensive guide for status updates 133 + - product-management/references/context_building.md - Deep-dive on gathering context 134 + - bigquery/references/ - API references and query examples 135 + 136 + ## When Reference Docs Are Useful 137 + 138 + Reference docs are ideal for: 139 + - Comprehensive API documentation 140 + - Detailed workflow guides 141 + - Complex multi-step processes 142 + - Information too lengthy for main SKILL.md 143 + - Content that's only needed for specific use cases 144 + 145 + ## Structure Suggestions 146 + 147 + ### API Reference Example 148 + - Overview 149 + - Authentication 150 + - Endpoints with examples 151 + - Error codes 152 + - Rate limits 153 + 154 + ### Workflow Guide Example 155 + - Prerequisites 156 + - Step-by-step instructions 157 + - Common patterns 158 + - Troubleshooting 159 + - Best practices 160 + """ 161 + 162 + EXAMPLE_ASSET = """# Example Asset File 163 + 164 + This placeholder represents where asset files would be stored. 165 + Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed. 166 + 167 + Asset files are NOT intended to be loaded into context, but rather used within 168 + the output Claude produces. 169 + 170 + Example asset files from other skills: 171 + - Brand guidelines: logo.png, slides_template.pptx 172 + - Frontend builder: hello-world/ directory with HTML/React boilerplate 173 + - Typography: custom-font.ttf, font-family.woff2 174 + - Data: sample_data.csv, test_dataset.json 175 + 176 + ## Common Asset Types 177 + 178 + - Templates: .pptx, .docx, boilerplate directories 179 + - Images: .png, .jpg, .svg, .gif 180 + - Fonts: .ttf, .otf, .woff, .woff2 181 + - Boilerplate code: Project directories, starter files 182 + - Icons: .ico, .svg 183 + - Data files: .csv, .json, .xml, .yaml 184 + 185 + Note: This is a text placeholder. Actual assets can be any file type. 186 + """ 187 + 188 + 189 + def title_case_skill_name(skill_name): 190 + """Convert hyphenated skill name to Title Case for display.""" 191 + return ' '.join(word.capitalize() for word in skill_name.split('-')) 192 + 193 + 194 + def init_skill(skill_name, path): 195 + """ 196 + Initialize a new skill directory with template SKILL.md. 197 + 198 + Args: 199 + skill_name: Name of the skill 200 + path: Path where the skill directory should be created 201 + 202 + Returns: 203 + Path to created skill directory, or None if error 204 + """ 205 + # Determine skill directory path 206 + skill_dir = Path(path).resolve() / skill_name 207 + 208 + # Check if directory already exists 209 + if skill_dir.exists(): 210 + print(f"❌ Error: Skill directory already exists: {skill_dir}") 211 + return None 212 + 213 + # Create skill directory 214 + try: 215 + skill_dir.mkdir(parents=True, exist_ok=False) 216 + print(f"✅ Created skill directory: {skill_dir}") 217 + except Exception as e: 218 + print(f"❌ Error creating directory: {e}") 219 + return None 220 + 221 + # Create SKILL.md from template 222 + skill_title = title_case_skill_name(skill_name) 223 + skill_content = SKILL_TEMPLATE.format( 224 + skill_name=skill_name, 225 + skill_title=skill_title 226 + ) 227 + 228 + skill_md_path = skill_dir / 'SKILL.md' 229 + try: 230 + skill_md_path.write_text(skill_content) 231 + print("✅ Created SKILL.md") 232 + except Exception as e: 233 + print(f"❌ Error creating SKILL.md: {e}") 234 + return None 235 + 236 + # Create resource directories with example files 237 + try: 238 + # Create scripts/ directory with example script 239 + scripts_dir = skill_dir / 'scripts' 240 + scripts_dir.mkdir(exist_ok=True) 241 + example_script = scripts_dir / 'example.py' 242 + example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name)) 243 + example_script.chmod(0o755) 244 + print("✅ Created scripts/example.py") 245 + 246 + # Create references/ directory with example reference doc 247 + references_dir = skill_dir / 'references' 248 + references_dir.mkdir(exist_ok=True) 249 + example_reference = references_dir / 'api_reference.md' 250 + example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title)) 251 + print("✅ Created references/api_reference.md") 252 + 253 + # Create assets/ directory with example asset placeholder 254 + assets_dir = skill_dir / 'assets' 255 + assets_dir.mkdir(exist_ok=True) 256 + example_asset = assets_dir / 'example_asset.txt' 257 + example_asset.write_text(EXAMPLE_ASSET) 258 + print("✅ Created assets/example_asset.txt") 259 + except Exception as e: 260 + print(f"❌ Error creating resource directories: {e}") 261 + return None 262 + 263 + # Print next steps 264 + print(f"\n✅ Skill '{skill_name}' initialized successfully at {skill_dir}") 265 + print("\nNext steps:") 266 + print("1. Edit SKILL.md to complete the TODO items and update the description") 267 + print("2. Customize or delete the example files in scripts/, references/, and assets/") 268 + print("3. Run the validator when ready to check the skill structure") 269 + 270 + return skill_dir 271 + 272 + 273 + def main(): 274 + if len(sys.argv) < 4 or sys.argv[2] != '--path': 275 + print("Usage: init_skill.py <skill-name> --path <path>") 276 + print("\nSkill name requirements:") 277 + print(" - Hyphen-case identifier (e.g., 'data-analyzer')") 278 + print(" - Lowercase letters, digits, and hyphens only") 279 + print(" - Max 40 characters") 280 + print(" - Must match directory name exactly") 281 + print("\nExamples:") 282 + print(" init_skill.py my-new-skill --path skills/public") 283 + print(" init_skill.py my-api-helper --path skills/private") 284 + print(" init_skill.py custom-skill --path /custom/location") 285 + sys.exit(1) 286 + 287 + skill_name = sys.argv[1] 288 + path = sys.argv[3] 289 + 290 + print(f"🚀 Initializing skill: {skill_name}") 291 + print(f" Location: {path}") 292 + print() 293 + 294 + result = init_skill(skill_name, path) 295 + 296 + if result: 297 + sys.exit(0) 298 + else: 299 + sys.exit(1) 300 + 301 + 302 + if __name__ == "__main__": 303 + main()
+110
.agents/skills/skill-creator/scripts/package_skill.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Skill Packager - Creates a distributable .skill file of a skill folder 4 + 5 + Usage: 6 + python utils/package_skill.py <path/to/skill-folder> [output-directory] 7 + 8 + Example: 9 + python utils/package_skill.py skills/public/my-skill 10 + python utils/package_skill.py skills/public/my-skill ./dist 11 + """ 12 + 13 + import sys 14 + import zipfile 15 + from pathlib import Path 16 + from quick_validate import validate_skill 17 + 18 + 19 + def package_skill(skill_path, output_dir=None): 20 + """ 21 + Package a skill folder into a .skill file. 22 + 23 + Args: 24 + skill_path: Path to the skill folder 25 + output_dir: Optional output directory for the .skill file (defaults to current directory) 26 + 27 + Returns: 28 + Path to the created .skill file, or None if error 29 + """ 30 + skill_path = Path(skill_path).resolve() 31 + 32 + # Validate skill folder exists 33 + if not skill_path.exists(): 34 + print(f"❌ Error: Skill folder not found: {skill_path}") 35 + return None 36 + 37 + if not skill_path.is_dir(): 38 + print(f"❌ Error: Path is not a directory: {skill_path}") 39 + return None 40 + 41 + # Validate SKILL.md exists 42 + skill_md = skill_path / "SKILL.md" 43 + if not skill_md.exists(): 44 + print(f"❌ Error: SKILL.md not found in {skill_path}") 45 + return None 46 + 47 + # Run validation before packaging 48 + print("🔍 Validating skill...") 49 + valid, message = validate_skill(skill_path) 50 + if not valid: 51 + print(f"❌ Validation failed: {message}") 52 + print(" Please fix the validation errors before packaging.") 53 + return None 54 + print(f"✅ {message}\n") 55 + 56 + # Determine output location 57 + skill_name = skill_path.name 58 + if output_dir: 59 + output_path = Path(output_dir).resolve() 60 + output_path.mkdir(parents=True, exist_ok=True) 61 + else: 62 + output_path = Path.cwd() 63 + 64 + skill_filename = output_path / f"{skill_name}.skill" 65 + 66 + # Create the .skill file (zip format) 67 + try: 68 + with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf: 69 + # Walk through the skill directory 70 + for file_path in skill_path.rglob('*'): 71 + if file_path.is_file(): 72 + # Calculate the relative path within the zip 73 + arcname = file_path.relative_to(skill_path.parent) 74 + zipf.write(file_path, arcname) 75 + print(f" Added: {arcname}") 76 + 77 + print(f"\n✅ Successfully packaged skill to: {skill_filename}") 78 + return skill_filename 79 + 80 + except Exception as e: 81 + print(f"❌ Error creating .skill file: {e}") 82 + return None 83 + 84 + 85 + def main(): 86 + if len(sys.argv) < 2: 87 + print("Usage: python utils/package_skill.py <path/to/skill-folder> [output-directory]") 88 + print("\nExample:") 89 + print(" python utils/package_skill.py skills/public/my-skill") 90 + print(" python utils/package_skill.py skills/public/my-skill ./dist") 91 + sys.exit(1) 92 + 93 + skill_path = sys.argv[1] 94 + output_dir = sys.argv[2] if len(sys.argv) > 2 else None 95 + 96 + print(f"📦 Packaging skill: {skill_path}") 97 + if output_dir: 98 + print(f" Output directory: {output_dir}") 99 + print() 100 + 101 + result = package_skill(skill_path, output_dir) 102 + 103 + if result: 104 + sys.exit(0) 105 + else: 106 + sys.exit(1) 107 + 108 + 109 + if __name__ == "__main__": 110 + main()
+95
.agents/skills/skill-creator/scripts/quick_validate.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Quick validation script for skills - minimal version 4 + """ 5 + 6 + import sys 7 + import os 8 + import re 9 + import yaml 10 + from pathlib import Path 11 + 12 + def validate_skill(skill_path): 13 + """Basic validation of a skill""" 14 + skill_path = Path(skill_path) 15 + 16 + # Check SKILL.md exists 17 + skill_md = skill_path / 'SKILL.md' 18 + if not skill_md.exists(): 19 + return False, "SKILL.md not found" 20 + 21 + # Read and validate frontmatter 22 + content = skill_md.read_text() 23 + if not content.startswith('---'): 24 + return False, "No YAML frontmatter found" 25 + 26 + # Extract frontmatter 27 + match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL) 28 + if not match: 29 + return False, "Invalid frontmatter format" 30 + 31 + frontmatter_text = match.group(1) 32 + 33 + # Parse YAML frontmatter 34 + try: 35 + frontmatter = yaml.safe_load(frontmatter_text) 36 + if not isinstance(frontmatter, dict): 37 + return False, "Frontmatter must be a YAML dictionary" 38 + except yaml.YAMLError as e: 39 + return False, f"Invalid YAML in frontmatter: {e}" 40 + 41 + # Define allowed properties 42 + ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata'} 43 + 44 + # Check for unexpected properties (excluding nested keys under metadata) 45 + unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES 46 + if unexpected_keys: 47 + return False, ( 48 + f"Unexpected key(s) in SKILL.md frontmatter: {', '.join(sorted(unexpected_keys))}. " 49 + f"Allowed properties are: {', '.join(sorted(ALLOWED_PROPERTIES))}" 50 + ) 51 + 52 + # Check required fields 53 + if 'name' not in frontmatter: 54 + return False, "Missing 'name' in frontmatter" 55 + if 'description' not in frontmatter: 56 + return False, "Missing 'description' in frontmatter" 57 + 58 + # Extract name for validation 59 + name = frontmatter.get('name', '') 60 + if not isinstance(name, str): 61 + return False, f"Name must be a string, got {type(name).__name__}" 62 + name = name.strip() 63 + if name: 64 + # Check naming convention (hyphen-case: lowercase with hyphens) 65 + if not re.match(r'^[a-z0-9-]+$', name): 66 + return False, f"Name '{name}' should be hyphen-case (lowercase letters, digits, and hyphens only)" 67 + if name.startswith('-') or name.endswith('-') or '--' in name: 68 + return False, f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens" 69 + # Check name length (max 64 characters per spec) 70 + if len(name) > 64: 71 + return False, f"Name is too long ({len(name)} characters). Maximum is 64 characters." 72 + 73 + # Extract and validate description 74 + description = frontmatter.get('description', '') 75 + if not isinstance(description, str): 76 + return False, f"Description must be a string, got {type(description).__name__}" 77 + description = description.strip() 78 + if description: 79 + # Check for angle brackets 80 + if '<' in description or '>' in description: 81 + return False, "Description cannot contain angle brackets (< or >)" 82 + # Check description length (max 1024 characters per spec) 83 + if len(description) > 1024: 84 + return False, f"Description is too long ({len(description)} characters). Maximum is 1024 characters." 85 + 86 + return True, "Skill is valid!" 87 + 88 + if __name__ == "__main__": 89 + if len(sys.argv) != 2: 90 + print("Usage: python quick_validate.py <skill_directory>") 91 + sys.exit(1) 92 + 93 + valid, message = validate_skill(sys.argv[1]) 94 + print(message) 95 + sys.exit(0 if valid else 1)
+946
.agents/skills/vercel-composition-patterns/AGENTS.md
··· 1 + # React Composition Patterns 2 + 3 + **Version 1.0.0** 4 + Engineering 5 + January 2026 6 + 7 + > **Note:** 8 + > This document is mainly for agents and LLMs to follow when maintaining, 9 + > generating, or refactoring React codebases using composition. Humans 10 + > may also find it useful, but guidance here is optimized for automation 11 + > and consistency by AI-assisted workflows. 12 + 13 + --- 14 + 15 + ## Abstract 16 + 17 + Composition patterns for building flexible, maintainable React components. Avoid boolean prop proliferation by using compound components, lifting state, and composing internals. These patterns make codebases easier for both humans and AI agents to work with as they scale. 18 + 19 + --- 20 + 21 + ## Table of Contents 22 + 23 + 1. [Component Architecture](#1-component-architecture) — **HIGH** 24 + - 1.1 [Avoid Boolean Prop Proliferation](#11-avoid-boolean-prop-proliferation) 25 + - 1.2 [Use Compound Components](#12-use-compound-components) 26 + 2. [State Management](#2-state-management) — **MEDIUM** 27 + - 2.1 [Decouple State Management from UI](#21-decouple-state-management-from-ui) 28 + - 2.2 [Define Generic Context Interfaces for Dependency Injection](#22-define-generic-context-interfaces-for-dependency-injection) 29 + - 2.3 [Lift State into Provider Components](#23-lift-state-into-provider-components) 30 + 3. [Implementation Patterns](#3-implementation-patterns) — **MEDIUM** 31 + - 3.1 [Create Explicit Component Variants](#31-create-explicit-component-variants) 32 + - 3.2 [Prefer Composing Children Over Render Props](#32-prefer-composing-children-over-render-props) 33 + 4. [React 19 APIs](#4-react-19-apis) — **MEDIUM** 34 + - 4.1 [React 19 API Changes](#41-react-19-api-changes) 35 + 36 + --- 37 + 38 + ## 1. Component Architecture 39 + 40 + **Impact: HIGH** 41 + 42 + Fundamental patterns for structuring components to avoid prop 43 + proliferation and enable flexible composition. 44 + 45 + ### 1.1 Avoid Boolean Prop Proliferation 46 + 47 + **Impact: CRITICAL (prevents unmaintainable component variants)** 48 + 49 + Don't add boolean props like `isThread`, `isEditing`, `isDMThread` to customize 50 + 51 + component behavior. Each boolean doubles possible states and creates 52 + 53 + unmaintainable conditional logic. Use composition instead. 54 + 55 + **Incorrect: boolean props create exponential complexity** 56 + 57 + ```tsx 58 + function Composer({ 59 + onSubmit, 60 + isThread, 61 + channelId, 62 + isDMThread, 63 + dmId, 64 + isEditing, 65 + isForwarding, 66 + }: Props) { 67 + return ( 68 + <form> 69 + <Header /> 70 + <Input /> 71 + {isDMThread ? ( 72 + <AlsoSendToDMField id={dmId} /> 73 + ) : isThread ? ( 74 + <AlsoSendToChannelField id={channelId} /> 75 + ) : null} 76 + {isEditing ? ( 77 + <EditActions /> 78 + ) : isForwarding ? ( 79 + <ForwardActions /> 80 + ) : ( 81 + <DefaultActions /> 82 + )} 83 + <Footer onSubmit={onSubmit} /> 84 + </form> 85 + ) 86 + } 87 + ``` 88 + 89 + **Correct: composition eliminates conditionals** 90 + 91 + ```tsx 92 + // Channel composer 93 + function ChannelComposer() { 94 + return ( 95 + <Composer.Frame> 96 + <Composer.Header /> 97 + <Composer.Input /> 98 + <Composer.Footer> 99 + <Composer.Attachments /> 100 + <Composer.Formatting /> 101 + <Composer.Emojis /> 102 + <Composer.Submit /> 103 + </Composer.Footer> 104 + </Composer.Frame> 105 + ) 106 + } 107 + 108 + // Thread composer - adds "also send to channel" field 109 + function ThreadComposer({ channelId }: { channelId: string }) { 110 + return ( 111 + <Composer.Frame> 112 + <Composer.Header /> 113 + <Composer.Input /> 114 + <AlsoSendToChannelField id={channelId} /> 115 + <Composer.Footer> 116 + <Composer.Formatting /> 117 + <Composer.Emojis /> 118 + <Composer.Submit /> 119 + </Composer.Footer> 120 + </Composer.Frame> 121 + ) 122 + } 123 + 124 + // Edit composer - different footer actions 125 + function EditComposer() { 126 + return ( 127 + <Composer.Frame> 128 + <Composer.Input /> 129 + <Composer.Footer> 130 + <Composer.Formatting /> 131 + <Composer.Emojis /> 132 + <Composer.CancelEdit /> 133 + <Composer.SaveEdit /> 134 + </Composer.Footer> 135 + </Composer.Frame> 136 + ) 137 + } 138 + ``` 139 + 140 + Each variant is explicit about what it renders. We can share internals without 141 + 142 + sharing a single monolithic parent. 143 + 144 + ### 1.2 Use Compound Components 145 + 146 + **Impact: HIGH (enables flexible composition without prop drilling)** 147 + 148 + Structure complex components as compound components with a shared context. Each 149 + 150 + subcomponent accesses shared state via context, not props. Consumers compose the 151 + 152 + pieces they need. 153 + 154 + **Incorrect: monolithic component with render props** 155 + 156 + ```tsx 157 + function Composer({ 158 + renderHeader, 159 + renderFooter, 160 + renderActions, 161 + showAttachments, 162 + showFormatting, 163 + showEmojis, 164 + }: Props) { 165 + return ( 166 + <form> 167 + {renderHeader?.()} 168 + <Input /> 169 + {showAttachments && <Attachments />} 170 + {renderFooter ? ( 171 + renderFooter() 172 + ) : ( 173 + <Footer> 174 + {showFormatting && <Formatting />} 175 + {showEmojis && <Emojis />} 176 + {renderActions?.()} 177 + </Footer> 178 + )} 179 + </form> 180 + ) 181 + } 182 + ``` 183 + 184 + **Correct: compound components with shared context** 185 + 186 + ```tsx 187 + const ComposerContext = createContext<ComposerContextValue | null>(null) 188 + 189 + function ComposerProvider({ children, state, actions, meta }: ProviderProps) { 190 + return ( 191 + <ComposerContext value={{ state, actions, meta }}> 192 + {children} 193 + </ComposerContext> 194 + ) 195 + } 196 + 197 + function ComposerFrame({ children }: { children: React.ReactNode }) { 198 + return <form>{children}</form> 199 + } 200 + 201 + function ComposerInput() { 202 + const { 203 + state, 204 + actions: { update }, 205 + meta: { inputRef }, 206 + } = use(ComposerContext) 207 + return ( 208 + <TextInput 209 + ref={inputRef} 210 + value={state.input} 211 + onChangeText={(text) => update((s) => ({ ...s, input: text }))} 212 + /> 213 + ) 214 + } 215 + 216 + function ComposerSubmit() { 217 + const { 218 + actions: { submit }, 219 + } = use(ComposerContext) 220 + return <Button onPress={submit}>Send</Button> 221 + } 222 + 223 + // Export as compound component 224 + const Composer = { 225 + Provider: ComposerProvider, 226 + Frame: ComposerFrame, 227 + Input: ComposerInput, 228 + Submit: ComposerSubmit, 229 + Header: ComposerHeader, 230 + Footer: ComposerFooter, 231 + Attachments: ComposerAttachments, 232 + Formatting: ComposerFormatting, 233 + Emojis: ComposerEmojis, 234 + } 235 + ``` 236 + 237 + **Usage:** 238 + 239 + ```tsx 240 + <Composer.Provider state={state} actions={actions} meta={meta}> 241 + <Composer.Frame> 242 + <Composer.Header /> 243 + <Composer.Input /> 244 + <Composer.Footer> 245 + <Composer.Formatting /> 246 + <Composer.Submit /> 247 + </Composer.Footer> 248 + </Composer.Frame> 249 + </Composer.Provider> 250 + ``` 251 + 252 + Consumers explicitly compose exactly what they need. No hidden conditionals. And the state, actions and meta are dependency-injected by a parent provider, allowing multiple usages of the same component structure. 253 + 254 + --- 255 + 256 + ## 2. State Management 257 + 258 + **Impact: MEDIUM** 259 + 260 + Patterns for lifting state and managing shared context across 261 + composed components. 262 + 263 + ### 2.1 Decouple State Management from UI 264 + 265 + **Impact: MEDIUM (enables swapping state implementations without changing UI)** 266 + 267 + The provider component should be the only place that knows how state is managed. 268 + 269 + UI components consume the context interface—they don't know if state comes from 270 + 271 + useState, Zustand, or a server sync. 272 + 273 + **Incorrect: UI coupled to state implementation** 274 + 275 + ```tsx 276 + function ChannelComposer({ channelId }: { channelId: string }) { 277 + // UI component knows about global state implementation 278 + const state = useGlobalChannelState(channelId) 279 + const { submit, updateInput } = useChannelSync(channelId) 280 + 281 + return ( 282 + <Composer.Frame> 283 + <Composer.Input 284 + value={state.input} 285 + onChange={(text) => sync.updateInput(text)} 286 + /> 287 + <Composer.Submit onPress={() => sync.submit()} /> 288 + </Composer.Frame> 289 + ) 290 + } 291 + ``` 292 + 293 + **Correct: state management isolated in provider** 294 + 295 + ```tsx 296 + // Provider handles all state management details 297 + function ChannelProvider({ 298 + channelId, 299 + children, 300 + }: { 301 + channelId: string 302 + children: React.ReactNode 303 + }) { 304 + const { state, update, submit } = useGlobalChannel(channelId) 305 + const inputRef = useRef(null) 306 + 307 + return ( 308 + <Composer.Provider 309 + state={state} 310 + actions={{ update, submit }} 311 + meta={{ inputRef }} 312 + > 313 + {children} 314 + </Composer.Provider> 315 + ) 316 + } 317 + 318 + // UI component only knows about the context interface 319 + function ChannelComposer() { 320 + return ( 321 + <Composer.Frame> 322 + <Composer.Header /> 323 + <Composer.Input /> 324 + <Composer.Footer> 325 + <Composer.Submit /> 326 + </Composer.Footer> 327 + </Composer.Frame> 328 + ) 329 + } 330 + 331 + // Usage 332 + function Channel({ channelId }: { channelId: string }) { 333 + return ( 334 + <ChannelProvider channelId={channelId}> 335 + <ChannelComposer /> 336 + </ChannelProvider> 337 + ) 338 + } 339 + ``` 340 + 341 + **Different providers, same UI:** 342 + 343 + ```tsx 344 + // Local state for ephemeral forms 345 + function ForwardMessageProvider({ children }) { 346 + const [state, setState] = useState(initialState) 347 + const forwardMessage = useForwardMessage() 348 + 349 + return ( 350 + <Composer.Provider 351 + state={state} 352 + actions={{ update: setState, submit: forwardMessage }} 353 + > 354 + {children} 355 + </Composer.Provider> 356 + ) 357 + } 358 + 359 + // Global synced state for channels 360 + function ChannelProvider({ channelId, children }) { 361 + const { state, update, submit } = useGlobalChannel(channelId) 362 + 363 + return ( 364 + <Composer.Provider state={state} actions={{ update, submit }}> 365 + {children} 366 + </Composer.Provider> 367 + ) 368 + } 369 + ``` 370 + 371 + The same `Composer.Input` component works with both providers because it only 372 + 373 + depends on the context interface, not the implementation. 374 + 375 + ### 2.2 Define Generic Context Interfaces for Dependency Injection 376 + 377 + **Impact: HIGH (enables dependency-injectable state across use-cases)** 378 + 379 + Define a **generic interface** for your component context with three parts: 380 + 381 + `state`, `actions`, and `meta`. This interface is a contract that any provider 382 + 383 + can implement—enabling the same UI components to work with completely different 384 + 385 + state implementations. 386 + 387 + **Core principle:** Lift state, compose internals, make state 388 + 389 + dependency-injectable. 390 + 391 + **Incorrect: UI coupled to specific state implementation** 392 + 393 + ```tsx 394 + function ComposerInput() { 395 + // Tightly coupled to a specific hook 396 + const { input, setInput } = useChannelComposerState() 397 + return <TextInput value={input} onChangeText={setInput} /> 398 + } 399 + ``` 400 + 401 + **Correct: generic interface enables dependency injection** 402 + 403 + ```tsx 404 + // Define a GENERIC interface that any provider can implement 405 + interface ComposerState { 406 + input: string 407 + attachments: Attachment[] 408 + isSubmitting: boolean 409 + } 410 + 411 + interface ComposerActions { 412 + update: (updater: (state: ComposerState) => ComposerState) => void 413 + submit: () => void 414 + } 415 + 416 + interface ComposerMeta { 417 + inputRef: React.RefObject<TextInput> 418 + } 419 + 420 + interface ComposerContextValue { 421 + state: ComposerState 422 + actions: ComposerActions 423 + meta: ComposerMeta 424 + } 425 + 426 + const ComposerContext = createContext<ComposerContextValue | null>(null) 427 + ``` 428 + 429 + **UI components consume the interface, not the implementation:** 430 + 431 + ```tsx 432 + function ComposerInput() { 433 + const { 434 + state, 435 + actions: { update }, 436 + meta, 437 + } = use(ComposerContext) 438 + 439 + // This component works with ANY provider that implements the interface 440 + return ( 441 + <TextInput 442 + ref={meta.inputRef} 443 + value={state.input} 444 + onChangeText={(text) => update((s) => ({ ...s, input: text }))} 445 + /> 446 + ) 447 + } 448 + ``` 449 + 450 + **Different providers implement the same interface:** 451 + 452 + ```tsx 453 + // Provider A: Local state for ephemeral forms 454 + function ForwardMessageProvider({ children }: { children: React.ReactNode }) { 455 + const [state, setState] = useState(initialState) 456 + const inputRef = useRef(null) 457 + const submit = useForwardMessage() 458 + 459 + return ( 460 + <ComposerContext 461 + value={{ 462 + state, 463 + actions: { update: setState, submit }, 464 + meta: { inputRef }, 465 + }} 466 + > 467 + {children} 468 + </ComposerContext> 469 + ) 470 + } 471 + 472 + // Provider B: Global synced state for channels 473 + function ChannelProvider({ channelId, children }: Props) { 474 + const { state, update, submit } = useGlobalChannel(channelId) 475 + const inputRef = useRef(null) 476 + 477 + return ( 478 + <ComposerContext 479 + value={{ 480 + state, 481 + actions: { update, submit }, 482 + meta: { inputRef }, 483 + }} 484 + > 485 + {children} 486 + </ComposerContext> 487 + ) 488 + } 489 + ``` 490 + 491 + **The same composed UI works with both:** 492 + 493 + ```tsx 494 + // Works with ForwardMessageProvider (local state) 495 + <ForwardMessageProvider> 496 + <Composer.Frame> 497 + <Composer.Input /> 498 + <Composer.Submit /> 499 + </Composer.Frame> 500 + </ForwardMessageProvider> 501 + 502 + // Works with ChannelProvider (global synced state) 503 + <ChannelProvider channelId="abc"> 504 + <Composer.Frame> 505 + <Composer.Input /> 506 + <Composer.Submit /> 507 + </Composer.Frame> 508 + </ChannelProvider> 509 + ``` 510 + 511 + **Custom UI outside the component can access state and actions:** 512 + 513 + ```tsx 514 + function ForwardMessageDialog() { 515 + return ( 516 + <ForwardMessageProvider> 517 + <Dialog> 518 + {/* The composer UI */} 519 + <Composer.Frame> 520 + <Composer.Input placeholder="Add a message, if you'd like." /> 521 + <Composer.Footer> 522 + <Composer.Formatting /> 523 + <Composer.Emojis /> 524 + </Composer.Footer> 525 + </Composer.Frame> 526 + 527 + {/* Custom UI OUTSIDE the composer, but INSIDE the provider */} 528 + <MessagePreview /> 529 + 530 + {/* Actions at the bottom of the dialog */} 531 + <DialogActions> 532 + <CancelButton /> 533 + <ForwardButton /> 534 + </DialogActions> 535 + </Dialog> 536 + </ForwardMessageProvider> 537 + ) 538 + } 539 + 540 + // This button lives OUTSIDE Composer.Frame but can still submit based on its context! 541 + function ForwardButton() { 542 + const { 543 + actions: { submit }, 544 + } = use(ComposerContext) 545 + return <Button onPress={submit}>Forward</Button> 546 + } 547 + 548 + // This preview lives OUTSIDE Composer.Frame but can read composer's state! 549 + function MessagePreview() { 550 + const { state } = use(ComposerContext) 551 + return <Preview message={state.input} attachments={state.attachments} /> 552 + } 553 + ``` 554 + 555 + The provider boundary is what matters—not the visual nesting. Components that 556 + 557 + need shared state don't have to be inside the `Composer.Frame`. They just need 558 + 559 + to be within the provider. 560 + 561 + The `ForwardButton` and `MessagePreview` are not visually inside the composer 562 + 563 + box, but they can still access its state and actions. This is the power of 564 + 565 + lifting state into providers. 566 + 567 + The UI is reusable bits you compose together. The state is dependency-injected 568 + 569 + by the provider. Swap the provider, keep the UI. 570 + 571 + ### 2.3 Lift State into Provider Components 572 + 573 + **Impact: HIGH (enables state sharing outside component boundaries)** 574 + 575 + Move state management into dedicated provider components. This allows sibling 576 + 577 + components outside the main UI to access and modify state without prop drilling 578 + 579 + or awkward refs. 580 + 581 + **Incorrect: state trapped inside component** 582 + 583 + ```tsx 584 + function ForwardMessageComposer() { 585 + const [state, setState] = useState(initialState) 586 + const forwardMessage = useForwardMessage() 587 + 588 + return ( 589 + <Composer.Frame> 590 + <Composer.Input /> 591 + <Composer.Footer /> 592 + </Composer.Frame> 593 + ) 594 + } 595 + 596 + // Problem: How does this button access composer state? 597 + function ForwardMessageDialog() { 598 + return ( 599 + <Dialog> 600 + <ForwardMessageComposer /> 601 + <MessagePreview /> {/* Needs composer state */} 602 + <DialogActions> 603 + <CancelButton /> 604 + <ForwardButton /> {/* Needs to call submit */} 605 + </DialogActions> 606 + </Dialog> 607 + ) 608 + } 609 + ``` 610 + 611 + **Incorrect: useEffect to sync state up** 612 + 613 + ```tsx 614 + function ForwardMessageDialog() { 615 + const [input, setInput] = useState('') 616 + return ( 617 + <Dialog> 618 + <ForwardMessageComposer onInputChange={setInput} /> 619 + <MessagePreview input={input} /> 620 + </Dialog> 621 + ) 622 + } 623 + 624 + function ForwardMessageComposer({ onInputChange }) { 625 + const [state, setState] = useState(initialState) 626 + useEffect(() => { 627 + onInputChange(state.input) // Sync on every change 😬 628 + }, [state.input]) 629 + } 630 + ``` 631 + 632 + **Incorrect: reading state from ref on submit** 633 + 634 + ```tsx 635 + function ForwardMessageDialog() { 636 + const stateRef = useRef(null) 637 + return ( 638 + <Dialog> 639 + <ForwardMessageComposer stateRef={stateRef} /> 640 + <ForwardButton onPress={() => submit(stateRef.current)} /> 641 + </Dialog> 642 + ) 643 + } 644 + ``` 645 + 646 + **Correct: state lifted to provider** 647 + 648 + ```tsx 649 + function ForwardMessageProvider({ children }: { children: React.ReactNode }) { 650 + const [state, setState] = useState(initialState) 651 + const forwardMessage = useForwardMessage() 652 + const inputRef = useRef(null) 653 + 654 + return ( 655 + <Composer.Provider 656 + state={state} 657 + actions={{ update: setState, submit: forwardMessage }} 658 + meta={{ inputRef }} 659 + > 660 + {children} 661 + </Composer.Provider> 662 + ) 663 + } 664 + 665 + function ForwardMessageDialog() { 666 + return ( 667 + <ForwardMessageProvider> 668 + <Dialog> 669 + <ForwardMessageComposer /> 670 + <MessagePreview /> {/* Custom components can access state and actions */} 671 + <DialogActions> 672 + <CancelButton /> 673 + <ForwardButton /> {/* Custom components can access state and actions */} 674 + </DialogActions> 675 + </Dialog> 676 + </ForwardMessageProvider> 677 + ) 678 + } 679 + 680 + function ForwardButton() { 681 + const { actions } = use(Composer.Context) 682 + return <Button onPress={actions.submit}>Forward</Button> 683 + } 684 + ``` 685 + 686 + The ForwardButton lives outside the Composer.Frame but still has access to the 687 + 688 + submit action because it's within the provider. Even though it's a one-off 689 + 690 + component, it can still access the composer's state and actions from outside the 691 + 692 + UI itself. 693 + 694 + **Key insight:** Components that need shared state don't have to be visually 695 + 696 + nested inside each other—they just need to be within the same provider. 697 + 698 + --- 699 + 700 + ## 3. Implementation Patterns 701 + 702 + **Impact: MEDIUM** 703 + 704 + Specific techniques for implementing compound components and 705 + context providers. 706 + 707 + ### 3.1 Create Explicit Component Variants 708 + 709 + **Impact: MEDIUM (self-documenting code, no hidden conditionals)** 710 + 711 + Instead of one component with many boolean props, create explicit variant 712 + 713 + components. Each variant composes the pieces it needs. The code documents 714 + 715 + itself. 716 + 717 + **Incorrect: one component, many modes** 718 + 719 + ```tsx 720 + // What does this component actually render? 721 + <Composer 722 + isThread 723 + isEditing={false} 724 + channelId='abc' 725 + showAttachments 726 + showFormatting={false} 727 + /> 728 + ``` 729 + 730 + **Correct: explicit variants** 731 + 732 + ```tsx 733 + // Immediately clear what this renders 734 + <ThreadComposer channelId="abc" /> 735 + 736 + // Or 737 + <EditMessageComposer messageId="xyz" /> 738 + 739 + // Or 740 + <ForwardMessageComposer messageId="123" /> 741 + ``` 742 + 743 + Each implementation is unique, explicit and self-contained. Yet they can each 744 + 745 + use shared parts. 746 + 747 + **Implementation:** 748 + 749 + ```tsx 750 + function ThreadComposer({ channelId }: { channelId: string }) { 751 + return ( 752 + <ThreadProvider channelId={channelId}> 753 + <Composer.Frame> 754 + <Composer.Input /> 755 + <AlsoSendToChannelField channelId={channelId} /> 756 + <Composer.Footer> 757 + <Composer.Formatting /> 758 + <Composer.Emojis /> 759 + <Composer.Submit /> 760 + </Composer.Footer> 761 + </Composer.Frame> 762 + </ThreadProvider> 763 + ) 764 + } 765 + 766 + function EditMessageComposer({ messageId }: { messageId: string }) { 767 + return ( 768 + <EditMessageProvider messageId={messageId}> 769 + <Composer.Frame> 770 + <Composer.Input /> 771 + <Composer.Footer> 772 + <Composer.Formatting /> 773 + <Composer.Emojis /> 774 + <Composer.CancelEdit /> 775 + <Composer.SaveEdit /> 776 + </Composer.Footer> 777 + </Composer.Frame> 778 + </EditMessageProvider> 779 + ) 780 + } 781 + 782 + function ForwardMessageComposer({ messageId }: { messageId: string }) { 783 + return ( 784 + <ForwardMessageProvider messageId={messageId}> 785 + <Composer.Frame> 786 + <Composer.Input placeholder="Add a message, if you'd like." /> 787 + <Composer.Footer> 788 + <Composer.Formatting /> 789 + <Composer.Emojis /> 790 + <Composer.Mentions /> 791 + </Composer.Footer> 792 + </Composer.Frame> 793 + </ForwardMessageProvider> 794 + ) 795 + } 796 + ``` 797 + 798 + Each variant is explicit about: 799 + 800 + - What provider/state it uses 801 + 802 + - What UI elements it includes 803 + 804 + - What actions are available 805 + 806 + No boolean prop combinations to reason about. No impossible states. 807 + 808 + ### 3.2 Prefer Composing Children Over Render Props 809 + 810 + **Impact: MEDIUM (cleaner composition, better readability)** 811 + 812 + Use `children` for composition instead of `renderX` props. Children are more 813 + 814 + readable, compose naturally, and don't require understanding callback 815 + 816 + signatures. 817 + 818 + **Incorrect: render props** 819 + 820 + ```tsx 821 + function Composer({ 822 + renderHeader, 823 + renderFooter, 824 + renderActions, 825 + }: { 826 + renderHeader?: () => React.ReactNode 827 + renderFooter?: () => React.ReactNode 828 + renderActions?: () => React.ReactNode 829 + }) { 830 + return ( 831 + <form> 832 + {renderHeader?.()} 833 + <Input /> 834 + {renderFooter ? renderFooter() : <DefaultFooter />} 835 + {renderActions?.()} 836 + </form> 837 + ) 838 + } 839 + 840 + // Usage is awkward and inflexible 841 + return ( 842 + <Composer 843 + renderHeader={() => <CustomHeader />} 844 + renderFooter={() => ( 845 + <> 846 + <Formatting /> 847 + <Emojis /> 848 + </> 849 + )} 850 + renderActions={() => <SubmitButton />} 851 + /> 852 + ) 853 + ``` 854 + 855 + **Correct: compound components with children** 856 + 857 + ```tsx 858 + function ComposerFrame({ children }: { children: React.ReactNode }) { 859 + return <form>{children}</form> 860 + } 861 + 862 + function ComposerFooter({ children }: { children: React.ReactNode }) { 863 + return <footer className='flex'>{children}</footer> 864 + } 865 + 866 + // Usage is flexible 867 + return ( 868 + <Composer.Frame> 869 + <CustomHeader /> 870 + <Composer.Input /> 871 + <Composer.Footer> 872 + <Composer.Formatting /> 873 + <Composer.Emojis /> 874 + <SubmitButton /> 875 + </Composer.Footer> 876 + </Composer.Frame> 877 + ) 878 + ``` 879 + 880 + **When render props are appropriate:** 881 + 882 + ```tsx 883 + // Render props work well when you need to pass data back 884 + <List 885 + data={items} 886 + renderItem={({ item, index }) => <Item item={item} index={index} />} 887 + /> 888 + ``` 889 + 890 + Use render props when the parent needs to provide data or state to the child. 891 + 892 + Use children when composing static structure. 893 + 894 + --- 895 + 896 + ## 4. React 19 APIs 897 + 898 + **Impact: MEDIUM** 899 + 900 + React 19+ only. Don't use `forwardRef`; use `use()` instead of `useContext()`. 901 + 902 + ### 4.1 React 19 API Changes 903 + 904 + **Impact: MEDIUM (cleaner component definitions and context usage)** 905 + 906 + > **⚠️ React 19+ only.** Skip this if you're on React 18 or earlier. 907 + 908 + In React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `use()` replaces `useContext()`. 909 + 910 + **Incorrect: forwardRef in React 19** 911 + 912 + ```tsx 913 + const ComposerInput = forwardRef<TextInput, Props>((props, ref) => { 914 + return <TextInput ref={ref} {...props} /> 915 + }) 916 + ``` 917 + 918 + **Correct: ref as a regular prop** 919 + 920 + ```tsx 921 + function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) { 922 + return <TextInput ref={ref} {...props} /> 923 + } 924 + ``` 925 + 926 + **Incorrect: useContext in React 19** 927 + 928 + ```tsx 929 + const value = useContext(MyContext) 930 + ``` 931 + 932 + **Correct: use instead of useContext** 933 + 934 + ```tsx 935 + const value = use(MyContext) 936 + ``` 937 + 938 + `use()` can also be called conditionally, unlike `useContext()`. 939 + 940 + --- 941 + 942 + ## References 943 + 944 + 1. [https://react.dev](https://react.dev) 945 + 2. [https://react.dev/learn/passing-data-deeply-with-context](https://react.dev/learn/passing-data-deeply-with-context) 946 + 3. [https://react.dev/reference/react/use](https://react.dev/reference/react/use)
+89
.agents/skills/vercel-composition-patterns/SKILL.md
··· 1 + --- 2 + name: vercel-composition-patterns 3 + description: 4 + React composition patterns that scale. Use when refactoring components with 5 + boolean prop proliferation, building flexible component libraries, or 6 + designing reusable APIs. Triggers on tasks involving compound components, 7 + render props, context providers, or component architecture. Includes React 19 8 + API changes. 9 + license: MIT 10 + metadata: 11 + author: vercel 12 + version: '1.0.0' 13 + --- 14 + 15 + # React Composition Patterns 16 + 17 + Composition patterns for building flexible, maintainable React components. Avoid 18 + boolean prop proliferation by using compound components, lifting state, and 19 + composing internals. These patterns make codebases easier for both humans and AI 20 + agents to work with as they scale. 21 + 22 + ## When to Apply 23 + 24 + Reference these guidelines when: 25 + 26 + - Refactoring components with many boolean props 27 + - Building reusable component libraries 28 + - Designing flexible component APIs 29 + - Reviewing component architecture 30 + - Working with compound components or context providers 31 + 32 + ## Rule Categories by Priority 33 + 34 + | Priority | Category | Impact | Prefix | 35 + | -------- | ----------------------- | ------ | --------------- | 36 + | 1 | Component Architecture | HIGH | `architecture-` | 37 + | 2 | State Management | MEDIUM | `state-` | 38 + | 3 | Implementation Patterns | MEDIUM | `patterns-` | 39 + | 4 | React 19 APIs | MEDIUM | `react19-` | 40 + 41 + ## Quick Reference 42 + 43 + ### 1. Component Architecture (HIGH) 44 + 45 + - `architecture-avoid-boolean-props` - Don't add boolean props to customize 46 + behavior; use composition 47 + - `architecture-compound-components` - Structure complex components with shared 48 + context 49 + 50 + ### 2. State Management (MEDIUM) 51 + 52 + - `state-decouple-implementation` - Provider is the only place that knows how 53 + state is managed 54 + - `state-context-interface` - Define generic interface with state, actions, meta 55 + for dependency injection 56 + - `state-lift-state` - Move state into provider components for sibling access 57 + 58 + ### 3. Implementation Patterns (MEDIUM) 59 + 60 + - `patterns-explicit-variants` - Create explicit variant components instead of 61 + boolean modes 62 + - `patterns-children-over-render-props` - Use children for composition instead 63 + of renderX props 64 + 65 + ### 4. React 19 APIs (MEDIUM) 66 + 67 + > **⚠️ React 19+ only.** Skip this section if using React 18 or earlier. 68 + 69 + - `react19-no-forwardref` - Don't use `forwardRef`; use `use()` instead of `useContext()` 70 + 71 + ## How to Use 72 + 73 + Read individual rule files for detailed explanations and code examples: 74 + 75 + ``` 76 + rules/architecture-avoid-boolean-props.md 77 + rules/state-context-interface.md 78 + ``` 79 + 80 + Each rule file contains: 81 + 82 + - Brief explanation of why it matters 83 + - Incorrect code example with explanation 84 + - Correct code example with explanation 85 + - Additional context and references 86 + 87 + ## Full Compiled Document 88 + 89 + For the complete guide with all rules expanded: `AGENTS.md`
+100
.agents/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md
··· 1 + --- 2 + title: Avoid Boolean Prop Proliferation 3 + impact: CRITICAL 4 + impactDescription: prevents unmaintainable component variants 5 + tags: composition, props, architecture 6 + --- 7 + 8 + ## Avoid Boolean Prop Proliferation 9 + 10 + Don't add boolean props like `isThread`, `isEditing`, `isDMThread` to customize 11 + component behavior. Each boolean doubles possible states and creates 12 + unmaintainable conditional logic. Use composition instead. 13 + 14 + **Incorrect (boolean props create exponential complexity):** 15 + 16 + ```tsx 17 + function Composer({ 18 + onSubmit, 19 + isThread, 20 + channelId, 21 + isDMThread, 22 + dmId, 23 + isEditing, 24 + isForwarding, 25 + }: Props) { 26 + return ( 27 + <form> 28 + <Header /> 29 + <Input /> 30 + {isDMThread ? ( 31 + <AlsoSendToDMField id={dmId} /> 32 + ) : isThread ? ( 33 + <AlsoSendToChannelField id={channelId} /> 34 + ) : null} 35 + {isEditing ? ( 36 + <EditActions /> 37 + ) : isForwarding ? ( 38 + <ForwardActions /> 39 + ) : ( 40 + <DefaultActions /> 41 + )} 42 + <Footer onSubmit={onSubmit} /> 43 + </form> 44 + ) 45 + } 46 + ``` 47 + 48 + **Correct (composition eliminates conditionals):** 49 + 50 + ```tsx 51 + // Channel composer 52 + function ChannelComposer() { 53 + return ( 54 + <Composer.Frame> 55 + <Composer.Header /> 56 + <Composer.Input /> 57 + <Composer.Footer> 58 + <Composer.Attachments /> 59 + <Composer.Formatting /> 60 + <Composer.Emojis /> 61 + <Composer.Submit /> 62 + </Composer.Footer> 63 + </Composer.Frame> 64 + ) 65 + } 66 + 67 + // Thread composer - adds "also send to channel" field 68 + function ThreadComposer({ channelId }: { channelId: string }) { 69 + return ( 70 + <Composer.Frame> 71 + <Composer.Header /> 72 + <Composer.Input /> 73 + <AlsoSendToChannelField id={channelId} /> 74 + <Composer.Footer> 75 + <Composer.Formatting /> 76 + <Composer.Emojis /> 77 + <Composer.Submit /> 78 + </Composer.Footer> 79 + </Composer.Frame> 80 + ) 81 + } 82 + 83 + // Edit composer - different footer actions 84 + function EditComposer() { 85 + return ( 86 + <Composer.Frame> 87 + <Composer.Input /> 88 + <Composer.Footer> 89 + <Composer.Formatting /> 90 + <Composer.Emojis /> 91 + <Composer.CancelEdit /> 92 + <Composer.SaveEdit /> 93 + </Composer.Footer> 94 + </Composer.Frame> 95 + ) 96 + } 97 + ``` 98 + 99 + Each variant is explicit about what it renders. We can share internals without 100 + sharing a single monolithic parent.
+112
.agents/skills/vercel-composition-patterns/rules/architecture-compound-components.md
··· 1 + --- 2 + title: Use Compound Components 3 + impact: HIGH 4 + impactDescription: enables flexible composition without prop drilling 5 + tags: composition, compound-components, architecture 6 + --- 7 + 8 + ## Use Compound Components 9 + 10 + Structure complex components as compound components with a shared context. Each 11 + subcomponent accesses shared state via context, not props. Consumers compose the 12 + pieces they need. 13 + 14 + **Incorrect (monolithic component with render props):** 15 + 16 + ```tsx 17 + function Composer({ 18 + renderHeader, 19 + renderFooter, 20 + renderActions, 21 + showAttachments, 22 + showFormatting, 23 + showEmojis, 24 + }: Props) { 25 + return ( 26 + <form> 27 + {renderHeader?.()} 28 + <Input /> 29 + {showAttachments && <Attachments />} 30 + {renderFooter ? ( 31 + renderFooter() 32 + ) : ( 33 + <Footer> 34 + {showFormatting && <Formatting />} 35 + {showEmojis && <Emojis />} 36 + {renderActions?.()} 37 + </Footer> 38 + )} 39 + </form> 40 + ) 41 + } 42 + ``` 43 + 44 + **Correct (compound components with shared context):** 45 + 46 + ```tsx 47 + const ComposerContext = createContext<ComposerContextValue | null>(null) 48 + 49 + function ComposerProvider({ children, state, actions, meta }: ProviderProps) { 50 + return ( 51 + <ComposerContext value={{ state, actions, meta }}> 52 + {children} 53 + </ComposerContext> 54 + ) 55 + } 56 + 57 + function ComposerFrame({ children }: { children: React.ReactNode }) { 58 + return <form>{children}</form> 59 + } 60 + 61 + function ComposerInput() { 62 + const { 63 + state, 64 + actions: { update }, 65 + meta: { inputRef }, 66 + } = use(ComposerContext) 67 + return ( 68 + <TextInput 69 + ref={inputRef} 70 + value={state.input} 71 + onChangeText={(text) => update((s) => ({ ...s, input: text }))} 72 + /> 73 + ) 74 + } 75 + 76 + function ComposerSubmit() { 77 + const { 78 + actions: { submit }, 79 + } = use(ComposerContext) 80 + return <Button onPress={submit}>Send</Button> 81 + } 82 + 83 + // Export as compound component 84 + const Composer = { 85 + Provider: ComposerProvider, 86 + Frame: ComposerFrame, 87 + Input: ComposerInput, 88 + Submit: ComposerSubmit, 89 + Header: ComposerHeader, 90 + Footer: ComposerFooter, 91 + Attachments: ComposerAttachments, 92 + Formatting: ComposerFormatting, 93 + Emojis: ComposerEmojis, 94 + } 95 + ``` 96 + 97 + **Usage:** 98 + 99 + ```tsx 100 + <Composer.Provider state={state} actions={actions} meta={meta}> 101 + <Composer.Frame> 102 + <Composer.Header /> 103 + <Composer.Input /> 104 + <Composer.Footer> 105 + <Composer.Formatting /> 106 + <Composer.Submit /> 107 + </Composer.Footer> 108 + </Composer.Frame> 109 + </Composer.Provider> 110 + ``` 111 + 112 + Consumers explicitly compose exactly what they need. No hidden conditionals. And the state, actions and meta are dependency-injected by a parent provider, allowing multiple usages of the same component structure.
+87
.agents/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md
··· 1 + --- 2 + title: Prefer Composing Children Over Render Props 3 + impact: MEDIUM 4 + impactDescription: cleaner composition, better readability 5 + tags: composition, children, render-props 6 + --- 7 + 8 + ## Prefer Children Over Render Props 9 + 10 + Use `children` for composition instead of `renderX` props. Children are more 11 + readable, compose naturally, and don't require understanding callback 12 + signatures. 13 + 14 + **Incorrect (render props):** 15 + 16 + ```tsx 17 + function Composer({ 18 + renderHeader, 19 + renderFooter, 20 + renderActions, 21 + }: { 22 + renderHeader?: () => React.ReactNode 23 + renderFooter?: () => React.ReactNode 24 + renderActions?: () => React.ReactNode 25 + }) { 26 + return ( 27 + <form> 28 + {renderHeader?.()} 29 + <Input /> 30 + {renderFooter ? renderFooter() : <DefaultFooter />} 31 + {renderActions?.()} 32 + </form> 33 + ) 34 + } 35 + 36 + // Usage is awkward and inflexible 37 + return ( 38 + <Composer 39 + renderHeader={() => <CustomHeader />} 40 + renderFooter={() => ( 41 + <> 42 + <Formatting /> 43 + <Emojis /> 44 + </> 45 + )} 46 + renderActions={() => <SubmitButton />} 47 + /> 48 + ) 49 + ``` 50 + 51 + **Correct (compound components with children):** 52 + 53 + ```tsx 54 + function ComposerFrame({ children }: { children: React.ReactNode }) { 55 + return <form>{children}</form> 56 + } 57 + 58 + function ComposerFooter({ children }: { children: React.ReactNode }) { 59 + return <footer className='flex'>{children}</footer> 60 + } 61 + 62 + // Usage is flexible 63 + return ( 64 + <Composer.Frame> 65 + <CustomHeader /> 66 + <Composer.Input /> 67 + <Composer.Footer> 68 + <Composer.Formatting /> 69 + <Composer.Emojis /> 70 + <SubmitButton /> 71 + </Composer.Footer> 72 + </Composer.Frame> 73 + ) 74 + ``` 75 + 76 + **When render props are appropriate:** 77 + 78 + ```tsx 79 + // Render props work well when you need to pass data back 80 + <List 81 + data={items} 82 + renderItem={({ item, index }) => <Item item={item} index={index} />} 83 + /> 84 + ``` 85 + 86 + Use render props when the parent needs to provide data or state to the child. 87 + Use children when composing static structure.
+100
.agents/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md
··· 1 + --- 2 + title: Create Explicit Component Variants 3 + impact: MEDIUM 4 + impactDescription: self-documenting code, no hidden conditionals 5 + tags: composition, variants, architecture 6 + --- 7 + 8 + ## Create Explicit Component Variants 9 + 10 + Instead of one component with many boolean props, create explicit variant 11 + components. Each variant composes the pieces it needs. The code documents 12 + itself. 13 + 14 + **Incorrect (one component, many modes):** 15 + 16 + ```tsx 17 + // What does this component actually render? 18 + <Composer 19 + isThread 20 + isEditing={false} 21 + channelId='abc' 22 + showAttachments 23 + showFormatting={false} 24 + /> 25 + ``` 26 + 27 + **Correct (explicit variants):** 28 + 29 + ```tsx 30 + // Immediately clear what this renders 31 + <ThreadComposer channelId="abc" /> 32 + 33 + // Or 34 + <EditMessageComposer messageId="xyz" /> 35 + 36 + // Or 37 + <ForwardMessageComposer messageId="123" /> 38 + ``` 39 + 40 + Each implementation is unique, explicit and self-contained. Yet they can each 41 + use shared parts. 42 + 43 + **Implementation:** 44 + 45 + ```tsx 46 + function ThreadComposer({ channelId }: { channelId: string }) { 47 + return ( 48 + <ThreadProvider channelId={channelId}> 49 + <Composer.Frame> 50 + <Composer.Input /> 51 + <AlsoSendToChannelField channelId={channelId} /> 52 + <Composer.Footer> 53 + <Composer.Formatting /> 54 + <Composer.Emojis /> 55 + <Composer.Submit /> 56 + </Composer.Footer> 57 + </Composer.Frame> 58 + </ThreadProvider> 59 + ) 60 + } 61 + 62 + function EditMessageComposer({ messageId }: { messageId: string }) { 63 + return ( 64 + <EditMessageProvider messageId={messageId}> 65 + <Composer.Frame> 66 + <Composer.Input /> 67 + <Composer.Footer> 68 + <Composer.Formatting /> 69 + <Composer.Emojis /> 70 + <Composer.CancelEdit /> 71 + <Composer.SaveEdit /> 72 + </Composer.Footer> 73 + </Composer.Frame> 74 + </EditMessageProvider> 75 + ) 76 + } 77 + 78 + function ForwardMessageComposer({ messageId }: { messageId: string }) { 79 + return ( 80 + <ForwardMessageProvider messageId={messageId}> 81 + <Composer.Frame> 82 + <Composer.Input placeholder="Add a message, if you'd like." /> 83 + <Composer.Footer> 84 + <Composer.Formatting /> 85 + <Composer.Emojis /> 86 + <Composer.Mentions /> 87 + </Composer.Footer> 88 + </Composer.Frame> 89 + </ForwardMessageProvider> 90 + ) 91 + } 92 + ``` 93 + 94 + Each variant is explicit about: 95 + 96 + - What provider/state it uses 97 + - What UI elements it includes 98 + - What actions are available 99 + 100 + No boolean prop combinations to reason about. No impossible states.
+42
.agents/skills/vercel-composition-patterns/rules/react19-no-forwardref.md
··· 1 + --- 2 + title: React 19 API Changes 3 + impact: MEDIUM 4 + impactDescription: cleaner component definitions and context usage 5 + tags: react19, refs, context, hooks 6 + --- 7 + 8 + ## React 19 API Changes 9 + 10 + > **⚠️ React 19+ only.** Skip this if you're on React 18 or earlier. 11 + 12 + In React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `use()` replaces `useContext()`. 13 + 14 + **Incorrect (forwardRef in React 19):** 15 + 16 + ```tsx 17 + const ComposerInput = forwardRef<TextInput, Props>((props, ref) => { 18 + return <TextInput ref={ref} {...props} /> 19 + }) 20 + ``` 21 + 22 + **Correct (ref as a regular prop):** 23 + 24 + ```tsx 25 + function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) { 26 + return <TextInput ref={ref} {...props} /> 27 + } 28 + ``` 29 + 30 + **Incorrect (useContext in React 19):** 31 + 32 + ```tsx 33 + const value = useContext(MyContext) 34 + ``` 35 + 36 + **Correct (use instead of useContext):** 37 + 38 + ```tsx 39 + const value = use(MyContext) 40 + ``` 41 + 42 + `use()` can also be called conditionally, unlike `useContext()`.
+191
.agents/skills/vercel-composition-patterns/rules/state-context-interface.md
··· 1 + --- 2 + title: Define Generic Context Interfaces for Dependency Injection 3 + impact: HIGH 4 + impactDescription: enables dependency-injectable state across use-cases 5 + tags: composition, context, state, typescript, dependency-injection 6 + --- 7 + 8 + ## Define Generic Context Interfaces for Dependency Injection 9 + 10 + Define a **generic interface** for your component context with three parts: 11 + `state`, `actions`, and `meta`. This interface is a contract that any provider 12 + can implement—enabling the same UI components to work with completely different 13 + state implementations. 14 + 15 + **Core principle:** Lift state, compose internals, make state 16 + dependency-injectable. 17 + 18 + **Incorrect (UI coupled to specific state implementation):** 19 + 20 + ```tsx 21 + function ComposerInput() { 22 + // Tightly coupled to a specific hook 23 + const { input, setInput } = useChannelComposerState() 24 + return <TextInput value={input} onChangeText={setInput} /> 25 + } 26 + ``` 27 + 28 + **Correct (generic interface enables dependency injection):** 29 + 30 + ```tsx 31 + // Define a GENERIC interface that any provider can implement 32 + interface ComposerState { 33 + input: string 34 + attachments: Attachment[] 35 + isSubmitting: boolean 36 + } 37 + 38 + interface ComposerActions { 39 + update: (updater: (state: ComposerState) => ComposerState) => void 40 + submit: () => void 41 + } 42 + 43 + interface ComposerMeta { 44 + inputRef: React.RefObject<TextInput> 45 + } 46 + 47 + interface ComposerContextValue { 48 + state: ComposerState 49 + actions: ComposerActions 50 + meta: ComposerMeta 51 + } 52 + 53 + const ComposerContext = createContext<ComposerContextValue | null>(null) 54 + ``` 55 + 56 + **UI components consume the interface, not the implementation:** 57 + 58 + ```tsx 59 + function ComposerInput() { 60 + const { 61 + state, 62 + actions: { update }, 63 + meta, 64 + } = use(ComposerContext) 65 + 66 + // This component works with ANY provider that implements the interface 67 + return ( 68 + <TextInput 69 + ref={meta.inputRef} 70 + value={state.input} 71 + onChangeText={(text) => update((s) => ({ ...s, input: text }))} 72 + /> 73 + ) 74 + } 75 + ``` 76 + 77 + **Different providers implement the same interface:** 78 + 79 + ```tsx 80 + // Provider A: Local state for ephemeral forms 81 + function ForwardMessageProvider({ children }: { children: React.ReactNode }) { 82 + const [state, setState] = useState(initialState) 83 + const inputRef = useRef(null) 84 + const submit = useForwardMessage() 85 + 86 + return ( 87 + <ComposerContext 88 + value={{ 89 + state, 90 + actions: { update: setState, submit }, 91 + meta: { inputRef }, 92 + }} 93 + > 94 + {children} 95 + </ComposerContext> 96 + ) 97 + } 98 + 99 + // Provider B: Global synced state for channels 100 + function ChannelProvider({ channelId, children }: Props) { 101 + const { state, update, submit } = useGlobalChannel(channelId) 102 + const inputRef = useRef(null) 103 + 104 + return ( 105 + <ComposerContext 106 + value={{ 107 + state, 108 + actions: { update, submit }, 109 + meta: { inputRef }, 110 + }} 111 + > 112 + {children} 113 + </ComposerContext> 114 + ) 115 + } 116 + ``` 117 + 118 + **The same composed UI works with both:** 119 + 120 + ```tsx 121 + // Works with ForwardMessageProvider (local state) 122 + <ForwardMessageProvider> 123 + <Composer.Frame> 124 + <Composer.Input /> 125 + <Composer.Submit /> 126 + </Composer.Frame> 127 + </ForwardMessageProvider> 128 + 129 + // Works with ChannelProvider (global synced state) 130 + <ChannelProvider channelId="abc"> 131 + <Composer.Frame> 132 + <Composer.Input /> 133 + <Composer.Submit /> 134 + </Composer.Frame> 135 + </ChannelProvider> 136 + ``` 137 + 138 + **Custom UI outside the component can access state and actions:** 139 + 140 + The provider boundary is what matters—not the visual nesting. Components that 141 + need shared state don't have to be inside the `Composer.Frame`. They just need 142 + to be within the provider. 143 + 144 + ```tsx 145 + function ForwardMessageDialog() { 146 + return ( 147 + <ForwardMessageProvider> 148 + <Dialog> 149 + {/* The composer UI */} 150 + <Composer.Frame> 151 + <Composer.Input placeholder="Add a message, if you'd like." /> 152 + <Composer.Footer> 153 + <Composer.Formatting /> 154 + <Composer.Emojis /> 155 + </Composer.Footer> 156 + </Composer.Frame> 157 + 158 + {/* Custom UI OUTSIDE the composer, but INSIDE the provider */} 159 + <MessagePreview /> 160 + 161 + {/* Actions at the bottom of the dialog */} 162 + <DialogActions> 163 + <CancelButton /> 164 + <ForwardButton /> 165 + </DialogActions> 166 + </Dialog> 167 + </ForwardMessageProvider> 168 + ) 169 + } 170 + 171 + // This button lives OUTSIDE Composer.Frame but can still submit based on its context! 172 + function ForwardButton() { 173 + const { 174 + actions: { submit }, 175 + } = use(ComposerContext) 176 + return <Button onPress={submit}>Forward</Button> 177 + } 178 + 179 + // This preview lives OUTSIDE Composer.Frame but can read composer's state! 180 + function MessagePreview() { 181 + const { state } = use(ComposerContext) 182 + return <Preview message={state.input} attachments={state.attachments} /> 183 + } 184 + ``` 185 + 186 + The `ForwardButton` and `MessagePreview` are not visually inside the composer 187 + box, but they can still access its state and actions. This is the power of 188 + lifting state into providers. 189 + 190 + The UI is reusable bits you compose together. The state is dependency-injected 191 + by the provider. Swap the provider, keep the UI.
+113
.agents/skills/vercel-composition-patterns/rules/state-decouple-implementation.md
··· 1 + --- 2 + title: Decouple State Management from UI 3 + impact: MEDIUM 4 + impactDescription: enables swapping state implementations without changing UI 5 + tags: composition, state, architecture 6 + --- 7 + 8 + ## Decouple State Management from UI 9 + 10 + The provider component should be the only place that knows how state is managed. 11 + UI components consume the context interface—they don't know if state comes from 12 + useState, Zustand, or a server sync. 13 + 14 + **Incorrect (UI coupled to state implementation):** 15 + 16 + ```tsx 17 + function ChannelComposer({ channelId }: { channelId: string }) { 18 + // UI component knows about global state implementation 19 + const state = useGlobalChannelState(channelId) 20 + const { submit, updateInput } = useChannelSync(channelId) 21 + 22 + return ( 23 + <Composer.Frame> 24 + <Composer.Input 25 + value={state.input} 26 + onChange={(text) => sync.updateInput(text)} 27 + /> 28 + <Composer.Submit onPress={() => sync.submit()} /> 29 + </Composer.Frame> 30 + ) 31 + } 32 + ``` 33 + 34 + **Correct (state management isolated in provider):** 35 + 36 + ```tsx 37 + // Provider handles all state management details 38 + function ChannelProvider({ 39 + channelId, 40 + children, 41 + }: { 42 + channelId: string 43 + children: React.ReactNode 44 + }) { 45 + const { state, update, submit } = useGlobalChannel(channelId) 46 + const inputRef = useRef(null) 47 + 48 + return ( 49 + <Composer.Provider 50 + state={state} 51 + actions={{ update, submit }} 52 + meta={{ inputRef }} 53 + > 54 + {children} 55 + </Composer.Provider> 56 + ) 57 + } 58 + 59 + // UI component only knows about the context interface 60 + function ChannelComposer() { 61 + return ( 62 + <Composer.Frame> 63 + <Composer.Header /> 64 + <Composer.Input /> 65 + <Composer.Footer> 66 + <Composer.Submit /> 67 + </Composer.Footer> 68 + </Composer.Frame> 69 + ) 70 + } 71 + 72 + // Usage 73 + function Channel({ channelId }: { channelId: string }) { 74 + return ( 75 + <ChannelProvider channelId={channelId}> 76 + <ChannelComposer /> 77 + </ChannelProvider> 78 + ) 79 + } 80 + ``` 81 + 82 + **Different providers, same UI:** 83 + 84 + ```tsx 85 + // Local state for ephemeral forms 86 + function ForwardMessageProvider({ children }) { 87 + const [state, setState] = useState(initialState) 88 + const forwardMessage = useForwardMessage() 89 + 90 + return ( 91 + <Composer.Provider 92 + state={state} 93 + actions={{ update: setState, submit: forwardMessage }} 94 + > 95 + {children} 96 + </Composer.Provider> 97 + ) 98 + } 99 + 100 + // Global synced state for channels 101 + function ChannelProvider({ channelId, children }) { 102 + const { state, update, submit } = useGlobalChannel(channelId) 103 + 104 + return ( 105 + <Composer.Provider state={state} actions={{ update, submit }}> 106 + {children} 107 + </Composer.Provider> 108 + ) 109 + } 110 + ``` 111 + 112 + The same `Composer.Input` component works with both providers because it only 113 + depends on the context interface, not the implementation.
+125
.agents/skills/vercel-composition-patterns/rules/state-lift-state.md
··· 1 + --- 2 + title: Lift State into Provider Components 3 + impact: HIGH 4 + impactDescription: enables state sharing outside component boundaries 5 + tags: composition, state, context, providers 6 + --- 7 + 8 + ## Lift State into Provider Components 9 + 10 + Move state management into dedicated provider components. This allows sibling 11 + components outside the main UI to access and modify state without prop drilling 12 + or awkward refs. 13 + 14 + **Incorrect (state trapped inside component):** 15 + 16 + ```tsx 17 + function ForwardMessageComposer() { 18 + const [state, setState] = useState(initialState) 19 + const forwardMessage = useForwardMessage() 20 + 21 + return ( 22 + <Composer.Frame> 23 + <Composer.Input /> 24 + <Composer.Footer /> 25 + </Composer.Frame> 26 + ) 27 + } 28 + 29 + // Problem: How does this button access composer state? 30 + function ForwardMessageDialog() { 31 + return ( 32 + <Dialog> 33 + <ForwardMessageComposer /> 34 + <MessagePreview /> {/* Needs composer state */} 35 + <DialogActions> 36 + <CancelButton /> 37 + <ForwardButton /> {/* Needs to call submit */} 38 + </DialogActions> 39 + </Dialog> 40 + ) 41 + } 42 + ``` 43 + 44 + **Incorrect (useEffect to sync state up):** 45 + 46 + ```tsx 47 + function ForwardMessageDialog() { 48 + const [input, setInput] = useState('') 49 + return ( 50 + <Dialog> 51 + <ForwardMessageComposer onInputChange={setInput} /> 52 + <MessagePreview input={input} /> 53 + </Dialog> 54 + ) 55 + } 56 + 57 + function ForwardMessageComposer({ onInputChange }) { 58 + const [state, setState] = useState(initialState) 59 + useEffect(() => { 60 + onInputChange(state.input) // Sync on every change 😬 61 + }, [state.input]) 62 + } 63 + ``` 64 + 65 + **Incorrect (reading state from ref on submit):** 66 + 67 + ```tsx 68 + function ForwardMessageDialog() { 69 + const stateRef = useRef(null) 70 + return ( 71 + <Dialog> 72 + <ForwardMessageComposer stateRef={stateRef} /> 73 + <ForwardButton onPress={() => submit(stateRef.current)} /> 74 + </Dialog> 75 + ) 76 + } 77 + ``` 78 + 79 + **Correct (state lifted to provider):** 80 + 81 + ```tsx 82 + function ForwardMessageProvider({ children }: { children: React.ReactNode }) { 83 + const [state, setState] = useState(initialState) 84 + const forwardMessage = useForwardMessage() 85 + const inputRef = useRef(null) 86 + 87 + return ( 88 + <Composer.Provider 89 + state={state} 90 + actions={{ update: setState, submit: forwardMessage }} 91 + meta={{ inputRef }} 92 + > 93 + {children} 94 + </Composer.Provider> 95 + ) 96 + } 97 + 98 + function ForwardMessageDialog() { 99 + return ( 100 + <ForwardMessageProvider> 101 + <Dialog> 102 + <ForwardMessageComposer /> 103 + <MessagePreview /> {/* Custom components can access state and actions */} 104 + <DialogActions> 105 + <CancelButton /> 106 + <ForwardButton /> {/* Custom components can access state and actions */} 107 + </DialogActions> 108 + </Dialog> 109 + </ForwardMessageProvider> 110 + ) 111 + } 112 + 113 + function ForwardButton() { 114 + const { actions } = use(Composer.Context) 115 + return <Button onPress={actions.submit}>Forward</Button> 116 + } 117 + ``` 118 + 119 + The ForwardButton lives outside the Composer.Frame but still has access to the 120 + submit action because it's within the provider. Even though it's a one-off 121 + component, it can still access the composer's state and actions from outside the 122 + UI itself. 123 + 124 + **Key insight:** Components that need shared state don't have to be visually 125 + nested inside each other—they just need to be within the same provider.
+2934
.agents/skills/vercel-react-best-practices/AGENTS.md
··· 1 + # React Best Practices 2 + 3 + **Version 1.0.0** 4 + Vercel Engineering 5 + January 2026 6 + 7 + > **Note:** 8 + > This document is mainly for agents and LLMs to follow when maintaining, 9 + > generating, or refactoring React and Next.js codebases. Humans 10 + > may also find it useful, but guidance here is optimized for automation 11 + > and consistency by AI-assisted workflows. 12 + 13 + --- 14 + 15 + ## Abstract 16 + 17 + 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. 18 + 19 + --- 20 + 21 + ## Table of Contents 22 + 23 + 1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL** 24 + - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed) 25 + - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization) 26 + - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes) 27 + - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations) 28 + - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries) 29 + 2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL** 30 + - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports) 31 + - 2.2 [Conditional Module Loading](#22-conditional-module-loading) 32 + - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries) 33 + - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components) 34 + - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent) 35 + 3. [Server-Side Performance](#3-server-side-performance) — **HIGH** 36 + - 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes) 37 + - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props) 38 + - 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching) 39 + - 3.4 [Minimize Serialization at RSC Boundaries](#34-minimize-serialization-at-rsc-boundaries) 40 + - 3.5 [Parallel Data Fetching with Component Composition](#35-parallel-data-fetching-with-component-composition) 41 + - 3.6 [Per-Request Deduplication with React.cache()](#36-per-request-deduplication-with-reactcache) 42 + - 3.7 [Use after() for Non-Blocking Operations](#37-use-after-for-non-blocking-operations) 43 + 4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH** 44 + - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners) 45 + - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance) 46 + - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication) 47 + - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data) 48 + 5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM** 49 + - 5.1 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering) 50 + - 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point) 51 + - 5.3 [Do not wrap a simple expression with a primitive result type in useMemo](#53-do-not-wrap-a-simple-expression-with-a-primitive-result-type-in-usememo) 52 + - 5.4 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#54-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant) 53 + - 5.5 [Extract to Memoized Components](#55-extract-to-memoized-components) 54 + - 5.6 [Narrow Effect Dependencies](#56-narrow-effect-dependencies) 55 + - 5.7 [Put Interaction Logic in Event Handlers](#57-put-interaction-logic-in-event-handlers) 56 + - 5.8 [Subscribe to Derived State](#58-subscribe-to-derived-state) 57 + - 5.9 [Use Functional setState Updates](#59-use-functional-setstate-updates) 58 + - 5.10 [Use Lazy State Initialization](#510-use-lazy-state-initialization) 59 + - 5.11 [Use Transitions for Non-Urgent Updates](#511-use-transitions-for-non-urgent-updates) 60 + - 5.12 [Use useRef for Transient Values](#512-use-useref-for-transient-values) 61 + 6. [Rendering Performance](#6-rendering-performance) — **MEDIUM** 62 + - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element) 63 + - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists) 64 + - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements) 65 + - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision) 66 + - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering) 67 + - 6.6 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches) 68 + - 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide) 69 + - 6.8 [Use Explicit Conditional Rendering](#68-use-explicit-conditional-rendering) 70 + - 6.9 [Use useTransition Over Manual Loading States](#69-use-usetransition-over-manual-loading-states) 71 + 7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM** 72 + - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing) 73 + - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups) 74 + - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops) 75 + - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls) 76 + - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls) 77 + - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations) 78 + - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons) 79 + - 7.8 [Early Return from Functions](#78-early-return-from-functions) 80 + - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation) 81 + - 7.10 [Use Loop for Min/Max Instead of Sort](#710-use-loop-for-minmax-instead-of-sort) 82 + - 7.11 [Use Set/Map for O(1) Lookups](#711-use-setmap-for-o1-lookups) 83 + - 7.12 [Use toSorted() Instead of sort() for Immutability](#712-use-tosorted-instead-of-sort-for-immutability) 84 + 8. [Advanced Patterns](#8-advanced-patterns) — **LOW** 85 + - 8.1 [Initialize App Once, Not Per Mount](#81-initialize-app-once-not-per-mount) 86 + - 8.2 [Store Event Handlers in Refs](#82-store-event-handlers-in-refs) 87 + - 8.3 [useEffectEvent for Stable Callback Refs](#83-useeffectevent-for-stable-callback-refs) 88 + 89 + --- 90 + 91 + ## 1. Eliminating Waterfalls 92 + 93 + **Impact: CRITICAL** 94 + 95 + Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains. 96 + 97 + ### 1.1 Defer Await Until Needed 98 + 99 + **Impact: HIGH (avoids blocking unused code paths)** 100 + 101 + Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them. 102 + 103 + **Incorrect: blocks both branches** 104 + 105 + ```typescript 106 + async function handleRequest(userId: string, skipProcessing: boolean) { 107 + const userData = await fetchUserData(userId) 108 + 109 + if (skipProcessing) { 110 + // Returns immediately but still waited for userData 111 + return { skipped: true } 112 + } 113 + 114 + // Only this branch uses userData 115 + return processUserData(userData) 116 + } 117 + ``` 118 + 119 + **Correct: only blocks when needed** 120 + 121 + ```typescript 122 + async function handleRequest(userId: string, skipProcessing: boolean) { 123 + if (skipProcessing) { 124 + // Returns immediately without waiting 125 + return { skipped: true } 126 + } 127 + 128 + // Fetch only when needed 129 + const userData = await fetchUserData(userId) 130 + return processUserData(userData) 131 + } 132 + ``` 133 + 134 + **Another example: early return optimization** 135 + 136 + ```typescript 137 + // Incorrect: always fetches permissions 138 + async function updateResource(resourceId: string, userId: string) { 139 + const permissions = await fetchPermissions(userId) 140 + const resource = await getResource(resourceId) 141 + 142 + if (!resource) { 143 + return { error: 'Not found' } 144 + } 145 + 146 + if (!permissions.canEdit) { 147 + return { error: 'Forbidden' } 148 + } 149 + 150 + return await updateResourceData(resource, permissions) 151 + } 152 + 153 + // Correct: fetches only when needed 154 + async function updateResource(resourceId: string, userId: string) { 155 + const resource = await getResource(resourceId) 156 + 157 + if (!resource) { 158 + return { error: 'Not found' } 159 + } 160 + 161 + const permissions = await fetchPermissions(userId) 162 + 163 + if (!permissions.canEdit) { 164 + return { error: 'Forbidden' } 165 + } 166 + 167 + return await updateResourceData(resource, permissions) 168 + } 169 + ``` 170 + 171 + This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive. 172 + 173 + ### 1.2 Dependency-Based Parallelization 174 + 175 + **Impact: CRITICAL (2-10× improvement)** 176 + 177 + For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment. 178 + 179 + **Incorrect: profile waits for config unnecessarily** 180 + 181 + ```typescript 182 + const [user, config] = await Promise.all([ 183 + fetchUser(), 184 + fetchConfig() 185 + ]) 186 + const profile = await fetchProfile(user.id) 187 + ``` 188 + 189 + **Correct: config and profile run in parallel** 190 + 191 + ```typescript 192 + import { all } from 'better-all' 193 + 194 + const { user, config, profile } = await all({ 195 + async user() { return fetchUser() }, 196 + async config() { return fetchConfig() }, 197 + async profile() { 198 + return fetchProfile((await this.$.user).id) 199 + } 200 + }) 201 + ``` 202 + 203 + **Alternative without extra dependencies:** 204 + 205 + ```typescript 206 + const userPromise = fetchUser() 207 + const profilePromise = userPromise.then(user => fetchProfile(user.id)) 208 + 209 + const [user, config, profile] = await Promise.all([ 210 + userPromise, 211 + fetchConfig(), 212 + profilePromise 213 + ]) 214 + ``` 215 + 216 + We can also create all the promises first, and do `Promise.all()` at the end. 217 + 218 + Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all) 219 + 220 + ### 1.3 Prevent Waterfall Chains in API Routes 221 + 222 + **Impact: CRITICAL (2-10× improvement)** 223 + 224 + In API routes and Server Actions, start independent operations immediately, even if you don't await them yet. 225 + 226 + **Incorrect: config waits for auth, data waits for both** 227 + 228 + ```typescript 229 + export async function GET(request: Request) { 230 + const session = await auth() 231 + const config = await fetchConfig() 232 + const data = await fetchData(session.user.id) 233 + return Response.json({ data, config }) 234 + } 235 + ``` 236 + 237 + **Correct: auth and config start immediately** 238 + 239 + ```typescript 240 + export async function GET(request: Request) { 241 + const sessionPromise = auth() 242 + const configPromise = fetchConfig() 243 + const session = await sessionPromise 244 + const [config, data] = await Promise.all([ 245 + configPromise, 246 + fetchData(session.user.id) 247 + ]) 248 + return Response.json({ data, config }) 249 + } 250 + ``` 251 + 252 + For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization). 253 + 254 + ### 1.4 Promise.all() for Independent Operations 255 + 256 + **Impact: CRITICAL (2-10× improvement)** 257 + 258 + When async operations have no interdependencies, execute them concurrently using `Promise.all()`. 259 + 260 + **Incorrect: sequential execution, 3 round trips** 261 + 262 + ```typescript 263 + const user = await fetchUser() 264 + const posts = await fetchPosts() 265 + const comments = await fetchComments() 266 + ``` 267 + 268 + **Correct: parallel execution, 1 round trip** 269 + 270 + ```typescript 271 + const [user, posts, comments] = await Promise.all([ 272 + fetchUser(), 273 + fetchPosts(), 274 + fetchComments() 275 + ]) 276 + ``` 277 + 278 + ### 1.5 Strategic Suspense Boundaries 279 + 280 + **Impact: HIGH (faster initial paint)** 281 + 282 + Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads. 283 + 284 + **Incorrect: wrapper blocked by data fetching** 285 + 286 + ```tsx 287 + async function Page() { 288 + const data = await fetchData() // Blocks entire page 289 + 290 + return ( 291 + <div> 292 + <div>Sidebar</div> 293 + <div>Header</div> 294 + <div> 295 + <DataDisplay data={data} /> 296 + </div> 297 + <div>Footer</div> 298 + </div> 299 + ) 300 + } 301 + ``` 302 + 303 + The entire layout waits for data even though only the middle section needs it. 304 + 305 + **Correct: wrapper shows immediately, data streams in** 306 + 307 + ```tsx 308 + function Page() { 309 + return ( 310 + <div> 311 + <div>Sidebar</div> 312 + <div>Header</div> 313 + <div> 314 + <Suspense fallback={<Skeleton />}> 315 + <DataDisplay /> 316 + </Suspense> 317 + </div> 318 + <div>Footer</div> 319 + </div> 320 + ) 321 + } 322 + 323 + async function DataDisplay() { 324 + const data = await fetchData() // Only blocks this component 325 + return <div>{data.content}</div> 326 + } 327 + ``` 328 + 329 + Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data. 330 + 331 + **Alternative: share promise across components** 332 + 333 + ```tsx 334 + function Page() { 335 + // Start fetch immediately, but don't await 336 + const dataPromise = fetchData() 337 + 338 + return ( 339 + <div> 340 + <div>Sidebar</div> 341 + <div>Header</div> 342 + <Suspense fallback={<Skeleton />}> 343 + <DataDisplay dataPromise={dataPromise} /> 344 + <DataSummary dataPromise={dataPromise} /> 345 + </Suspense> 346 + <div>Footer</div> 347 + </div> 348 + ) 349 + } 350 + 351 + function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) { 352 + const data = use(dataPromise) // Unwraps the promise 353 + return <div>{data.content}</div> 354 + } 355 + 356 + function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) { 357 + const data = use(dataPromise) // Reuses the same promise 358 + return <div>{data.summary}</div> 359 + } 360 + ``` 361 + 362 + Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together. 363 + 364 + **When NOT to use this pattern:** 365 + 366 + - Critical data needed for layout decisions (affects positioning) 367 + 368 + - SEO-critical content above the fold 369 + 370 + - Small, fast queries where suspense overhead isn't worth it 371 + 372 + - When you want to avoid layout shift (loading → content jump) 373 + 374 + **Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities. 375 + 376 + --- 377 + 378 + ## 2. Bundle Size Optimization 379 + 380 + **Impact: CRITICAL** 381 + 382 + Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint. 383 + 384 + ### 2.1 Avoid Barrel File Imports 385 + 386 + **Impact: CRITICAL (200-800ms import cost, slow builds)** 387 + 388 + 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'`). 389 + 390 + 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. 391 + 392 + **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. 393 + 394 + **Incorrect: imports entire library** 395 + 396 + ```tsx 397 + import { Check, X, Menu } from 'lucide-react' 398 + // Loads 1,583 modules, takes ~2.8s extra in dev 399 + // Runtime cost: 200-800ms on every cold start 400 + 401 + import { Button, TextField } from '@mui/material' 402 + // Loads 2,225 modules, takes ~4.2s extra in dev 403 + ``` 404 + 405 + **Correct: imports only what you need** 406 + 407 + ```tsx 408 + import Check from 'lucide-react/dist/esm/icons/check' 409 + import X from 'lucide-react/dist/esm/icons/x' 410 + import Menu from 'lucide-react/dist/esm/icons/menu' 411 + // Loads only 3 modules (~2KB vs ~1MB) 412 + 413 + import Button from '@mui/material/Button' 414 + import TextField from '@mui/material/TextField' 415 + // Loads only what you use 416 + ``` 417 + 418 + **Alternative: Next.js 13.5+** 419 + 420 + ```js 421 + // next.config.js - use optimizePackageImports 422 + module.exports = { 423 + experimental: { 424 + optimizePackageImports: ['lucide-react', '@mui/material'] 425 + } 426 + } 427 + 428 + // Then you can keep the ergonomic barrel imports: 429 + import { Check, X, Menu } from 'lucide-react' 430 + // Automatically transformed to direct imports at build time 431 + ``` 432 + 433 + Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR. 434 + 435 + 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`. 436 + 437 + Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) 438 + 439 + ### 2.2 Conditional Module Loading 440 + 441 + **Impact: HIGH (loads large data only when needed)** 442 + 443 + Load large data or modules only when a feature is activated. 444 + 445 + **Example: lazy-load animation frames** 446 + 447 + ```tsx 448 + function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) { 449 + const [frames, setFrames] = useState<Frame[] | null>(null) 450 + 451 + useEffect(() => { 452 + if (enabled && !frames && typeof window !== 'undefined') { 453 + import('./animation-frames.js') 454 + .then(mod => setFrames(mod.frames)) 455 + .catch(() => setEnabled(false)) 456 + } 457 + }, [enabled, frames, setEnabled]) 458 + 459 + if (!frames) return <Skeleton /> 460 + return <Canvas frames={frames} /> 461 + } 462 + ``` 463 + 464 + The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed. 465 + 466 + ### 2.3 Defer Non-Critical Third-Party Libraries 467 + 468 + **Impact: MEDIUM (loads after hydration)** 469 + 470 + Analytics, logging, and error tracking don't block user interaction. Load them after hydration. 471 + 472 + **Incorrect: blocks initial bundle** 473 + 474 + ```tsx 475 + import { Analytics } from '@vercel/analytics/react' 476 + 477 + export default function RootLayout({ children }) { 478 + return ( 479 + <html> 480 + <body> 481 + {children} 482 + <Analytics /> 483 + </body> 484 + </html> 485 + ) 486 + } 487 + ``` 488 + 489 + **Correct: loads after hydration** 490 + 491 + ```tsx 492 + import dynamic from 'next/dynamic' 493 + 494 + const Analytics = dynamic( 495 + () => import('@vercel/analytics/react').then(m => m.Analytics), 496 + { ssr: false } 497 + ) 498 + 499 + export default function RootLayout({ children }) { 500 + return ( 501 + <html> 502 + <body> 503 + {children} 504 + <Analytics /> 505 + </body> 506 + </html> 507 + ) 508 + } 509 + ``` 510 + 511 + ### 2.4 Dynamic Imports for Heavy Components 512 + 513 + **Impact: CRITICAL (directly affects TTI and LCP)** 514 + 515 + Use `next/dynamic` to lazy-load large components not needed on initial render. 516 + 517 + **Incorrect: Monaco bundles with main chunk ~300KB** 518 + 519 + ```tsx 520 + import { MonacoEditor } from './monaco-editor' 521 + 522 + function CodePanel({ code }: { code: string }) { 523 + return <MonacoEditor value={code} /> 524 + } 525 + ``` 526 + 527 + **Correct: Monaco loads on demand** 528 + 529 + ```tsx 530 + import dynamic from 'next/dynamic' 531 + 532 + const MonacoEditor = dynamic( 533 + () => import('./monaco-editor').then(m => m.MonacoEditor), 534 + { ssr: false } 535 + ) 536 + 537 + function CodePanel({ code }: { code: string }) { 538 + return <MonacoEditor value={code} /> 539 + } 540 + ``` 541 + 542 + ### 2.5 Preload Based on User Intent 543 + 544 + **Impact: MEDIUM (reduces perceived latency)** 545 + 546 + Preload heavy bundles before they're needed to reduce perceived latency. 547 + 548 + **Example: preload on hover/focus** 549 + 550 + ```tsx 551 + function EditorButton({ onClick }: { onClick: () => void }) { 552 + const preload = () => { 553 + if (typeof window !== 'undefined') { 554 + void import('./monaco-editor') 555 + } 556 + } 557 + 558 + return ( 559 + <button 560 + onMouseEnter={preload} 561 + onFocus={preload} 562 + onClick={onClick} 563 + > 564 + Open Editor 565 + </button> 566 + ) 567 + } 568 + ``` 569 + 570 + **Example: preload when feature flag is enabled** 571 + 572 + ```tsx 573 + function FlagsProvider({ children, flags }: Props) { 574 + useEffect(() => { 575 + if (flags.editorEnabled && typeof window !== 'undefined') { 576 + void import('./monaco-editor').then(mod => mod.init()) 577 + } 578 + }, [flags.editorEnabled]) 579 + 580 + return <FlagsContext.Provider value={flags}> 581 + {children} 582 + </FlagsContext.Provider> 583 + } 584 + ``` 585 + 586 + The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed. 587 + 588 + --- 589 + 590 + ## 3. Server-Side Performance 591 + 592 + **Impact: HIGH** 593 + 594 + Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times. 595 + 596 + ### 3.1 Authenticate Server Actions Like API Routes 597 + 598 + **Impact: CRITICAL (prevents unauthorized access to server mutations)** 599 + 600 + 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. 601 + 602 + 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." 603 + 604 + **Incorrect: no authentication check** 605 + 606 + ```typescript 607 + 'use server' 608 + 609 + export async function deleteUser(userId: string) { 610 + // Anyone can call this! No auth check 611 + await db.user.delete({ where: { id: userId } }) 612 + return { success: true } 613 + } 614 + ``` 615 + 616 + **Correct: authentication inside the action** 617 + 618 + ```typescript 619 + 'use server' 620 + 621 + import { verifySession } from '@/lib/auth' 622 + import { unauthorized } from '@/lib/errors' 623 + 624 + export async function deleteUser(userId: string) { 625 + // Always check auth inside the action 626 + const session = await verifySession() 627 + 628 + if (!session) { 629 + throw unauthorized('Must be logged in') 630 + } 631 + 632 + // Check authorization too 633 + if (session.user.role !== 'admin' && session.user.id !== userId) { 634 + throw unauthorized('Cannot delete other users') 635 + } 636 + 637 + await db.user.delete({ where: { id: userId } }) 638 + return { success: true } 639 + } 640 + ``` 641 + 642 + **With input validation:** 643 + 644 + ```typescript 645 + 'use server' 646 + 647 + import { verifySession } from '@/lib/auth' 648 + import { z } from 'zod' 649 + 650 + const updateProfileSchema = z.object({ 651 + userId: z.string().uuid(), 652 + name: z.string().min(1).max(100), 653 + email: z.string().email() 654 + }) 655 + 656 + export async function updateProfile(data: unknown) { 657 + // Validate input first 658 + const validated = updateProfileSchema.parse(data) 659 + 660 + // Then authenticate 661 + const session = await verifySession() 662 + if (!session) { 663 + throw new Error('Unauthorized') 664 + } 665 + 666 + // Then authorize 667 + if (session.user.id !== validated.userId) { 668 + throw new Error('Can only update own profile') 669 + } 670 + 671 + // Finally perform the mutation 672 + await db.user.update({ 673 + where: { id: validated.userId }, 674 + data: { 675 + name: validated.name, 676 + email: validated.email 677 + } 678 + }) 679 + 680 + return { success: true } 681 + } 682 + ``` 683 + 684 + Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication) 685 + 686 + ### 3.2 Avoid Duplicate Serialization in RSC Props 687 + 688 + **Impact: LOW (reduces network payload by avoiding duplicate serialization)** 689 + 690 + RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server. 691 + 692 + **Incorrect: duplicates array** 693 + 694 + ```tsx 695 + // RSC: sends 6 strings (2 arrays × 3 items) 696 + <ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} /> 697 + ``` 698 + 699 + **Correct: sends 3 strings** 700 + 701 + ```tsx 702 + // RSC: send once 703 + <ClientList usernames={usernames} /> 704 + 705 + // Client: transform there 706 + 'use client' 707 + const sorted = useMemo(() => [...usernames].sort(), [usernames]) 708 + ``` 709 + 710 + **Nested deduplication behavior:** 711 + 712 + ```tsx 713 + // string[] - duplicates everything 714 + usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings 715 + 716 + // object[] - duplicates array structure only 717 + users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4) 718 + ``` 719 + 720 + Deduplication works recursively. Impact varies by data type: 721 + 722 + - `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated 723 + 724 + - `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference 725 + 726 + **Operations breaking deduplication: create new references** 727 + 728 + - Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]` 729 + 730 + - Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())` 731 + 732 + **More examples:** 733 + 734 + ```tsx 735 + // ❌ Bad 736 + <C users={users} active={users.filter(u => u.active)} /> 737 + <C product={product} productName={product.name} /> 738 + 739 + // ✅ Good 740 + <C users={users} /> 741 + <C product={product} /> 742 + // Do filtering/destructuring in client 743 + ``` 744 + 745 + **Exception:** Pass derived data when transformation is expensive or client doesn't need original. 746 + 747 + ### 3.3 Cross-Request LRU Caching 748 + 749 + **Impact: HIGH (caches across requests)** 750 + 751 + `React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache. 752 + 753 + **Implementation:** 754 + 755 + ```typescript 756 + import { LRUCache } from 'lru-cache' 757 + 758 + const cache = new LRUCache<string, any>({ 759 + max: 1000, 760 + ttl: 5 * 60 * 1000 // 5 minutes 761 + }) 762 + 763 + export async function getUser(id: string) { 764 + const cached = cache.get(id) 765 + if (cached) return cached 766 + 767 + const user = await db.user.findUnique({ where: { id } }) 768 + cache.set(id, user) 769 + return user 770 + } 771 + 772 + // Request 1: DB query, result cached 773 + // Request 2: cache hit, no DB query 774 + ``` 775 + 776 + Use when sequential user actions hit multiple endpoints needing the same data within seconds. 777 + 778 + **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. 779 + 780 + **In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching. 781 + 782 + Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache) 783 + 784 + ### 3.4 Minimize Serialization at RSC Boundaries 785 + 786 + **Impact: HIGH (reduces data transfer size)** 787 + 788 + 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. 789 + 790 + **Incorrect: serializes all 50 fields** 791 + 792 + ```tsx 793 + async function Page() { 794 + const user = await fetchUser() // 50 fields 795 + return <Profile user={user} /> 796 + } 797 + 798 + 'use client' 799 + function Profile({ user }: { user: User }) { 800 + return <div>{user.name}</div> // uses 1 field 801 + } 802 + ``` 803 + 804 + **Correct: serializes only 1 field** 805 + 806 + ```tsx 807 + async function Page() { 808 + const user = await fetchUser() 809 + return <Profile name={user.name} /> 810 + } 811 + 812 + 'use client' 813 + function Profile({ name }: { name: string }) { 814 + return <div>{name}</div> 815 + } 816 + ``` 817 + 818 + ### 3.5 Parallel Data Fetching with Component Composition 819 + 820 + **Impact: CRITICAL (eliminates server-side waterfalls)** 821 + 822 + React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching. 823 + 824 + **Incorrect: Sidebar waits for Page's fetch to complete** 825 + 826 + ```tsx 827 + export default async function Page() { 828 + const header = await fetchHeader() 829 + return ( 830 + <div> 831 + <div>{header}</div> 832 + <Sidebar /> 833 + </div> 834 + ) 835 + } 836 + 837 + async function Sidebar() { 838 + const items = await fetchSidebarItems() 839 + return <nav>{items.map(renderItem)}</nav> 840 + } 841 + ``` 842 + 843 + **Correct: both fetch simultaneously** 844 + 845 + ```tsx 846 + async function Header() { 847 + const data = await fetchHeader() 848 + return <div>{data}</div> 849 + } 850 + 851 + async function Sidebar() { 852 + const items = await fetchSidebarItems() 853 + return <nav>{items.map(renderItem)}</nav> 854 + } 855 + 856 + export default function Page() { 857 + return ( 858 + <div> 859 + <Header /> 860 + <Sidebar /> 861 + </div> 862 + ) 863 + } 864 + ``` 865 + 866 + **Alternative with children prop:** 867 + 868 + ```tsx 869 + async function Header() { 870 + const data = await fetchHeader() 871 + return <div>{data}</div> 872 + } 873 + 874 + async function Sidebar() { 875 + const items = await fetchSidebarItems() 876 + return <nav>{items.map(renderItem)}</nav> 877 + } 878 + 879 + function Layout({ children }: { children: ReactNode }) { 880 + return ( 881 + <div> 882 + <Header /> 883 + {children} 884 + </div> 885 + ) 886 + } 887 + 888 + export default function Page() { 889 + return ( 890 + <Layout> 891 + <Sidebar /> 892 + </Layout> 893 + ) 894 + } 895 + ``` 896 + 897 + ### 3.6 Per-Request Deduplication with React.cache() 898 + 899 + **Impact: MEDIUM (deduplicates within request)** 900 + 901 + Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most. 902 + 903 + **Usage:** 904 + 905 + ```typescript 906 + import { cache } from 'react' 907 + 908 + export const getCurrentUser = cache(async () => { 909 + const session = await auth() 910 + if (!session?.user?.id) return null 911 + return await db.user.findUnique({ 912 + where: { id: session.user.id } 913 + }) 914 + }) 915 + ``` 916 + 917 + Within a single request, multiple calls to `getCurrentUser()` execute the query only once. 918 + 919 + **Avoid inline objects as arguments:** 920 + 921 + `React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits. 922 + 923 + **Incorrect: always cache miss** 924 + 925 + ```typescript 926 + const getUser = cache(async (params: { uid: number }) => { 927 + return await db.user.findUnique({ where: { id: params.uid } }) 928 + }) 929 + 930 + // Each call creates new object, never hits cache 931 + getUser({ uid: 1 }) 932 + getUser({ uid: 1 }) // Cache miss, runs query again 933 + ``` 934 + 935 + **Correct: cache hit** 936 + 937 + ```typescript 938 + const params = { uid: 1 } 939 + getUser(params) // Query runs 940 + getUser(params) // Cache hit (same reference) 941 + ``` 942 + 943 + If you must pass objects, pass the same reference: 944 + 945 + **Next.js-Specific Note:** 946 + 947 + 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: 948 + 949 + - Database queries (Prisma, Drizzle, etc.) 950 + 951 + - Heavy computations 952 + 953 + - Authentication checks 954 + 955 + - File system operations 956 + 957 + - Any non-fetch async work 958 + 959 + Use `React.cache()` to deduplicate these operations across your component tree. 960 + 961 + Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache) 962 + 963 + ### 3.7 Use after() for Non-Blocking Operations 964 + 965 + **Impact: MEDIUM (faster response times)** 966 + 967 + 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. 968 + 969 + **Incorrect: blocks response** 970 + 971 + ```tsx 972 + import { logUserAction } from '@/app/utils' 973 + 974 + export async function POST(request: Request) { 975 + // Perform mutation 976 + await updateDatabase(request) 977 + 978 + // Logging blocks the response 979 + const userAgent = request.headers.get('user-agent') || 'unknown' 980 + await logUserAction({ userAgent }) 981 + 982 + return new Response(JSON.stringify({ status: 'success' }), { 983 + status: 200, 984 + headers: { 'Content-Type': 'application/json' } 985 + }) 986 + } 987 + ``` 988 + 989 + **Correct: non-blocking** 990 + 991 + ```tsx 992 + import { after } from 'next/server' 993 + import { headers, cookies } from 'next/headers' 994 + import { logUserAction } from '@/app/utils' 995 + 996 + export async function POST(request: Request) { 997 + // Perform mutation 998 + await updateDatabase(request) 999 + 1000 + // Log after response is sent 1001 + after(async () => { 1002 + const userAgent = (await headers()).get('user-agent') || 'unknown' 1003 + const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous' 1004 + 1005 + logUserAction({ sessionCookie, userAgent }) 1006 + }) 1007 + 1008 + return new Response(JSON.stringify({ status: 'success' }), { 1009 + status: 200, 1010 + headers: { 'Content-Type': 'application/json' } 1011 + }) 1012 + } 1013 + ``` 1014 + 1015 + The response is sent immediately while logging happens in the background. 1016 + 1017 + **Common use cases:** 1018 + 1019 + - Analytics tracking 1020 + 1021 + - Audit logging 1022 + 1023 + - Sending notifications 1024 + 1025 + - Cache invalidation 1026 + 1027 + - Cleanup tasks 1028 + 1029 + **Important notes:** 1030 + 1031 + - `after()` runs even if the response fails or redirects 1032 + 1033 + - Works in Server Actions, Route Handlers, and Server Components 1034 + 1035 + Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after) 1036 + 1037 + --- 1038 + 1039 + ## 4. Client-Side Data Fetching 1040 + 1041 + **Impact: MEDIUM-HIGH** 1042 + 1043 + Automatic deduplication and efficient data fetching patterns reduce redundant network requests. 1044 + 1045 + ### 4.1 Deduplicate Global Event Listeners 1046 + 1047 + **Impact: LOW (single listener for N components)** 1048 + 1049 + Use `useSWRSubscription()` to share global event listeners across component instances. 1050 + 1051 + **Incorrect: N instances = N listeners** 1052 + 1053 + ```tsx 1054 + function useKeyboardShortcut(key: string, callback: () => void) { 1055 + useEffect(() => { 1056 + const handler = (e: KeyboardEvent) => { 1057 + if (e.metaKey && e.key === key) { 1058 + callback() 1059 + } 1060 + } 1061 + window.addEventListener('keydown', handler) 1062 + return () => window.removeEventListener('keydown', handler) 1063 + }, [key, callback]) 1064 + } 1065 + ``` 1066 + 1067 + When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener. 1068 + 1069 + **Correct: N instances = 1 listener** 1070 + 1071 + ```tsx 1072 + import useSWRSubscription from 'swr/subscription' 1073 + 1074 + // Module-level Map to track callbacks per key 1075 + const keyCallbacks = new Map<string, Set<() => void>>() 1076 + 1077 + function useKeyboardShortcut(key: string, callback: () => void) { 1078 + // Register this callback in the Map 1079 + useEffect(() => { 1080 + if (!keyCallbacks.has(key)) { 1081 + keyCallbacks.set(key, new Set()) 1082 + } 1083 + keyCallbacks.get(key)!.add(callback) 1084 + 1085 + return () => { 1086 + const set = keyCallbacks.get(key) 1087 + if (set) { 1088 + set.delete(callback) 1089 + if (set.size === 0) { 1090 + keyCallbacks.delete(key) 1091 + } 1092 + } 1093 + } 1094 + }, [key, callback]) 1095 + 1096 + useSWRSubscription('global-keydown', () => { 1097 + const handler = (e: KeyboardEvent) => { 1098 + if (e.metaKey && keyCallbacks.has(e.key)) { 1099 + keyCallbacks.get(e.key)!.forEach(cb => cb()) 1100 + } 1101 + } 1102 + window.addEventListener('keydown', handler) 1103 + return () => window.removeEventListener('keydown', handler) 1104 + }) 1105 + } 1106 + 1107 + function Profile() { 1108 + // Multiple shortcuts will share the same listener 1109 + useKeyboardShortcut('p', () => { /* ... */ }) 1110 + useKeyboardShortcut('k', () => { /* ... */ }) 1111 + // ... 1112 + } 1113 + ``` 1114 + 1115 + ### 4.2 Use Passive Event Listeners for Scrolling Performance 1116 + 1117 + **Impact: MEDIUM (eliminates scroll delay caused by event listeners)** 1118 + 1119 + 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. 1120 + 1121 + **Incorrect:** 1122 + 1123 + ```typescript 1124 + useEffect(() => { 1125 + const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX) 1126 + const handleWheel = (e: WheelEvent) => console.log(e.deltaY) 1127 + 1128 + document.addEventListener('touchstart', handleTouch) 1129 + document.addEventListener('wheel', handleWheel) 1130 + 1131 + return () => { 1132 + document.removeEventListener('touchstart', handleTouch) 1133 + document.removeEventListener('wheel', handleWheel) 1134 + } 1135 + }, []) 1136 + ``` 1137 + 1138 + **Correct:** 1139 + 1140 + ```typescript 1141 + useEffect(() => { 1142 + const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX) 1143 + const handleWheel = (e: WheelEvent) => console.log(e.deltaY) 1144 + 1145 + document.addEventListener('touchstart', handleTouch, { passive: true }) 1146 + document.addEventListener('wheel', handleWheel, { passive: true }) 1147 + 1148 + return () => { 1149 + document.removeEventListener('touchstart', handleTouch) 1150 + document.removeEventListener('wheel', handleWheel) 1151 + } 1152 + }, []) 1153 + ``` 1154 + 1155 + **Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`. 1156 + 1157 + **Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`. 1158 + 1159 + ### 4.3 Use SWR for Automatic Deduplication 1160 + 1161 + **Impact: MEDIUM-HIGH (automatic deduplication)** 1162 + 1163 + SWR enables request deduplication, caching, and revalidation across component instances. 1164 + 1165 + **Incorrect: no deduplication, each instance fetches** 1166 + 1167 + ```tsx 1168 + function UserList() { 1169 + const [users, setUsers] = useState([]) 1170 + useEffect(() => { 1171 + fetch('/api/users') 1172 + .then(r => r.json()) 1173 + .then(setUsers) 1174 + }, []) 1175 + } 1176 + ``` 1177 + 1178 + **Correct: multiple instances share one request** 1179 + 1180 + ```tsx 1181 + import useSWR from 'swr' 1182 + 1183 + function UserList() { 1184 + const { data: users } = useSWR('/api/users', fetcher) 1185 + } 1186 + ``` 1187 + 1188 + **For immutable data:** 1189 + 1190 + ```tsx 1191 + import { useImmutableSWR } from '@/lib/swr' 1192 + 1193 + function StaticContent() { 1194 + const { data } = useImmutableSWR('/api/config', fetcher) 1195 + } 1196 + ``` 1197 + 1198 + **For mutations:** 1199 + 1200 + ```tsx 1201 + import { useSWRMutation } from 'swr/mutation' 1202 + 1203 + function UpdateButton() { 1204 + const { trigger } = useSWRMutation('/api/user', updateUser) 1205 + return <button onClick={() => trigger()}>Update</button> 1206 + } 1207 + ``` 1208 + 1209 + Reference: [https://swr.vercel.app](https://swr.vercel.app) 1210 + 1211 + ### 4.4 Version and Minimize localStorage Data 1212 + 1213 + **Impact: MEDIUM (prevents schema conflicts, reduces storage size)** 1214 + 1215 + Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data. 1216 + 1217 + **Incorrect:** 1218 + 1219 + ```typescript 1220 + // No version, stores everything, no error handling 1221 + localStorage.setItem('userConfig', JSON.stringify(fullUserObject)) 1222 + const data = localStorage.getItem('userConfig') 1223 + ``` 1224 + 1225 + **Correct:** 1226 + 1227 + ```typescript 1228 + const VERSION = 'v2' 1229 + 1230 + function saveConfig(config: { theme: string; language: string }) { 1231 + try { 1232 + localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config)) 1233 + } catch { 1234 + // Throws in incognito/private browsing, quota exceeded, or disabled 1235 + } 1236 + } 1237 + 1238 + function loadConfig() { 1239 + try { 1240 + const data = localStorage.getItem(`userConfig:${VERSION}`) 1241 + return data ? JSON.parse(data) : null 1242 + } catch { 1243 + return null 1244 + } 1245 + } 1246 + 1247 + // Migration from v1 to v2 1248 + function migrate() { 1249 + try { 1250 + const v1 = localStorage.getItem('userConfig:v1') 1251 + if (v1) { 1252 + const old = JSON.parse(v1) 1253 + saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang }) 1254 + localStorage.removeItem('userConfig:v1') 1255 + } 1256 + } catch {} 1257 + } 1258 + ``` 1259 + 1260 + **Store minimal fields from server responses:** 1261 + 1262 + ```typescript 1263 + // User object has 20+ fields, only store what UI needs 1264 + function cachePrefs(user: FullUser) { 1265 + try { 1266 + localStorage.setItem('prefs:v1', JSON.stringify({ 1267 + theme: user.preferences.theme, 1268 + notifications: user.preferences.notifications 1269 + })) 1270 + } catch {} 1271 + } 1272 + ``` 1273 + 1274 + **Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled. 1275 + 1276 + **Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags. 1277 + 1278 + --- 1279 + 1280 + ## 5. Re-render Optimization 1281 + 1282 + **Impact: MEDIUM** 1283 + 1284 + Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness. 1285 + 1286 + ### 5.1 Calculate Derived State During Rendering 1287 + 1288 + **Impact: MEDIUM (avoids redundant renders and state drift)** 1289 + 1290 + If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead. 1291 + 1292 + **Incorrect: redundant state and effect** 1293 + 1294 + ```tsx 1295 + function Form() { 1296 + const [firstName, setFirstName] = useState('First') 1297 + const [lastName, setLastName] = useState('Last') 1298 + const [fullName, setFullName] = useState('') 1299 + 1300 + useEffect(() => { 1301 + setFullName(firstName + ' ' + lastName) 1302 + }, [firstName, lastName]) 1303 + 1304 + return <p>{fullName}</p> 1305 + } 1306 + ``` 1307 + 1308 + **Correct: derive during render** 1309 + 1310 + ```tsx 1311 + function Form() { 1312 + const [firstName, setFirstName] = useState('First') 1313 + const [lastName, setLastName] = useState('Last') 1314 + const fullName = firstName + ' ' + lastName 1315 + 1316 + return <p>{fullName}</p> 1317 + } 1318 + ``` 1319 + 1320 + Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect) 1321 + 1322 + ### 5.2 Defer State Reads to Usage Point 1323 + 1324 + **Impact: MEDIUM (avoids unnecessary subscriptions)** 1325 + 1326 + Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks. 1327 + 1328 + **Incorrect: subscribes to all searchParams changes** 1329 + 1330 + ```tsx 1331 + function ShareButton({ chatId }: { chatId: string }) { 1332 + const searchParams = useSearchParams() 1333 + 1334 + const handleShare = () => { 1335 + const ref = searchParams.get('ref') 1336 + shareChat(chatId, { ref }) 1337 + } 1338 + 1339 + return <button onClick={handleShare}>Share</button> 1340 + } 1341 + ``` 1342 + 1343 + **Correct: reads on demand, no subscription** 1344 + 1345 + ```tsx 1346 + function ShareButton({ chatId }: { chatId: string }) { 1347 + const handleShare = () => { 1348 + const params = new URLSearchParams(window.location.search) 1349 + const ref = params.get('ref') 1350 + shareChat(chatId, { ref }) 1351 + } 1352 + 1353 + return <button onClick={handleShare}>Share</button> 1354 + } 1355 + ``` 1356 + 1357 + ### 5.3 Do not wrap a simple expression with a primitive result type in useMemo 1358 + 1359 + **Impact: LOW-MEDIUM (wasted computation on every render)** 1360 + 1361 + When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`. 1362 + 1363 + Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself. 1364 + 1365 + **Incorrect:** 1366 + 1367 + ```tsx 1368 + function Header({ user, notifications }: Props) { 1369 + const isLoading = useMemo(() => { 1370 + return user.isLoading || notifications.isLoading 1371 + }, [user.isLoading, notifications.isLoading]) 1372 + 1373 + if (isLoading) return <Skeleton /> 1374 + // return some markup 1375 + } 1376 + ``` 1377 + 1378 + **Correct:** 1379 + 1380 + ```tsx 1381 + function Header({ user, notifications }: Props) { 1382 + const isLoading = user.isLoading || notifications.isLoading 1383 + 1384 + if (isLoading) return <Skeleton /> 1385 + // return some markup 1386 + } 1387 + ``` 1388 + 1389 + ### 5.4 Extract Default Non-primitive Parameter Value from Memoized Component to Constant 1390 + 1391 + **Impact: MEDIUM (restores memoization by using a constant for default value)** 1392 + 1393 + 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()`. 1394 + 1395 + To address this issue, extract the default value into a constant. 1396 + 1397 + **Incorrect: `onClick` has different values on every rerender** 1398 + 1399 + ```tsx 1400 + const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) { 1401 + // ... 1402 + }) 1403 + 1404 + // Used without optional onClick 1405 + <UserAvatar /> 1406 + ``` 1407 + 1408 + **Correct: stable default value** 1409 + 1410 + ```tsx 1411 + const NOOP = () => {}; 1412 + 1413 + const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) { 1414 + // ... 1415 + }) 1416 + 1417 + // Used without optional onClick 1418 + <UserAvatar /> 1419 + ``` 1420 + 1421 + ### 5.5 Extract to Memoized Components 1422 + 1423 + **Impact: MEDIUM (enables early returns)** 1424 + 1425 + Extract expensive work into memoized components to enable early returns before computation. 1426 + 1427 + **Incorrect: computes avatar even when loading** 1428 + 1429 + ```tsx 1430 + function Profile({ user, loading }: Props) { 1431 + const avatar = useMemo(() => { 1432 + const id = computeAvatarId(user) 1433 + return <Avatar id={id} /> 1434 + }, [user]) 1435 + 1436 + if (loading) return <Skeleton /> 1437 + return <div>{avatar}</div> 1438 + } 1439 + ``` 1440 + 1441 + **Correct: skips computation when loading** 1442 + 1443 + ```tsx 1444 + const UserAvatar = memo(function UserAvatar({ user }: { user: User }) { 1445 + const id = useMemo(() => computeAvatarId(user), [user]) 1446 + return <Avatar id={id} /> 1447 + }) 1448 + 1449 + function Profile({ user, loading }: Props) { 1450 + if (loading) return <Skeleton /> 1451 + return ( 1452 + <div> 1453 + <UserAvatar user={user} /> 1454 + </div> 1455 + ) 1456 + } 1457 + ``` 1458 + 1459 + **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. 1460 + 1461 + ### 5.6 Narrow Effect Dependencies 1462 + 1463 + **Impact: LOW (minimizes effect re-runs)** 1464 + 1465 + Specify primitive dependencies instead of objects to minimize effect re-runs. 1466 + 1467 + **Incorrect: re-runs on any user field change** 1468 + 1469 + ```tsx 1470 + useEffect(() => { 1471 + console.log(user.id) 1472 + }, [user]) 1473 + ``` 1474 + 1475 + **Correct: re-runs only when id changes** 1476 + 1477 + ```tsx 1478 + useEffect(() => { 1479 + console.log(user.id) 1480 + }, [user.id]) 1481 + ``` 1482 + 1483 + **For derived state, compute outside effect:** 1484 + 1485 + ```tsx 1486 + // Incorrect: runs on width=767, 766, 765... 1487 + useEffect(() => { 1488 + if (width < 768) { 1489 + enableMobileMode() 1490 + } 1491 + }, [width]) 1492 + 1493 + // Correct: runs only on boolean transition 1494 + const isMobile = width < 768 1495 + useEffect(() => { 1496 + if (isMobile) { 1497 + enableMobileMode() 1498 + } 1499 + }, [isMobile]) 1500 + ``` 1501 + 1502 + ### 5.7 Put Interaction Logic in Event Handlers 1503 + 1504 + **Impact: MEDIUM (avoids effect re-runs and duplicate side effects)** 1505 + 1506 + If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action. 1507 + 1508 + **Incorrect: event modeled as state + effect** 1509 + 1510 + ```tsx 1511 + function Form() { 1512 + const [submitted, setSubmitted] = useState(false) 1513 + const theme = useContext(ThemeContext) 1514 + 1515 + useEffect(() => { 1516 + if (submitted) { 1517 + post('/api/register') 1518 + showToast('Registered', theme) 1519 + } 1520 + }, [submitted, theme]) 1521 + 1522 + return <button onClick={() => setSubmitted(true)}>Submit</button> 1523 + } 1524 + ``` 1525 + 1526 + **Correct: do it in the handler** 1527 + 1528 + ```tsx 1529 + function Form() { 1530 + const theme = useContext(ThemeContext) 1531 + 1532 + function handleSubmit() { 1533 + post('/api/register') 1534 + showToast('Registered', theme) 1535 + } 1536 + 1537 + return <button onClick={handleSubmit}>Submit</button> 1538 + } 1539 + ``` 1540 + 1541 + Reference: [https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler) 1542 + 1543 + ### 5.8 Subscribe to Derived State 1544 + 1545 + **Impact: MEDIUM (reduces re-render frequency)** 1546 + 1547 + Subscribe to derived boolean state instead of continuous values to reduce re-render frequency. 1548 + 1549 + **Incorrect: re-renders on every pixel change** 1550 + 1551 + ```tsx 1552 + function Sidebar() { 1553 + const width = useWindowWidth() // updates continuously 1554 + const isMobile = width < 768 1555 + return <nav className={isMobile ? 'mobile' : 'desktop'} /> 1556 + } 1557 + ``` 1558 + 1559 + **Correct: re-renders only when boolean changes** 1560 + 1561 + ```tsx 1562 + function Sidebar() { 1563 + const isMobile = useMediaQuery('(max-width: 767px)') 1564 + return <nav className={isMobile ? 'mobile' : 'desktop'} /> 1565 + } 1566 + ``` 1567 + 1568 + ### 5.9 Use Functional setState Updates 1569 + 1570 + **Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)** 1571 + 1572 + 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. 1573 + 1574 + **Incorrect: requires state as dependency** 1575 + 1576 + ```tsx 1577 + function TodoList() { 1578 + const [items, setItems] = useState(initialItems) 1579 + 1580 + // Callback must depend on items, recreated on every items change 1581 + const addItems = useCallback((newItems: Item[]) => { 1582 + setItems([...items, ...newItems]) 1583 + }, [items]) // ❌ items dependency causes recreations 1584 + 1585 + // Risk of stale closure if dependency is forgotten 1586 + const removeItem = useCallback((id: string) => { 1587 + setItems(items.filter(item => item.id !== id)) 1588 + }, []) // ❌ Missing items dependency - will use stale items! 1589 + 1590 + return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} /> 1591 + } 1592 + ``` 1593 + 1594 + 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. 1595 + 1596 + **Correct: stable callbacks, no stale closures** 1597 + 1598 + ```tsx 1599 + function TodoList() { 1600 + const [items, setItems] = useState(initialItems) 1601 + 1602 + // Stable callback, never recreated 1603 + const addItems = useCallback((newItems: Item[]) => { 1604 + setItems(curr => [...curr, ...newItems]) 1605 + }, []) // ✅ No dependencies needed 1606 + 1607 + // Always uses latest state, no stale closure risk 1608 + const removeItem = useCallback((id: string) => { 1609 + setItems(curr => curr.filter(item => item.id !== id)) 1610 + }, []) // ✅ Safe and stable 1611 + 1612 + return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} /> 1613 + } 1614 + ``` 1615 + 1616 + **Benefits:** 1617 + 1618 + 1. **Stable callback references** - Callbacks don't need to be recreated when state changes 1619 + 1620 + 2. **No stale closures** - Always operates on the latest state value 1621 + 1622 + 3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks 1623 + 1624 + 4. **Prevents bugs** - Eliminates the most common source of React closure bugs 1625 + 1626 + **When to use functional updates:** 1627 + 1628 + - Any setState that depends on the current state value 1629 + 1630 + - Inside useCallback/useMemo when state is needed 1631 + 1632 + - Event handlers that reference state 1633 + 1634 + - Async operations that update state 1635 + 1636 + **When direct updates are fine:** 1637 + 1638 + - Setting state to a static value: `setCount(0)` 1639 + 1640 + - Setting state from props/arguments only: `setName(newName)` 1641 + 1642 + - State doesn't depend on previous value 1643 + 1644 + **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. 1645 + 1646 + ### 5.10 Use Lazy State Initialization 1647 + 1648 + **Impact: MEDIUM (wasted computation on every render)** 1649 + 1650 + 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. 1651 + 1652 + **Incorrect: runs on every render** 1653 + 1654 + ```tsx 1655 + function FilteredList({ items }: { items: Item[] }) { 1656 + // buildSearchIndex() runs on EVERY render, even after initialization 1657 + const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items)) 1658 + const [query, setQuery] = useState('') 1659 + 1660 + // When query changes, buildSearchIndex runs again unnecessarily 1661 + return <SearchResults index={searchIndex} query={query} /> 1662 + } 1663 + 1664 + function UserProfile() { 1665 + // JSON.parse runs on every render 1666 + const [settings, setSettings] = useState( 1667 + JSON.parse(localStorage.getItem('settings') || '{}') 1668 + ) 1669 + 1670 + return <SettingsForm settings={settings} onChange={setSettings} /> 1671 + } 1672 + ``` 1673 + 1674 + **Correct: runs only once** 1675 + 1676 + ```tsx 1677 + function FilteredList({ items }: { items: Item[] }) { 1678 + // buildSearchIndex() runs ONLY on initial render 1679 + const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items)) 1680 + const [query, setQuery] = useState('') 1681 + 1682 + return <SearchResults index={searchIndex} query={query} /> 1683 + } 1684 + 1685 + function UserProfile() { 1686 + // JSON.parse runs only on initial render 1687 + const [settings, setSettings] = useState(() => { 1688 + const stored = localStorage.getItem('settings') 1689 + return stored ? JSON.parse(stored) : {} 1690 + }) 1691 + 1692 + return <SettingsForm settings={settings} onChange={setSettings} /> 1693 + } 1694 + ``` 1695 + 1696 + Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations. 1697 + 1698 + For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary. 1699 + 1700 + ### 5.11 Use Transitions for Non-Urgent Updates 1701 + 1702 + **Impact: MEDIUM (maintains UI responsiveness)** 1703 + 1704 + Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness. 1705 + 1706 + **Incorrect: blocks UI on every scroll** 1707 + 1708 + ```tsx 1709 + function ScrollTracker() { 1710 + const [scrollY, setScrollY] = useState(0) 1711 + useEffect(() => { 1712 + const handler = () => setScrollY(window.scrollY) 1713 + window.addEventListener('scroll', handler, { passive: true }) 1714 + return () => window.removeEventListener('scroll', handler) 1715 + }, []) 1716 + } 1717 + ``` 1718 + 1719 + **Correct: non-blocking updates** 1720 + 1721 + ```tsx 1722 + import { startTransition } from 'react' 1723 + 1724 + function ScrollTracker() { 1725 + const [scrollY, setScrollY] = useState(0) 1726 + useEffect(() => { 1727 + const handler = () => { 1728 + startTransition(() => setScrollY(window.scrollY)) 1729 + } 1730 + window.addEventListener('scroll', handler, { passive: true }) 1731 + return () => window.removeEventListener('scroll', handler) 1732 + }, []) 1733 + } 1734 + ``` 1735 + 1736 + ### 5.12 Use useRef for Transient Values 1737 + 1738 + **Impact: MEDIUM (avoids unnecessary re-renders on frequent updates)** 1739 + 1740 + When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render. 1741 + 1742 + **Incorrect: renders every update** 1743 + 1744 + ```tsx 1745 + function Tracker() { 1746 + const [lastX, setLastX] = useState(0) 1747 + 1748 + useEffect(() => { 1749 + const onMove = (e: MouseEvent) => setLastX(e.clientX) 1750 + window.addEventListener('mousemove', onMove) 1751 + return () => window.removeEventListener('mousemove', onMove) 1752 + }, []) 1753 + 1754 + return ( 1755 + <div 1756 + style={{ 1757 + position: 'fixed', 1758 + top: 0, 1759 + left: lastX, 1760 + width: 8, 1761 + height: 8, 1762 + background: 'black', 1763 + }} 1764 + /> 1765 + ) 1766 + } 1767 + ``` 1768 + 1769 + **Correct: no re-render for tracking** 1770 + 1771 + ```tsx 1772 + function Tracker() { 1773 + const lastXRef = useRef(0) 1774 + const dotRef = useRef<HTMLDivElement>(null) 1775 + 1776 + useEffect(() => { 1777 + const onMove = (e: MouseEvent) => { 1778 + lastXRef.current = e.clientX 1779 + const node = dotRef.current 1780 + if (node) { 1781 + node.style.transform = `translateX(${e.clientX}px)` 1782 + } 1783 + } 1784 + window.addEventListener('mousemove', onMove) 1785 + return () => window.removeEventListener('mousemove', onMove) 1786 + }, []) 1787 + 1788 + return ( 1789 + <div 1790 + ref={dotRef} 1791 + style={{ 1792 + position: 'fixed', 1793 + top: 0, 1794 + left: 0, 1795 + width: 8, 1796 + height: 8, 1797 + background: 'black', 1798 + transform: 'translateX(0px)', 1799 + }} 1800 + /> 1801 + ) 1802 + } 1803 + ``` 1804 + 1805 + --- 1806 + 1807 + ## 6. Rendering Performance 1808 + 1809 + **Impact: MEDIUM** 1810 + 1811 + Optimizing the rendering process reduces the work the browser needs to do. 1812 + 1813 + ### 6.1 Animate SVG Wrapper Instead of SVG Element 1814 + 1815 + **Impact: LOW (enables hardware acceleration)** 1816 + 1817 + Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead. 1818 + 1819 + **Incorrect: animating SVG directly - no hardware acceleration** 1820 + 1821 + ```tsx 1822 + function LoadingSpinner() { 1823 + return ( 1824 + <svg 1825 + className="animate-spin" 1826 + width="24" 1827 + height="24" 1828 + viewBox="0 0 24 24" 1829 + > 1830 + <circle cx="12" cy="12" r="10" stroke="currentColor" /> 1831 + </svg> 1832 + ) 1833 + } 1834 + ``` 1835 + 1836 + **Correct: animating wrapper div - hardware accelerated** 1837 + 1838 + ```tsx 1839 + function LoadingSpinner() { 1840 + return ( 1841 + <div className="animate-spin"> 1842 + <svg 1843 + width="24" 1844 + height="24" 1845 + viewBox="0 0 24 24" 1846 + > 1847 + <circle cx="12" cy="12" r="10" stroke="currentColor" /> 1848 + </svg> 1849 + </div> 1850 + ) 1851 + } 1852 + ``` 1853 + 1854 + This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations. 1855 + 1856 + ### 6.2 CSS content-visibility for Long Lists 1857 + 1858 + **Impact: HIGH (faster initial render)** 1859 + 1860 + Apply `content-visibility: auto` to defer off-screen rendering. 1861 + 1862 + **CSS:** 1863 + 1864 + ```css 1865 + .message-item { 1866 + content-visibility: auto; 1867 + contain-intrinsic-size: 0 80px; 1868 + } 1869 + ``` 1870 + 1871 + **Example:** 1872 + 1873 + ```tsx 1874 + function MessageList({ messages }: { messages: Message[] }) { 1875 + return ( 1876 + <div className="overflow-y-auto h-screen"> 1877 + {messages.map(msg => ( 1878 + <div key={msg.id} className="message-item"> 1879 + <Avatar user={msg.author} /> 1880 + <div>{msg.content}</div> 1881 + </div> 1882 + ))} 1883 + </div> 1884 + ) 1885 + } 1886 + ``` 1887 + 1888 + For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render). 1889 + 1890 + ### 6.3 Hoist Static JSX Elements 1891 + 1892 + **Impact: LOW (avoids re-creation)** 1893 + 1894 + Extract static JSX outside components to avoid re-creation. 1895 + 1896 + **Incorrect: recreates element every render** 1897 + 1898 + ```tsx 1899 + function LoadingSkeleton() { 1900 + return <div className="animate-pulse h-20 bg-gray-200" /> 1901 + } 1902 + 1903 + function Container() { 1904 + return ( 1905 + <div> 1906 + {loading && <LoadingSkeleton />} 1907 + </div> 1908 + ) 1909 + } 1910 + ``` 1911 + 1912 + **Correct: reuses same element** 1913 + 1914 + ```tsx 1915 + const loadingSkeleton = ( 1916 + <div className="animate-pulse h-20 bg-gray-200" /> 1917 + ) 1918 + 1919 + function Container() { 1920 + return ( 1921 + <div> 1922 + {loading && loadingSkeleton} 1923 + </div> 1924 + ) 1925 + } 1926 + ``` 1927 + 1928 + This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render. 1929 + 1930 + **Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary. 1931 + 1932 + ### 6.4 Optimize SVG Precision 1933 + 1934 + **Impact: LOW (reduces file size)** 1935 + 1936 + Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered. 1937 + 1938 + **Incorrect: excessive precision** 1939 + 1940 + ```svg 1941 + <path d="M 10.293847 20.847362 L 30.938472 40.192837" /> 1942 + ``` 1943 + 1944 + **Correct: 1 decimal place** 1945 + 1946 + ```svg 1947 + <path d="M 10.3 20.8 L 30.9 40.2" /> 1948 + ``` 1949 + 1950 + **Automate with SVGO:** 1951 + 1952 + ```bash 1953 + npx svgo --precision=1 --multipass icon.svg 1954 + ``` 1955 + 1956 + ### 6.5 Prevent Hydration Mismatch Without Flickering 1957 + 1958 + **Impact: MEDIUM (avoids visual flicker and hydration errors)** 1959 + 1960 + When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates. 1961 + 1962 + **Incorrect: breaks SSR** 1963 + 1964 + ```tsx 1965 + function ThemeWrapper({ children }: { children: ReactNode }) { 1966 + // localStorage is not available on server - throws error 1967 + const theme = localStorage.getItem('theme') || 'light' 1968 + 1969 + return ( 1970 + <div className={theme}> 1971 + {children} 1972 + </div> 1973 + ) 1974 + } 1975 + ``` 1976 + 1977 + Server-side rendering will fail because `localStorage` is undefined. 1978 + 1979 + **Incorrect: visual flickering** 1980 + 1981 + ```tsx 1982 + function ThemeWrapper({ children }: { children: ReactNode }) { 1983 + const [theme, setTheme] = useState('light') 1984 + 1985 + useEffect(() => { 1986 + // Runs after hydration - causes visible flash 1987 + const stored = localStorage.getItem('theme') 1988 + if (stored) { 1989 + setTheme(stored) 1990 + } 1991 + }, []) 1992 + 1993 + return ( 1994 + <div className={theme}> 1995 + {children} 1996 + </div> 1997 + ) 1998 + } 1999 + ``` 2000 + 2001 + Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content. 2002 + 2003 + **Correct: no flicker, no hydration mismatch** 2004 + 2005 + ```tsx 2006 + function ThemeWrapper({ children }: { children: ReactNode }) { 2007 + return ( 2008 + <> 2009 + <div id="theme-wrapper"> 2010 + {children} 2011 + </div> 2012 + <script 2013 + dangerouslySetInnerHTML={{ 2014 + __html: ` 2015 + (function() { 2016 + try { 2017 + var theme = localStorage.getItem('theme') || 'light'; 2018 + var el = document.getElementById('theme-wrapper'); 2019 + if (el) el.className = theme; 2020 + } catch (e) {} 2021 + })(); 2022 + `, 2023 + }} 2024 + /> 2025 + </> 2026 + ) 2027 + } 2028 + ``` 2029 + 2030 + The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch. 2031 + 2032 + This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values. 2033 + 2034 + ### 6.6 Suppress Expected Hydration Mismatches 2035 + 2036 + **Impact: LOW-MEDIUM (avoids noisy hydration warnings for known differences)** 2037 + 2038 + In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it. 2039 + 2040 + **Incorrect: known mismatch warnings** 2041 + 2042 + ```tsx 2043 + function Timestamp() { 2044 + return <span>{new Date().toLocaleString()}</span> 2045 + } 2046 + ``` 2047 + 2048 + **Correct: suppress expected mismatch only** 2049 + 2050 + ```tsx 2051 + function Timestamp() { 2052 + return ( 2053 + <span suppressHydrationWarning> 2054 + {new Date().toLocaleString()} 2055 + </span> 2056 + ) 2057 + } 2058 + ``` 2059 + 2060 + ### 6.7 Use Activity Component for Show/Hide 2061 + 2062 + **Impact: MEDIUM (preserves state/DOM)** 2063 + 2064 + Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility. 2065 + 2066 + **Usage:** 2067 + 2068 + ```tsx 2069 + import { Activity } from 'react' 2070 + 2071 + function Dropdown({ isOpen }: Props) { 2072 + return ( 2073 + <Activity mode={isOpen ? 'visible' : 'hidden'}> 2074 + <ExpensiveMenu /> 2075 + </Activity> 2076 + ) 2077 + } 2078 + ``` 2079 + 2080 + Avoids expensive re-renders and state loss. 2081 + 2082 + ### 6.8 Use Explicit Conditional Rendering 2083 + 2084 + **Impact: LOW (prevents rendering 0 or NaN)** 2085 + 2086 + Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render. 2087 + 2088 + **Incorrect: renders "0" when count is 0** 2089 + 2090 + ```tsx 2091 + function Badge({ count }: { count: number }) { 2092 + return ( 2093 + <div> 2094 + {count && <span className="badge">{count}</span>} 2095 + </div> 2096 + ) 2097 + } 2098 + 2099 + // When count = 0, renders: <div>0</div> 2100 + // When count = 5, renders: <div><span class="badge">5</span></div> 2101 + ``` 2102 + 2103 + **Correct: renders nothing when count is 0** 2104 + 2105 + ```tsx 2106 + function Badge({ count }: { count: number }) { 2107 + return ( 2108 + <div> 2109 + {count > 0 ? <span className="badge">{count}</span> : null} 2110 + </div> 2111 + ) 2112 + } 2113 + 2114 + // When count = 0, renders: <div></div> 2115 + // When count = 5, renders: <div><span class="badge">5</span></div> 2116 + ``` 2117 + 2118 + ### 6.9 Use useTransition Over Manual Loading States 2119 + 2120 + **Impact: LOW (reduces re-renders and improves code clarity)** 2121 + 2122 + Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions. 2123 + 2124 + **Incorrect: manual loading state** 2125 + 2126 + ```tsx 2127 + function SearchResults() { 2128 + const [query, setQuery] = useState('') 2129 + const [results, setResults] = useState([]) 2130 + const [isLoading, setIsLoading] = useState(false) 2131 + 2132 + const handleSearch = async (value: string) => { 2133 + setIsLoading(true) 2134 + setQuery(value) 2135 + const data = await fetchResults(value) 2136 + setResults(data) 2137 + setIsLoading(false) 2138 + } 2139 + 2140 + return ( 2141 + <> 2142 + <input onChange={(e) => handleSearch(e.target.value)} /> 2143 + {isLoading && <Spinner />} 2144 + <ResultsList results={results} /> 2145 + </> 2146 + ) 2147 + } 2148 + ``` 2149 + 2150 + **Correct: useTransition with built-in pending state** 2151 + 2152 + ```tsx 2153 + import { useTransition, useState } from 'react' 2154 + 2155 + function SearchResults() { 2156 + const [query, setQuery] = useState('') 2157 + const [results, setResults] = useState([]) 2158 + const [isPending, startTransition] = useTransition() 2159 + 2160 + const handleSearch = (value: string) => { 2161 + setQuery(value) // Update input immediately 2162 + 2163 + startTransition(async () => { 2164 + // Fetch and update results 2165 + const data = await fetchResults(value) 2166 + setResults(data) 2167 + }) 2168 + } 2169 + 2170 + return ( 2171 + <> 2172 + <input onChange={(e) => handleSearch(e.target.value)} /> 2173 + {isPending && <Spinner />} 2174 + <ResultsList results={results} /> 2175 + </> 2176 + ) 2177 + } 2178 + ``` 2179 + 2180 + **Benefits:** 2181 + 2182 + - **Automatic pending state**: No need to manually manage `setIsLoading(true/false)` 2183 + 2184 + - **Error resilience**: Pending state correctly resets even if the transition throws 2185 + 2186 + - **Better responsiveness**: Keeps the UI responsive during updates 2187 + 2188 + - **Interrupt handling**: New transitions automatically cancel pending ones 2189 + 2190 + Reference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition) 2191 + 2192 + --- 2193 + 2194 + ## 7. JavaScript Performance 2195 + 2196 + **Impact: LOW-MEDIUM** 2197 + 2198 + Micro-optimizations for hot paths can add up to meaningful improvements. 2199 + 2200 + ### 7.1 Avoid Layout Thrashing 2201 + 2202 + **Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)** 2203 + 2204 + Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow. 2205 + 2206 + **This is OK: browser batches style changes** 2207 + 2208 + ```typescript 2209 + function updateElementStyles(element: HTMLElement) { 2210 + // Each line invalidates style, but browser batches the recalculation 2211 + element.style.width = '100px' 2212 + element.style.height = '200px' 2213 + element.style.backgroundColor = 'blue' 2214 + element.style.border = '1px solid black' 2215 + } 2216 + ``` 2217 + 2218 + **Incorrect: interleaved reads and writes force reflows** 2219 + 2220 + ```typescript 2221 + function layoutThrashing(element: HTMLElement) { 2222 + element.style.width = '100px' 2223 + const width = element.offsetWidth // Forces reflow 2224 + element.style.height = '200px' 2225 + const height = element.offsetHeight // Forces another reflow 2226 + } 2227 + ``` 2228 + 2229 + **Correct: batch writes, then read once** 2230 + 2231 + ```typescript 2232 + function updateElementStyles(element: HTMLElement) { 2233 + // Batch all writes together 2234 + element.style.width = '100px' 2235 + element.style.height = '200px' 2236 + element.style.backgroundColor = 'blue' 2237 + element.style.border = '1px solid black' 2238 + 2239 + // Read after all writes are done (single reflow) 2240 + const { width, height } = element.getBoundingClientRect() 2241 + } 2242 + ``` 2243 + 2244 + **Correct: batch reads, then writes** 2245 + 2246 + ```typescript 2247 + function updateElementStyles(element: HTMLElement) { 2248 + element.classList.add('highlighted-box') 2249 + 2250 + const { width, height } = element.getBoundingClientRect() 2251 + } 2252 + ``` 2253 + 2254 + **Better: use CSS classes** 2255 + 2256 + **React example:** 2257 + 2258 + ```tsx 2259 + // Incorrect: interleaving style changes with layout queries 2260 + function Box({ isHighlighted }: { isHighlighted: boolean }) { 2261 + const ref = useRef<HTMLDivElement>(null) 2262 + 2263 + useEffect(() => { 2264 + if (ref.current && isHighlighted) { 2265 + ref.current.style.width = '100px' 2266 + const width = ref.current.offsetWidth // Forces layout 2267 + ref.current.style.height = '200px' 2268 + } 2269 + }, [isHighlighted]) 2270 + 2271 + return <div ref={ref}>Content</div> 2272 + } 2273 + 2274 + // Correct: toggle class 2275 + function Box({ isHighlighted }: { isHighlighted: boolean }) { 2276 + return ( 2277 + <div className={isHighlighted ? 'highlighted-box' : ''}> 2278 + Content 2279 + </div> 2280 + ) 2281 + } 2282 + ``` 2283 + 2284 + Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain. 2285 + 2286 + See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations. 2287 + 2288 + ### 7.2 Build Index Maps for Repeated Lookups 2289 + 2290 + **Impact: LOW-MEDIUM (1M ops to 2K ops)** 2291 + 2292 + Multiple `.find()` calls by the same key should use a Map. 2293 + 2294 + **Incorrect (O(n) per lookup):** 2295 + 2296 + ```typescript 2297 + function processOrders(orders: Order[], users: User[]) { 2298 + return orders.map(order => ({ 2299 + ...order, 2300 + user: users.find(u => u.id === order.userId) 2301 + })) 2302 + } 2303 + ``` 2304 + 2305 + **Correct (O(1) per lookup):** 2306 + 2307 + ```typescript 2308 + function processOrders(orders: Order[], users: User[]) { 2309 + const userById = new Map(users.map(u => [u.id, u])) 2310 + 2311 + return orders.map(order => ({ 2312 + ...order, 2313 + user: userById.get(order.userId) 2314 + })) 2315 + } 2316 + ``` 2317 + 2318 + Build map once (O(n)), then all lookups are O(1). 2319 + 2320 + For 1000 orders × 1000 users: 1M ops → 2K ops. 2321 + 2322 + ### 7.3 Cache Property Access in Loops 2323 + 2324 + **Impact: LOW-MEDIUM (reduces lookups)** 2325 + 2326 + Cache object property lookups in hot paths. 2327 + 2328 + **Incorrect: 3 lookups × N iterations** 2329 + 2330 + ```typescript 2331 + for (let i = 0; i < arr.length; i++) { 2332 + process(obj.config.settings.value) 2333 + } 2334 + ``` 2335 + 2336 + **Correct: 1 lookup total** 2337 + 2338 + ```typescript 2339 + const value = obj.config.settings.value 2340 + const len = arr.length 2341 + for (let i = 0; i < len; i++) { 2342 + process(value) 2343 + } 2344 + ``` 2345 + 2346 + ### 7.4 Cache Repeated Function Calls 2347 + 2348 + **Impact: MEDIUM (avoid redundant computation)** 2349 + 2350 + Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render. 2351 + 2352 + **Incorrect: redundant computation** 2353 + 2354 + ```typescript 2355 + function ProjectList({ projects }: { projects: Project[] }) { 2356 + return ( 2357 + <div> 2358 + {projects.map(project => { 2359 + // slugify() called 100+ times for same project names 2360 + const slug = slugify(project.name) 2361 + 2362 + return <ProjectCard key={project.id} slug={slug} /> 2363 + })} 2364 + </div> 2365 + ) 2366 + } 2367 + ``` 2368 + 2369 + **Correct: cached results** 2370 + 2371 + ```typescript 2372 + // Module-level cache 2373 + const slugifyCache = new Map<string, string>() 2374 + 2375 + function cachedSlugify(text: string): string { 2376 + if (slugifyCache.has(text)) { 2377 + return slugifyCache.get(text)! 2378 + } 2379 + const result = slugify(text) 2380 + slugifyCache.set(text, result) 2381 + return result 2382 + } 2383 + 2384 + function ProjectList({ projects }: { projects: Project[] }) { 2385 + return ( 2386 + <div> 2387 + {projects.map(project => { 2388 + // Computed only once per unique project name 2389 + const slug = cachedSlugify(project.name) 2390 + 2391 + return <ProjectCard key={project.id} slug={slug} /> 2392 + })} 2393 + </div> 2394 + ) 2395 + } 2396 + ``` 2397 + 2398 + **Simpler pattern for single-value functions:** 2399 + 2400 + ```typescript 2401 + let isLoggedInCache: boolean | null = null 2402 + 2403 + function isLoggedIn(): boolean { 2404 + if (isLoggedInCache !== null) { 2405 + return isLoggedInCache 2406 + } 2407 + 2408 + isLoggedInCache = document.cookie.includes('auth=') 2409 + return isLoggedInCache 2410 + } 2411 + 2412 + // Clear cache when auth changes 2413 + function onAuthChange() { 2414 + isLoggedInCache = null 2415 + } 2416 + ``` 2417 + 2418 + Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components. 2419 + 2420 + Reference: [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast) 2421 + 2422 + ### 7.5 Cache Storage API Calls 2423 + 2424 + **Impact: LOW-MEDIUM (reduces expensive I/O)** 2425 + 2426 + `localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory. 2427 + 2428 + **Incorrect: reads storage on every call** 2429 + 2430 + ```typescript 2431 + function getTheme() { 2432 + return localStorage.getItem('theme') ?? 'light' 2433 + } 2434 + // Called 10 times = 10 storage reads 2435 + ``` 2436 + 2437 + **Correct: Map cache** 2438 + 2439 + ```typescript 2440 + const storageCache = new Map<string, string | null>() 2441 + 2442 + function getLocalStorage(key: string) { 2443 + if (!storageCache.has(key)) { 2444 + storageCache.set(key, localStorage.getItem(key)) 2445 + } 2446 + return storageCache.get(key) 2447 + } 2448 + 2449 + function setLocalStorage(key: string, value: string) { 2450 + localStorage.setItem(key, value) 2451 + storageCache.set(key, value) // keep cache in sync 2452 + } 2453 + ``` 2454 + 2455 + Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components. 2456 + 2457 + **Cookie caching:** 2458 + 2459 + ```typescript 2460 + let cookieCache: Record<string, string> | null = null 2461 + 2462 + function getCookie(name: string) { 2463 + if (!cookieCache) { 2464 + cookieCache = Object.fromEntries( 2465 + document.cookie.split('; ').map(c => c.split('=')) 2466 + ) 2467 + } 2468 + return cookieCache[name] 2469 + } 2470 + ``` 2471 + 2472 + **Important: invalidate on external changes** 2473 + 2474 + ```typescript 2475 + window.addEventListener('storage', (e) => { 2476 + if (e.key) storageCache.delete(e.key) 2477 + }) 2478 + 2479 + document.addEventListener('visibilitychange', () => { 2480 + if (document.visibilityState === 'visible') { 2481 + storageCache.clear() 2482 + } 2483 + }) 2484 + ``` 2485 + 2486 + If storage can change externally (another tab, server-set cookies), invalidate cache: 2487 + 2488 + ### 7.6 Combine Multiple Array Iterations 2489 + 2490 + **Impact: LOW-MEDIUM (reduces iterations)** 2491 + 2492 + Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop. 2493 + 2494 + **Incorrect: 3 iterations** 2495 + 2496 + ```typescript 2497 + const admins = users.filter(u => u.isAdmin) 2498 + const testers = users.filter(u => u.isTester) 2499 + const inactive = users.filter(u => !u.isActive) 2500 + ``` 2501 + 2502 + **Correct: 1 iteration** 2503 + 2504 + ```typescript 2505 + const admins: User[] = [] 2506 + const testers: User[] = [] 2507 + const inactive: User[] = [] 2508 + 2509 + for (const user of users) { 2510 + if (user.isAdmin) admins.push(user) 2511 + if (user.isTester) testers.push(user) 2512 + if (!user.isActive) inactive.push(user) 2513 + } 2514 + ``` 2515 + 2516 + ### 7.7 Early Length Check for Array Comparisons 2517 + 2518 + **Impact: MEDIUM-HIGH (avoids expensive operations when lengths differ)** 2519 + 2520 + When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal. 2521 + 2522 + In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops). 2523 + 2524 + **Incorrect: always runs expensive comparison** 2525 + 2526 + ```typescript 2527 + function hasChanges(current: string[], original: string[]) { 2528 + // Always sorts and joins, even when lengths differ 2529 + return current.sort().join() !== original.sort().join() 2530 + } 2531 + ``` 2532 + 2533 + Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings. 2534 + 2535 + **Correct (O(1) length check first):** 2536 + 2537 + ```typescript 2538 + function hasChanges(current: string[], original: string[]) { 2539 + // Early return if lengths differ 2540 + if (current.length !== original.length) { 2541 + return true 2542 + } 2543 + // Only sort when lengths match 2544 + const currentSorted = current.toSorted() 2545 + const originalSorted = original.toSorted() 2546 + for (let i = 0; i < currentSorted.length; i++) { 2547 + if (currentSorted[i] !== originalSorted[i]) { 2548 + return true 2549 + } 2550 + } 2551 + return false 2552 + } 2553 + ``` 2554 + 2555 + This new approach is more efficient because: 2556 + 2557 + - It avoids the overhead of sorting and joining the arrays when lengths differ 2558 + 2559 + - It avoids consuming memory for the joined strings (especially important for large arrays) 2560 + 2561 + - It avoids mutating the original arrays 2562 + 2563 + - It returns early when a difference is found 2564 + 2565 + ### 7.8 Early Return from Functions 2566 + 2567 + **Impact: LOW-MEDIUM (avoids unnecessary computation)** 2568 + 2569 + Return early when result is determined to skip unnecessary processing. 2570 + 2571 + **Incorrect: processes all items even after finding answer** 2572 + 2573 + ```typescript 2574 + function validateUsers(users: User[]) { 2575 + let hasError = false 2576 + let errorMessage = '' 2577 + 2578 + for (const user of users) { 2579 + if (!user.email) { 2580 + hasError = true 2581 + errorMessage = 'Email required' 2582 + } 2583 + if (!user.name) { 2584 + hasError = true 2585 + errorMessage = 'Name required' 2586 + } 2587 + // Continues checking all users even after error found 2588 + } 2589 + 2590 + return hasError ? { valid: false, error: errorMessage } : { valid: true } 2591 + } 2592 + ``` 2593 + 2594 + **Correct: returns immediately on first error** 2595 + 2596 + ```typescript 2597 + function validateUsers(users: User[]) { 2598 + for (const user of users) { 2599 + if (!user.email) { 2600 + return { valid: false, error: 'Email required' } 2601 + } 2602 + if (!user.name) { 2603 + return { valid: false, error: 'Name required' } 2604 + } 2605 + } 2606 + 2607 + return { valid: true } 2608 + } 2609 + ``` 2610 + 2611 + ### 7.9 Hoist RegExp Creation 2612 + 2613 + **Impact: LOW-MEDIUM (avoids recreation)** 2614 + 2615 + Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`. 2616 + 2617 + **Incorrect: new RegExp every render** 2618 + 2619 + ```tsx 2620 + function Highlighter({ text, query }: Props) { 2621 + const regex = new RegExp(`(${query})`, 'gi') 2622 + const parts = text.split(regex) 2623 + return <>{parts.map((part, i) => ...)}</> 2624 + } 2625 + ``` 2626 + 2627 + **Correct: memoize or hoist** 2628 + 2629 + ```tsx 2630 + const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ 2631 + 2632 + function Highlighter({ text, query }: Props) { 2633 + const regex = useMemo( 2634 + () => new RegExp(`(${escapeRegex(query)})`, 'gi'), 2635 + [query] 2636 + ) 2637 + const parts = text.split(regex) 2638 + return <>{parts.map((part, i) => ...)}</> 2639 + } 2640 + ``` 2641 + 2642 + **Warning: global regex has mutable state** 2643 + 2644 + ```typescript 2645 + const regex = /foo/g 2646 + regex.test('foo') // true, lastIndex = 3 2647 + regex.test('foo') // false, lastIndex = 0 2648 + ``` 2649 + 2650 + Global regex (`/g`) has mutable `lastIndex` state: 2651 + 2652 + ### 7.10 Use Loop for Min/Max Instead of Sort 2653 + 2654 + **Impact: LOW (O(n) instead of O(n log n))** 2655 + 2656 + Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower. 2657 + 2658 + **Incorrect (O(n log n) - sort to find latest):** 2659 + 2660 + ```typescript 2661 + interface Project { 2662 + id: string 2663 + name: string 2664 + updatedAt: number 2665 + } 2666 + 2667 + function getLatestProject(projects: Project[]) { 2668 + const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt) 2669 + return sorted[0] 2670 + } 2671 + ``` 2672 + 2673 + Sorts the entire array just to find the maximum value. 2674 + 2675 + **Incorrect (O(n log n) - sort for oldest and newest):** 2676 + 2677 + ```typescript 2678 + function getOldestAndNewest(projects: Project[]) { 2679 + const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt) 2680 + return { oldest: sorted[0], newest: sorted[sorted.length - 1] } 2681 + } 2682 + ``` 2683 + 2684 + Still sorts unnecessarily when only min/max are needed. 2685 + 2686 + **Correct (O(n) - single loop):** 2687 + 2688 + ```typescript 2689 + function getLatestProject(projects: Project[]) { 2690 + if (projects.length === 0) return null 2691 + 2692 + let latest = projects[0] 2693 + 2694 + for (let i = 1; i < projects.length; i++) { 2695 + if (projects[i].updatedAt > latest.updatedAt) { 2696 + latest = projects[i] 2697 + } 2698 + } 2699 + 2700 + return latest 2701 + } 2702 + 2703 + function getOldestAndNewest(projects: Project[]) { 2704 + if (projects.length === 0) return { oldest: null, newest: null } 2705 + 2706 + let oldest = projects[0] 2707 + let newest = projects[0] 2708 + 2709 + for (let i = 1; i < projects.length; i++) { 2710 + if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i] 2711 + if (projects[i].updatedAt > newest.updatedAt) newest = projects[i] 2712 + } 2713 + 2714 + return { oldest, newest } 2715 + } 2716 + ``` 2717 + 2718 + Single pass through the array, no copying, no sorting. 2719 + 2720 + **Alternative: Math.min/Math.max for small arrays** 2721 + 2722 + ```typescript 2723 + const numbers = [5, 2, 8, 1, 9] 2724 + const min = Math.min(...numbers) 2725 + const max = Math.max(...numbers) 2726 + ``` 2727 + 2728 + This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability. 2729 + 2730 + ### 7.11 Use Set/Map for O(1) Lookups 2731 + 2732 + **Impact: LOW-MEDIUM (O(n) to O(1))** 2733 + 2734 + Convert arrays to Set/Map for repeated membership checks. 2735 + 2736 + **Incorrect (O(n) per check):** 2737 + 2738 + ```typescript 2739 + const allowedIds = ['a', 'b', 'c', ...] 2740 + items.filter(item => allowedIds.includes(item.id)) 2741 + ``` 2742 + 2743 + **Correct (O(1) per check):** 2744 + 2745 + ```typescript 2746 + const allowedIds = new Set(['a', 'b', 'c', ...]) 2747 + items.filter(item => allowedIds.has(item.id)) 2748 + ``` 2749 + 2750 + ### 7.12 Use toSorted() Instead of sort() for Immutability 2751 + 2752 + **Impact: MEDIUM-HIGH (prevents mutation bugs in React state)** 2753 + 2754 + `.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation. 2755 + 2756 + **Incorrect: mutates original array** 2757 + 2758 + ```typescript 2759 + function UserList({ users }: { users: User[] }) { 2760 + // Mutates the users prop array! 2761 + const sorted = useMemo( 2762 + () => users.sort((a, b) => a.name.localeCompare(b.name)), 2763 + [users] 2764 + ) 2765 + return <div>{sorted.map(renderUser)}</div> 2766 + } 2767 + ``` 2768 + 2769 + **Correct: creates new array** 2770 + 2771 + ```typescript 2772 + function UserList({ users }: { users: User[] }) { 2773 + // Creates new sorted array, original unchanged 2774 + const sorted = useMemo( 2775 + () => users.toSorted((a, b) => a.name.localeCompare(b.name)), 2776 + [users] 2777 + ) 2778 + return <div>{sorted.map(renderUser)}</div> 2779 + } 2780 + ``` 2781 + 2782 + **Why this matters in React:** 2783 + 2784 + 1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only 2785 + 2786 + 2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior 2787 + 2788 + **Browser support: fallback for older browsers** 2789 + 2790 + ```typescript 2791 + // Fallback for older browsers 2792 + const sorted = [...items].sort((a, b) => a.value - b.value) 2793 + ``` 2794 + 2795 + `.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator: 2796 + 2797 + **Other immutable array methods:** 2798 + 2799 + - `.toSorted()` - immutable sort 2800 + 2801 + - `.toReversed()` - immutable reverse 2802 + 2803 + - `.toSpliced()` - immutable splice 2804 + 2805 + - `.with()` - immutable element replacement 2806 + 2807 + --- 2808 + 2809 + ## 8. Advanced Patterns 2810 + 2811 + **Impact: LOW** 2812 + 2813 + Advanced patterns for specific cases that require careful implementation. 2814 + 2815 + ### 8.1 Initialize App Once, Not Per Mount 2816 + 2817 + **Impact: LOW-MEDIUM (avoids duplicate init in development)** 2818 + 2819 + Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead. 2820 + 2821 + **Incorrect: runs twice in dev, re-runs on remount** 2822 + 2823 + ```tsx 2824 + function Comp() { 2825 + useEffect(() => { 2826 + loadFromStorage() 2827 + checkAuthToken() 2828 + }, []) 2829 + 2830 + // ... 2831 + } 2832 + ``` 2833 + 2834 + **Correct: once per app load** 2835 + 2836 + ```tsx 2837 + let didInit = false 2838 + 2839 + function Comp() { 2840 + useEffect(() => { 2841 + if (didInit) return 2842 + didInit = true 2843 + loadFromStorage() 2844 + checkAuthToken() 2845 + }, []) 2846 + 2847 + // ... 2848 + } 2849 + ``` 2850 + 2851 + Reference: [https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application) 2852 + 2853 + ### 8.2 Store Event Handlers in Refs 2854 + 2855 + **Impact: LOW (stable subscriptions)** 2856 + 2857 + Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes. 2858 + 2859 + **Incorrect: re-subscribes on every render** 2860 + 2861 + ```tsx 2862 + function useWindowEvent(event: string, handler: (e) => void) { 2863 + useEffect(() => { 2864 + window.addEventListener(event, handler) 2865 + return () => window.removeEventListener(event, handler) 2866 + }, [event, handler]) 2867 + } 2868 + ``` 2869 + 2870 + **Correct: stable subscription** 2871 + 2872 + ```tsx 2873 + import { useEffectEvent } from 'react' 2874 + 2875 + function useWindowEvent(event: string, handler: (e) => void) { 2876 + const onEvent = useEffectEvent(handler) 2877 + 2878 + useEffect(() => { 2879 + window.addEventListener(event, onEvent) 2880 + return () => window.removeEventListener(event, onEvent) 2881 + }, [event]) 2882 + } 2883 + ``` 2884 + 2885 + **Alternative: use `useEffectEvent` if you're on latest React:** 2886 + 2887 + `useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler. 2888 + 2889 + ### 8.3 useEffectEvent for Stable Callback Refs 2890 + 2891 + **Impact: LOW (prevents effect re-runs)** 2892 + 2893 + Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures. 2894 + 2895 + **Incorrect: effect re-runs on every callback change** 2896 + 2897 + ```tsx 2898 + function SearchInput({ onSearch }: { onSearch: (q: string) => void }) { 2899 + const [query, setQuery] = useState('') 2900 + 2901 + useEffect(() => { 2902 + const timeout = setTimeout(() => onSearch(query), 300) 2903 + return () => clearTimeout(timeout) 2904 + }, [query, onSearch]) 2905 + } 2906 + ``` 2907 + 2908 + **Correct: using React's useEffectEvent** 2909 + 2910 + ```tsx 2911 + import { useEffectEvent } from 'react'; 2912 + 2913 + function SearchInput({ onSearch }: { onSearch: (q: string) => void }) { 2914 + const [query, setQuery] = useState('') 2915 + const onSearchEvent = useEffectEvent(onSearch) 2916 + 2917 + useEffect(() => { 2918 + const timeout = setTimeout(() => onSearchEvent(query), 300) 2919 + return () => clearTimeout(timeout) 2920 + }, [query]) 2921 + } 2922 + ``` 2923 + 2924 + --- 2925 + 2926 + ## References 2927 + 2928 + 1. [https://react.dev](https://react.dev) 2929 + 2. [https://nextjs.org](https://nextjs.org) 2930 + 3. [https://swr.vercel.app](https://swr.vercel.app) 2931 + 4. [https://github.com/shuding/better-all](https://github.com/shuding/better-all) 2932 + 5. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache) 2933 + 6. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) 2934 + 7. [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
+136
.agents/skills/vercel-react-best-practices/SKILL.md
··· 1 + --- 2 + name: vercel-react-best-practices 3 + 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. 4 + license: MIT 5 + metadata: 6 + author: vercel 7 + version: "1.0.0" 8 + --- 9 + 10 + # Vercel React Best Practices 11 + 12 + Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 57 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation. 13 + 14 + ## When to Apply 15 + 16 + Reference these guidelines when: 17 + - Writing new React components or Next.js pages 18 + - Implementing data fetching (client or server-side) 19 + - Reviewing code for performance issues 20 + - Refactoring existing React/Next.js code 21 + - Optimizing bundle size or load times 22 + 23 + ## Rule Categories by Priority 24 + 25 + | Priority | Category | Impact | Prefix | 26 + |----------|----------|--------|--------| 27 + | 1 | Eliminating Waterfalls | CRITICAL | `async-` | 28 + | 2 | Bundle Size Optimization | CRITICAL | `bundle-` | 29 + | 3 | Server-Side Performance | HIGH | `server-` | 30 + | 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` | 31 + | 5 | Re-render Optimization | MEDIUM | `rerender-` | 32 + | 6 | Rendering Performance | MEDIUM | `rendering-` | 33 + | 7 | JavaScript Performance | LOW-MEDIUM | `js-` | 34 + | 8 | Advanced Patterns | LOW | `advanced-` | 35 + 36 + ## Quick Reference 37 + 38 + ### 1. Eliminating Waterfalls (CRITICAL) 39 + 40 + - `async-defer-await` - Move await into branches where actually used 41 + - `async-parallel` - Use Promise.all() for independent operations 42 + - `async-dependencies` - Use better-all for partial dependencies 43 + - `async-api-routes` - Start promises early, await late in API routes 44 + - `async-suspense-boundaries` - Use Suspense to stream content 45 + 46 + ### 2. Bundle Size Optimization (CRITICAL) 47 + 48 + - `bundle-barrel-imports` - Import directly, avoid barrel files 49 + - `bundle-dynamic-imports` - Use next/dynamic for heavy components 50 + - `bundle-defer-third-party` - Load analytics/logging after hydration 51 + - `bundle-conditional` - Load modules only when feature is activated 52 + - `bundle-preload` - Preload on hover/focus for perceived speed 53 + 54 + ### 3. Server-Side Performance (HIGH) 55 + 56 + - `server-auth-actions` - Authenticate server actions like API routes 57 + - `server-cache-react` - Use React.cache() for per-request deduplication 58 + - `server-cache-lru` - Use LRU cache for cross-request caching 59 + - `server-dedup-props` - Avoid duplicate serialization in RSC props 60 + - `server-serialization` - Minimize data passed to client components 61 + - `server-parallel-fetching` - Restructure components to parallelize fetches 62 + - `server-after-nonblocking` - Use after() for non-blocking operations 63 + 64 + ### 4. Client-Side Data Fetching (MEDIUM-HIGH) 65 + 66 + - `client-swr-dedup` - Use SWR for automatic request deduplication 67 + - `client-event-listeners` - Deduplicate global event listeners 68 + - `client-passive-event-listeners` - Use passive listeners for scroll 69 + - `client-localstorage-schema` - Version and minimize localStorage data 70 + 71 + ### 5. Re-render Optimization (MEDIUM) 72 + 73 + - `rerender-defer-reads` - Don't subscribe to state only used in callbacks 74 + - `rerender-memo` - Extract expensive work into memoized components 75 + - `rerender-memo-with-default-value` - Hoist default non-primitive props 76 + - `rerender-dependencies` - Use primitive dependencies in effects 77 + - `rerender-derived-state` - Subscribe to derived booleans, not raw values 78 + - `rerender-derived-state-no-effect` - Derive state during render, not effects 79 + - `rerender-functional-setstate` - Use functional setState for stable callbacks 80 + - `rerender-lazy-state-init` - Pass function to useState for expensive values 81 + - `rerender-simple-expression-in-memo` - Avoid memo for simple primitives 82 + - `rerender-move-effect-to-event` - Put interaction logic in event handlers 83 + - `rerender-transitions` - Use startTransition for non-urgent updates 84 + - `rerender-use-ref-transient-values` - Use refs for transient frequent values 85 + 86 + ### 6. Rendering Performance (MEDIUM) 87 + 88 + - `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element 89 + - `rendering-content-visibility` - Use content-visibility for long lists 90 + - `rendering-hoist-jsx` - Extract static JSX outside components 91 + - `rendering-svg-precision` - Reduce SVG coordinate precision 92 + - `rendering-hydration-no-flicker` - Use inline script for client-only data 93 + - `rendering-hydration-suppress-warning` - Suppress expected mismatches 94 + - `rendering-activity` - Use Activity component for show/hide 95 + - `rendering-conditional-render` - Use ternary, not && for conditionals 96 + - `rendering-usetransition-loading` - Prefer useTransition for loading state 97 + 98 + ### 7. JavaScript Performance (LOW-MEDIUM) 99 + 100 + - `js-batch-dom-css` - Group CSS changes via classes or cssText 101 + - `js-index-maps` - Build Map for repeated lookups 102 + - `js-cache-property-access` - Cache object properties in loops 103 + - `js-cache-function-results` - Cache function results in module-level Map 104 + - `js-cache-storage` - Cache localStorage/sessionStorage reads 105 + - `js-combine-iterations` - Combine multiple filter/map into one loop 106 + - `js-length-check-first` - Check array length before expensive comparison 107 + - `js-early-exit` - Return early from functions 108 + - `js-hoist-regexp` - Hoist RegExp creation outside loops 109 + - `js-min-max-loop` - Use loop for min/max instead of sort 110 + - `js-set-map-lookups` - Use Set/Map for O(1) lookups 111 + - `js-tosorted-immutable` - Use toSorted() for immutability 112 + 113 + ### 8. Advanced Patterns (LOW) 114 + 115 + - `advanced-event-handler-refs` - Store event handlers in refs 116 + - `advanced-init-once` - Initialize app once per app load 117 + - `advanced-use-latest` - useLatest for stable callback refs 118 + 119 + ## How to Use 120 + 121 + Read individual rule files for detailed explanations and code examples: 122 + 123 + ``` 124 + rules/async-parallel.md 125 + rules/bundle-barrel-imports.md 126 + ``` 127 + 128 + Each rule file contains: 129 + - Brief explanation of why it matters 130 + - Incorrect code example with explanation 131 + - Correct code example with explanation 132 + - Additional context and references 133 + 134 + ## Full Compiled Document 135 + 136 + For the complete guide with all rules expanded: `AGENTS.md`
+55
.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md
··· 1 + --- 2 + title: Store Event Handlers in Refs 3 + impact: LOW 4 + impactDescription: stable subscriptions 5 + tags: advanced, hooks, refs, event-handlers, optimization 6 + --- 7 + 8 + ## Store Event Handlers in Refs 9 + 10 + Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes. 11 + 12 + **Incorrect (re-subscribes on every render):** 13 + 14 + ```tsx 15 + function useWindowEvent(event: string, handler: (e) => void) { 16 + useEffect(() => { 17 + window.addEventListener(event, handler) 18 + return () => window.removeEventListener(event, handler) 19 + }, [event, handler]) 20 + } 21 + ``` 22 + 23 + **Correct (stable subscription):** 24 + 25 + ```tsx 26 + function useWindowEvent(event: string, handler: (e) => void) { 27 + const handlerRef = useRef(handler) 28 + useEffect(() => { 29 + handlerRef.current = handler 30 + }, [handler]) 31 + 32 + useEffect(() => { 33 + const listener = (e) => handlerRef.current(e) 34 + window.addEventListener(event, listener) 35 + return () => window.removeEventListener(event, listener) 36 + }, [event]) 37 + } 38 + ``` 39 + 40 + **Alternative: use `useEffectEvent` if you're on latest React:** 41 + 42 + ```tsx 43 + import { useEffectEvent } from 'react' 44 + 45 + function useWindowEvent(event: string, handler: (e) => void) { 46 + const onEvent = useEffectEvent(handler) 47 + 48 + useEffect(() => { 49 + window.addEventListener(event, onEvent) 50 + return () => window.removeEventListener(event, onEvent) 51 + }, [event]) 52 + } 53 + ``` 54 + 55 + `useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
+42
.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md
··· 1 + --- 2 + title: Initialize App Once, Not Per Mount 3 + impact: LOW-MEDIUM 4 + impactDescription: avoids duplicate init in development 5 + tags: initialization, useEffect, app-startup, side-effects 6 + --- 7 + 8 + ## Initialize App Once, Not Per Mount 9 + 10 + Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead. 11 + 12 + **Incorrect (runs twice in dev, re-runs on remount):** 13 + 14 + ```tsx 15 + function Comp() { 16 + useEffect(() => { 17 + loadFromStorage() 18 + checkAuthToken() 19 + }, []) 20 + 21 + // ... 22 + } 23 + ``` 24 + 25 + **Correct (once per app load):** 26 + 27 + ```tsx 28 + let didInit = false 29 + 30 + function Comp() { 31 + useEffect(() => { 32 + if (didInit) return 33 + didInit = true 34 + loadFromStorage() 35 + checkAuthToken() 36 + }, []) 37 + 38 + // ... 39 + } 40 + ``` 41 + 42 + Reference: [Initializing the application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)
+39
.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md
··· 1 + --- 2 + title: useEffectEvent for Stable Callback Refs 3 + impact: LOW 4 + impactDescription: prevents effect re-runs 5 + tags: advanced, hooks, useEffectEvent, refs, optimization 6 + --- 7 + 8 + ## useEffectEvent for Stable Callback Refs 9 + 10 + Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures. 11 + 12 + **Incorrect (effect re-runs on every callback change):** 13 + 14 + ```tsx 15 + function SearchInput({ onSearch }: { onSearch: (q: string) => void }) { 16 + const [query, setQuery] = useState('') 17 + 18 + useEffect(() => { 19 + const timeout = setTimeout(() => onSearch(query), 300) 20 + return () => clearTimeout(timeout) 21 + }, [query, onSearch]) 22 + } 23 + ``` 24 + 25 + **Correct (using React's useEffectEvent):** 26 + 27 + ```tsx 28 + import { useEffectEvent } from 'react'; 29 + 30 + function SearchInput({ onSearch }: { onSearch: (q: string) => void }) { 31 + const [query, setQuery] = useState('') 32 + const onSearchEvent = useEffectEvent(onSearch) 33 + 34 + useEffect(() => { 35 + const timeout = setTimeout(() => onSearchEvent(query), 300) 36 + return () => clearTimeout(timeout) 37 + }, [query]) 38 + } 39 + ```
+38
.agents/skills/vercel-react-best-practices/rules/async-api-routes.md
··· 1 + --- 2 + title: Prevent Waterfall Chains in API Routes 3 + impact: CRITICAL 4 + impactDescription: 2-10× improvement 5 + tags: api-routes, server-actions, waterfalls, parallelization 6 + --- 7 + 8 + ## Prevent Waterfall Chains in API Routes 9 + 10 + In API routes and Server Actions, start independent operations immediately, even if you don't await them yet. 11 + 12 + **Incorrect (config waits for auth, data waits for both):** 13 + 14 + ```typescript 15 + export async function GET(request: Request) { 16 + const session = await auth() 17 + const config = await fetchConfig() 18 + const data = await fetchData(session.user.id) 19 + return Response.json({ data, config }) 20 + } 21 + ``` 22 + 23 + **Correct (auth and config start immediately):** 24 + 25 + ```typescript 26 + export async function GET(request: Request) { 27 + const sessionPromise = auth() 28 + const configPromise = fetchConfig() 29 + const session = await sessionPromise 30 + const [config, data] = await Promise.all([ 31 + configPromise, 32 + fetchData(session.user.id) 33 + ]) 34 + return Response.json({ data, config }) 35 + } 36 + ``` 37 + 38 + For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
+80
.agents/skills/vercel-react-best-practices/rules/async-defer-await.md
··· 1 + --- 2 + title: Defer Await Until Needed 3 + impact: HIGH 4 + impactDescription: avoids blocking unused code paths 5 + tags: async, await, conditional, optimization 6 + --- 7 + 8 + ## Defer Await Until Needed 9 + 10 + Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them. 11 + 12 + **Incorrect (blocks both branches):** 13 + 14 + ```typescript 15 + async function handleRequest(userId: string, skipProcessing: boolean) { 16 + const userData = await fetchUserData(userId) 17 + 18 + if (skipProcessing) { 19 + // Returns immediately but still waited for userData 20 + return { skipped: true } 21 + } 22 + 23 + // Only this branch uses userData 24 + return processUserData(userData) 25 + } 26 + ``` 27 + 28 + **Correct (only blocks when needed):** 29 + 30 + ```typescript 31 + async function handleRequest(userId: string, skipProcessing: boolean) { 32 + if (skipProcessing) { 33 + // Returns immediately without waiting 34 + return { skipped: true } 35 + } 36 + 37 + // Fetch only when needed 38 + const userData = await fetchUserData(userId) 39 + return processUserData(userData) 40 + } 41 + ``` 42 + 43 + **Another example (early return optimization):** 44 + 45 + ```typescript 46 + // Incorrect: always fetches permissions 47 + async function updateResource(resourceId: string, userId: string) { 48 + const permissions = await fetchPermissions(userId) 49 + const resource = await getResource(resourceId) 50 + 51 + if (!resource) { 52 + return { error: 'Not found' } 53 + } 54 + 55 + if (!permissions.canEdit) { 56 + return { error: 'Forbidden' } 57 + } 58 + 59 + return await updateResourceData(resource, permissions) 60 + } 61 + 62 + // Correct: fetches only when needed 63 + async function updateResource(resourceId: string, userId: string) { 64 + const resource = await getResource(resourceId) 65 + 66 + if (!resource) { 67 + return { error: 'Not found' } 68 + } 69 + 70 + const permissions = await fetchPermissions(userId) 71 + 72 + if (!permissions.canEdit) { 73 + return { error: 'Forbidden' } 74 + } 75 + 76 + return await updateResourceData(resource, permissions) 77 + } 78 + ``` 79 + 80 + This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
+51
.agents/skills/vercel-react-best-practices/rules/async-dependencies.md
··· 1 + --- 2 + title: Dependency-Based Parallelization 3 + impact: CRITICAL 4 + impactDescription: 2-10× improvement 5 + tags: async, parallelization, dependencies, better-all 6 + --- 7 + 8 + ## Dependency-Based Parallelization 9 + 10 + For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment. 11 + 12 + **Incorrect (profile waits for config unnecessarily):** 13 + 14 + ```typescript 15 + const [user, config] = await Promise.all([ 16 + fetchUser(), 17 + fetchConfig() 18 + ]) 19 + const profile = await fetchProfile(user.id) 20 + ``` 21 + 22 + **Correct (config and profile run in parallel):** 23 + 24 + ```typescript 25 + import { all } from 'better-all' 26 + 27 + const { user, config, profile } = await all({ 28 + async user() { return fetchUser() }, 29 + async config() { return fetchConfig() }, 30 + async profile() { 31 + return fetchProfile((await this.$.user).id) 32 + } 33 + }) 34 + ``` 35 + 36 + **Alternative without extra dependencies:** 37 + 38 + We can also create all the promises first, and do `Promise.all()` at the end. 39 + 40 + ```typescript 41 + const userPromise = fetchUser() 42 + const profilePromise = userPromise.then(user => fetchProfile(user.id)) 43 + 44 + const [user, config, profile] = await Promise.all([ 45 + userPromise, 46 + fetchConfig(), 47 + profilePromise 48 + ]) 49 + ``` 50 + 51 + Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
+28
.agents/skills/vercel-react-best-practices/rules/async-parallel.md
··· 1 + --- 2 + title: Promise.all() for Independent Operations 3 + impact: CRITICAL 4 + impactDescription: 2-10× improvement 5 + tags: async, parallelization, promises, waterfalls 6 + --- 7 + 8 + ## Promise.all() for Independent Operations 9 + 10 + When async operations have no interdependencies, execute them concurrently using `Promise.all()`. 11 + 12 + **Incorrect (sequential execution, 3 round trips):** 13 + 14 + ```typescript 15 + const user = await fetchUser() 16 + const posts = await fetchPosts() 17 + const comments = await fetchComments() 18 + ``` 19 + 20 + **Correct (parallel execution, 1 round trip):** 21 + 22 + ```typescript 23 + const [user, posts, comments] = await Promise.all([ 24 + fetchUser(), 25 + fetchPosts(), 26 + fetchComments() 27 + ]) 28 + ```
+99
.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md
··· 1 + --- 2 + title: Strategic Suspense Boundaries 3 + impact: HIGH 4 + impactDescription: faster initial paint 5 + tags: async, suspense, streaming, layout-shift 6 + --- 7 + 8 + ## Strategic Suspense Boundaries 9 + 10 + Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads. 11 + 12 + **Incorrect (wrapper blocked by data fetching):** 13 + 14 + ```tsx 15 + async function Page() { 16 + const data = await fetchData() // Blocks entire page 17 + 18 + return ( 19 + <div> 20 + <div>Sidebar</div> 21 + <div>Header</div> 22 + <div> 23 + <DataDisplay data={data} /> 24 + </div> 25 + <div>Footer</div> 26 + </div> 27 + ) 28 + } 29 + ``` 30 + 31 + The entire layout waits for data even though only the middle section needs it. 32 + 33 + **Correct (wrapper shows immediately, data streams in):** 34 + 35 + ```tsx 36 + function Page() { 37 + return ( 38 + <div> 39 + <div>Sidebar</div> 40 + <div>Header</div> 41 + <div> 42 + <Suspense fallback={<Skeleton />}> 43 + <DataDisplay /> 44 + </Suspense> 45 + </div> 46 + <div>Footer</div> 47 + </div> 48 + ) 49 + } 50 + 51 + async function DataDisplay() { 52 + const data = await fetchData() // Only blocks this component 53 + return <div>{data.content}</div> 54 + } 55 + ``` 56 + 57 + Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data. 58 + 59 + **Alternative (share promise across components):** 60 + 61 + ```tsx 62 + function Page() { 63 + // Start fetch immediately, but don't await 64 + const dataPromise = fetchData() 65 + 66 + return ( 67 + <div> 68 + <div>Sidebar</div> 69 + <div>Header</div> 70 + <Suspense fallback={<Skeleton />}> 71 + <DataDisplay dataPromise={dataPromise} /> 72 + <DataSummary dataPromise={dataPromise} /> 73 + </Suspense> 74 + <div>Footer</div> 75 + </div> 76 + ) 77 + } 78 + 79 + function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) { 80 + const data = use(dataPromise) // Unwraps the promise 81 + return <div>{data.content}</div> 82 + } 83 + 84 + function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) { 85 + const data = use(dataPromise) // Reuses the same promise 86 + return <div>{data.summary}</div> 87 + } 88 + ``` 89 + 90 + Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together. 91 + 92 + **When NOT to use this pattern:** 93 + 94 + - Critical data needed for layout decisions (affects positioning) 95 + - SEO-critical content above the fold 96 + - Small, fast queries where suspense overhead isn't worth it 97 + - When you want to avoid layout shift (loading → content jump) 98 + 99 + **Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
+59
.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md
··· 1 + --- 2 + title: Avoid Barrel File Imports 3 + impact: CRITICAL 4 + impactDescription: 200-800ms import cost, slow builds 5 + tags: bundle, imports, tree-shaking, barrel-files, performance 6 + --- 7 + 8 + ## Avoid Barrel File Imports 9 + 10 + 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'`). 11 + 12 + 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. 13 + 14 + **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. 15 + 16 + **Incorrect (imports entire library):** 17 + 18 + ```tsx 19 + import { Check, X, Menu } from 'lucide-react' 20 + // Loads 1,583 modules, takes ~2.8s extra in dev 21 + // Runtime cost: 200-800ms on every cold start 22 + 23 + import { Button, TextField } from '@mui/material' 24 + // Loads 2,225 modules, takes ~4.2s extra in dev 25 + ``` 26 + 27 + **Correct (imports only what you need):** 28 + 29 + ```tsx 30 + import Check from 'lucide-react/dist/esm/icons/check' 31 + import X from 'lucide-react/dist/esm/icons/x' 32 + import Menu from 'lucide-react/dist/esm/icons/menu' 33 + // Loads only 3 modules (~2KB vs ~1MB) 34 + 35 + import Button from '@mui/material/Button' 36 + import TextField from '@mui/material/TextField' 37 + // Loads only what you use 38 + ``` 39 + 40 + **Alternative (Next.js 13.5+):** 41 + 42 + ```js 43 + // next.config.js - use optimizePackageImports 44 + module.exports = { 45 + experimental: { 46 + optimizePackageImports: ['lucide-react', '@mui/material'] 47 + } 48 + } 49 + 50 + // Then you can keep the ergonomic barrel imports: 51 + import { Check, X, Menu } from 'lucide-react' 52 + // Automatically transformed to direct imports at build time 53 + ``` 54 + 55 + Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR. 56 + 57 + 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`. 58 + 59 + Reference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
+31
.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md
··· 1 + --- 2 + title: Conditional Module Loading 3 + impact: HIGH 4 + impactDescription: loads large data only when needed 5 + tags: bundle, conditional-loading, lazy-loading 6 + --- 7 + 8 + ## Conditional Module Loading 9 + 10 + Load large data or modules only when a feature is activated. 11 + 12 + **Example (lazy-load animation frames):** 13 + 14 + ```tsx 15 + function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) { 16 + const [frames, setFrames] = useState<Frame[] | null>(null) 17 + 18 + useEffect(() => { 19 + if (enabled && !frames && typeof window !== 'undefined') { 20 + import('./animation-frames.js') 21 + .then(mod => setFrames(mod.frames)) 22 + .catch(() => setEnabled(false)) 23 + } 24 + }, [enabled, frames, setEnabled]) 25 + 26 + if (!frames) return <Skeleton /> 27 + return <Canvas frames={frames} /> 28 + } 29 + ``` 30 + 31 + The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
+49
.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md
··· 1 + --- 2 + title: Defer Non-Critical Third-Party Libraries 3 + impact: MEDIUM 4 + impactDescription: loads after hydration 5 + tags: bundle, third-party, analytics, defer 6 + --- 7 + 8 + ## Defer Non-Critical Third-Party Libraries 9 + 10 + Analytics, logging, and error tracking don't block user interaction. Load them after hydration. 11 + 12 + **Incorrect (blocks initial bundle):** 13 + 14 + ```tsx 15 + import { Analytics } from '@vercel/analytics/react' 16 + 17 + export default function RootLayout({ children }) { 18 + return ( 19 + <html> 20 + <body> 21 + {children} 22 + <Analytics /> 23 + </body> 24 + </html> 25 + ) 26 + } 27 + ``` 28 + 29 + **Correct (loads after hydration):** 30 + 31 + ```tsx 32 + import dynamic from 'next/dynamic' 33 + 34 + const Analytics = dynamic( 35 + () => import('@vercel/analytics/react').then(m => m.Analytics), 36 + { ssr: false } 37 + ) 38 + 39 + export default function RootLayout({ children }) { 40 + return ( 41 + <html> 42 + <body> 43 + {children} 44 + <Analytics /> 45 + </body> 46 + </html> 47 + ) 48 + } 49 + ```
+35
.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md
··· 1 + --- 2 + title: Dynamic Imports for Heavy Components 3 + impact: CRITICAL 4 + impactDescription: directly affects TTI and LCP 5 + tags: bundle, dynamic-import, code-splitting, next-dynamic 6 + --- 7 + 8 + ## Dynamic Imports for Heavy Components 9 + 10 + Use `next/dynamic` to lazy-load large components not needed on initial render. 11 + 12 + **Incorrect (Monaco bundles with main chunk ~300KB):** 13 + 14 + ```tsx 15 + import { MonacoEditor } from './monaco-editor' 16 + 17 + function CodePanel({ code }: { code: string }) { 18 + return <MonacoEditor value={code} /> 19 + } 20 + ``` 21 + 22 + **Correct (Monaco loads on demand):** 23 + 24 + ```tsx 25 + import dynamic from 'next/dynamic' 26 + 27 + const MonacoEditor = dynamic( 28 + () => import('./monaco-editor').then(m => m.MonacoEditor), 29 + { ssr: false } 30 + ) 31 + 32 + function CodePanel({ code }: { code: string }) { 33 + return <MonacoEditor value={code} /> 34 + } 35 + ```
+50
.agents/skills/vercel-react-best-practices/rules/bundle-preload.md
··· 1 + --- 2 + title: Preload Based on User Intent 3 + impact: MEDIUM 4 + impactDescription: reduces perceived latency 5 + tags: bundle, preload, user-intent, hover 6 + --- 7 + 8 + ## Preload Based on User Intent 9 + 10 + Preload heavy bundles before they're needed to reduce perceived latency. 11 + 12 + **Example (preload on hover/focus):** 13 + 14 + ```tsx 15 + function EditorButton({ onClick }: { onClick: () => void }) { 16 + const preload = () => { 17 + if (typeof window !== 'undefined') { 18 + void import('./monaco-editor') 19 + } 20 + } 21 + 22 + return ( 23 + <button 24 + onMouseEnter={preload} 25 + onFocus={preload} 26 + onClick={onClick} 27 + > 28 + Open Editor 29 + </button> 30 + ) 31 + } 32 + ``` 33 + 34 + **Example (preload when feature flag is enabled):** 35 + 36 + ```tsx 37 + function FlagsProvider({ children, flags }: Props) { 38 + useEffect(() => { 39 + if (flags.editorEnabled && typeof window !== 'undefined') { 40 + void import('./monaco-editor').then(mod => mod.init()) 41 + } 42 + }, [flags.editorEnabled]) 43 + 44 + return <FlagsContext.Provider value={flags}> 45 + {children} 46 + </FlagsContext.Provider> 47 + } 48 + ``` 49 + 50 + The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
+74
.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md
··· 1 + --- 2 + title: Deduplicate Global Event Listeners 3 + impact: LOW 4 + impactDescription: single listener for N components 5 + tags: client, swr, event-listeners, subscription 6 + --- 7 + 8 + ## Deduplicate Global Event Listeners 9 + 10 + Use `useSWRSubscription()` to share global event listeners across component instances. 11 + 12 + **Incorrect (N instances = N listeners):** 13 + 14 + ```tsx 15 + function useKeyboardShortcut(key: string, callback: () => void) { 16 + useEffect(() => { 17 + const handler = (e: KeyboardEvent) => { 18 + if (e.metaKey && e.key === key) { 19 + callback() 20 + } 21 + } 22 + window.addEventListener('keydown', handler) 23 + return () => window.removeEventListener('keydown', handler) 24 + }, [key, callback]) 25 + } 26 + ``` 27 + 28 + When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener. 29 + 30 + **Correct (N instances = 1 listener):** 31 + 32 + ```tsx 33 + import useSWRSubscription from 'swr/subscription' 34 + 35 + // Module-level Map to track callbacks per key 36 + const keyCallbacks = new Map<string, Set<() => void>>() 37 + 38 + function useKeyboardShortcut(key: string, callback: () => void) { 39 + // Register this callback in the Map 40 + useEffect(() => { 41 + if (!keyCallbacks.has(key)) { 42 + keyCallbacks.set(key, new Set()) 43 + } 44 + keyCallbacks.get(key)!.add(callback) 45 + 46 + return () => { 47 + const set = keyCallbacks.get(key) 48 + if (set) { 49 + set.delete(callback) 50 + if (set.size === 0) { 51 + keyCallbacks.delete(key) 52 + } 53 + } 54 + } 55 + }, [key, callback]) 56 + 57 + useSWRSubscription('global-keydown', () => { 58 + const handler = (e: KeyboardEvent) => { 59 + if (e.metaKey && keyCallbacks.has(e.key)) { 60 + keyCallbacks.get(e.key)!.forEach(cb => cb()) 61 + } 62 + } 63 + window.addEventListener('keydown', handler) 64 + return () => window.removeEventListener('keydown', handler) 65 + }) 66 + } 67 + 68 + function Profile() { 69 + // Multiple shortcuts will share the same listener 70 + useKeyboardShortcut('p', () => { /* ... */ }) 71 + useKeyboardShortcut('k', () => { /* ... */ }) 72 + // ... 73 + } 74 + ```
+71
.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md
··· 1 + --- 2 + title: Version and Minimize localStorage Data 3 + impact: MEDIUM 4 + impactDescription: prevents schema conflicts, reduces storage size 5 + tags: client, localStorage, storage, versioning, data-minimization 6 + --- 7 + 8 + ## Version and Minimize localStorage Data 9 + 10 + Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data. 11 + 12 + **Incorrect:** 13 + 14 + ```typescript 15 + // No version, stores everything, no error handling 16 + localStorage.setItem('userConfig', JSON.stringify(fullUserObject)) 17 + const data = localStorage.getItem('userConfig') 18 + ``` 19 + 20 + **Correct:** 21 + 22 + ```typescript 23 + const VERSION = 'v2' 24 + 25 + function saveConfig(config: { theme: string; language: string }) { 26 + try { 27 + localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config)) 28 + } catch { 29 + // Throws in incognito/private browsing, quota exceeded, or disabled 30 + } 31 + } 32 + 33 + function loadConfig() { 34 + try { 35 + const data = localStorage.getItem(`userConfig:${VERSION}`) 36 + return data ? JSON.parse(data) : null 37 + } catch { 38 + return null 39 + } 40 + } 41 + 42 + // Migration from v1 to v2 43 + function migrate() { 44 + try { 45 + const v1 = localStorage.getItem('userConfig:v1') 46 + if (v1) { 47 + const old = JSON.parse(v1) 48 + saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang }) 49 + localStorage.removeItem('userConfig:v1') 50 + } 51 + } catch {} 52 + } 53 + ``` 54 + 55 + **Store minimal fields from server responses:** 56 + 57 + ```typescript 58 + // User object has 20+ fields, only store what UI needs 59 + function cachePrefs(user: FullUser) { 60 + try { 61 + localStorage.setItem('prefs:v1', JSON.stringify({ 62 + theme: user.preferences.theme, 63 + notifications: user.preferences.notifications 64 + })) 65 + } catch {} 66 + } 67 + ``` 68 + 69 + **Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled. 70 + 71 + **Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.
+48
.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md
··· 1 + --- 2 + title: Use Passive Event Listeners for Scrolling Performance 3 + impact: MEDIUM 4 + impactDescription: eliminates scroll delay caused by event listeners 5 + tags: client, event-listeners, scrolling, performance, touch, wheel 6 + --- 7 + 8 + ## Use Passive Event Listeners for Scrolling Performance 9 + 10 + 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. 11 + 12 + **Incorrect:** 13 + 14 + ```typescript 15 + useEffect(() => { 16 + const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX) 17 + const handleWheel = (e: WheelEvent) => console.log(e.deltaY) 18 + 19 + document.addEventListener('touchstart', handleTouch) 20 + document.addEventListener('wheel', handleWheel) 21 + 22 + return () => { 23 + document.removeEventListener('touchstart', handleTouch) 24 + document.removeEventListener('wheel', handleWheel) 25 + } 26 + }, []) 27 + ``` 28 + 29 + **Correct:** 30 + 31 + ```typescript 32 + useEffect(() => { 33 + const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX) 34 + const handleWheel = (e: WheelEvent) => console.log(e.deltaY) 35 + 36 + document.addEventListener('touchstart', handleTouch, { passive: true }) 37 + document.addEventListener('wheel', handleWheel, { passive: true }) 38 + 39 + return () => { 40 + document.removeEventListener('touchstart', handleTouch) 41 + document.removeEventListener('wheel', handleWheel) 42 + } 43 + }, []) 44 + ``` 45 + 46 + **Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`. 47 + 48 + **Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.
+56
.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md
··· 1 + --- 2 + title: Use SWR for Automatic Deduplication 3 + impact: MEDIUM-HIGH 4 + impactDescription: automatic deduplication 5 + tags: client, swr, deduplication, data-fetching 6 + --- 7 + 8 + ## Use SWR for Automatic Deduplication 9 + 10 + SWR enables request deduplication, caching, and revalidation across component instances. 11 + 12 + **Incorrect (no deduplication, each instance fetches):** 13 + 14 + ```tsx 15 + function UserList() { 16 + const [users, setUsers] = useState([]) 17 + useEffect(() => { 18 + fetch('/api/users') 19 + .then(r => r.json()) 20 + .then(setUsers) 21 + }, []) 22 + } 23 + ``` 24 + 25 + **Correct (multiple instances share one request):** 26 + 27 + ```tsx 28 + import useSWR from 'swr' 29 + 30 + function UserList() { 31 + const { data: users } = useSWR('/api/users', fetcher) 32 + } 33 + ``` 34 + 35 + **For immutable data:** 36 + 37 + ```tsx 38 + import { useImmutableSWR } from '@/lib/swr' 39 + 40 + function StaticContent() { 41 + const { data } = useImmutableSWR('/api/config', fetcher) 42 + } 43 + ``` 44 + 45 + **For mutations:** 46 + 47 + ```tsx 48 + import { useSWRMutation } from 'swr/mutation' 49 + 50 + function UpdateButton() { 51 + const { trigger } = useSWRMutation('/api/user', updateUser) 52 + return <button onClick={() => trigger()}>Update</button> 53 + } 54 + ``` 55 + 56 + Reference: [https://swr.vercel.app](https://swr.vercel.app)
+107
.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md
··· 1 + --- 2 + title: Avoid Layout Thrashing 3 + impact: MEDIUM 4 + impactDescription: prevents forced synchronous layouts and reduces performance bottlenecks 5 + tags: javascript, dom, css, performance, reflow, layout-thrashing 6 + --- 7 + 8 + ## Avoid Layout Thrashing 9 + 10 + Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow. 11 + 12 + **This is OK (browser batches style changes):** 13 + ```typescript 14 + function updateElementStyles(element: HTMLElement) { 15 + // Each line invalidates style, but browser batches the recalculation 16 + element.style.width = '100px' 17 + element.style.height = '200px' 18 + element.style.backgroundColor = 'blue' 19 + element.style.border = '1px solid black' 20 + } 21 + ``` 22 + 23 + **Incorrect (interleaved reads and writes force reflows):** 24 + ```typescript 25 + function layoutThrashing(element: HTMLElement) { 26 + element.style.width = '100px' 27 + const width = element.offsetWidth // Forces reflow 28 + element.style.height = '200px' 29 + const height = element.offsetHeight // Forces another reflow 30 + } 31 + ``` 32 + 33 + **Correct (batch writes, then read once):** 34 + ```typescript 35 + function updateElementStyles(element: HTMLElement) { 36 + // Batch all writes together 37 + element.style.width = '100px' 38 + element.style.height = '200px' 39 + element.style.backgroundColor = 'blue' 40 + element.style.border = '1px solid black' 41 + 42 + // Read after all writes are done (single reflow) 43 + const { width, height } = element.getBoundingClientRect() 44 + } 45 + ``` 46 + 47 + **Correct (batch reads, then writes):** 48 + ```typescript 49 + function avoidThrashing(element: HTMLElement) { 50 + // Read phase - all layout queries first 51 + const rect1 = element.getBoundingClientRect() 52 + const offsetWidth = element.offsetWidth 53 + const offsetHeight = element.offsetHeight 54 + 55 + // Write phase - all style changes after 56 + element.style.width = '100px' 57 + element.style.height = '200px' 58 + } 59 + ``` 60 + 61 + **Better: use CSS classes** 62 + ```css 63 + .highlighted-box { 64 + width: 100px; 65 + height: 200px; 66 + background-color: blue; 67 + border: 1px solid black; 68 + } 69 + ``` 70 + ```typescript 71 + function updateElementStyles(element: HTMLElement) { 72 + element.classList.add('highlighted-box') 73 + 74 + const { width, height } = element.getBoundingClientRect() 75 + } 76 + ``` 77 + 78 + **React example:** 79 + ```tsx 80 + // Incorrect: interleaving style changes with layout queries 81 + function Box({ isHighlighted }: { isHighlighted: boolean }) { 82 + const ref = useRef<HTMLDivElement>(null) 83 + 84 + useEffect(() => { 85 + if (ref.current && isHighlighted) { 86 + ref.current.style.width = '100px' 87 + const width = ref.current.offsetWidth // Forces layout 88 + ref.current.style.height = '200px' 89 + } 90 + }, [isHighlighted]) 91 + 92 + return <div ref={ref}>Content</div> 93 + } 94 + 95 + // Correct: toggle class 96 + function Box({ isHighlighted }: { isHighlighted: boolean }) { 97 + return ( 98 + <div className={isHighlighted ? 'highlighted-box' : ''}> 99 + Content 100 + </div> 101 + ) 102 + } 103 + ``` 104 + 105 + Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain. 106 + 107 + See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
+80
.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md
··· 1 + --- 2 + title: Cache Repeated Function Calls 3 + impact: MEDIUM 4 + impactDescription: avoid redundant computation 5 + tags: javascript, cache, memoization, performance 6 + --- 7 + 8 + ## Cache Repeated Function Calls 9 + 10 + Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render. 11 + 12 + **Incorrect (redundant computation):** 13 + 14 + ```typescript 15 + function ProjectList({ projects }: { projects: Project[] }) { 16 + return ( 17 + <div> 18 + {projects.map(project => { 19 + // slugify() called 100+ times for same project names 20 + const slug = slugify(project.name) 21 + 22 + return <ProjectCard key={project.id} slug={slug} /> 23 + })} 24 + </div> 25 + ) 26 + } 27 + ``` 28 + 29 + **Correct (cached results):** 30 + 31 + ```typescript 32 + // Module-level cache 33 + const slugifyCache = new Map<string, string>() 34 + 35 + function cachedSlugify(text: string): string { 36 + if (slugifyCache.has(text)) { 37 + return slugifyCache.get(text)! 38 + } 39 + const result = slugify(text) 40 + slugifyCache.set(text, result) 41 + return result 42 + } 43 + 44 + function ProjectList({ projects }: { projects: Project[] }) { 45 + return ( 46 + <div> 47 + {projects.map(project => { 48 + // Computed only once per unique project name 49 + const slug = cachedSlugify(project.name) 50 + 51 + return <ProjectCard key={project.id} slug={slug} /> 52 + })} 53 + </div> 54 + ) 55 + } 56 + ``` 57 + 58 + **Simpler pattern for single-value functions:** 59 + 60 + ```typescript 61 + let isLoggedInCache: boolean | null = null 62 + 63 + function isLoggedIn(): boolean { 64 + if (isLoggedInCache !== null) { 65 + return isLoggedInCache 66 + } 67 + 68 + isLoggedInCache = document.cookie.includes('auth=') 69 + return isLoggedInCache 70 + } 71 + 72 + // Clear cache when auth changes 73 + function onAuthChange() { 74 + isLoggedInCache = null 75 + } 76 + ``` 77 + 78 + Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components. 79 + 80 + Reference: [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
+28
.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md
··· 1 + --- 2 + title: Cache Property Access in Loops 3 + impact: LOW-MEDIUM 4 + impactDescription: reduces lookups 5 + tags: javascript, loops, optimization, caching 6 + --- 7 + 8 + ## Cache Property Access in Loops 9 + 10 + Cache object property lookups in hot paths. 11 + 12 + **Incorrect (3 lookups × N iterations):** 13 + 14 + ```typescript 15 + for (let i = 0; i < arr.length; i++) { 16 + process(obj.config.settings.value) 17 + } 18 + ``` 19 + 20 + **Correct (1 lookup total):** 21 + 22 + ```typescript 23 + const value = obj.config.settings.value 24 + const len = arr.length 25 + for (let i = 0; i < len; i++) { 26 + process(value) 27 + } 28 + ```
+70
.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md
··· 1 + --- 2 + title: Cache Storage API Calls 3 + impact: LOW-MEDIUM 4 + impactDescription: reduces expensive I/O 5 + tags: javascript, localStorage, storage, caching, performance 6 + --- 7 + 8 + ## Cache Storage API Calls 9 + 10 + `localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory. 11 + 12 + **Incorrect (reads storage on every call):** 13 + 14 + ```typescript 15 + function getTheme() { 16 + return localStorage.getItem('theme') ?? 'light' 17 + } 18 + // Called 10 times = 10 storage reads 19 + ``` 20 + 21 + **Correct (Map cache):** 22 + 23 + ```typescript 24 + const storageCache = new Map<string, string | null>() 25 + 26 + function getLocalStorage(key: string) { 27 + if (!storageCache.has(key)) { 28 + storageCache.set(key, localStorage.getItem(key)) 29 + } 30 + return storageCache.get(key) 31 + } 32 + 33 + function setLocalStorage(key: string, value: string) { 34 + localStorage.setItem(key, value) 35 + storageCache.set(key, value) // keep cache in sync 36 + } 37 + ``` 38 + 39 + Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components. 40 + 41 + **Cookie caching:** 42 + 43 + ```typescript 44 + let cookieCache: Record<string, string> | null = null 45 + 46 + function getCookie(name: string) { 47 + if (!cookieCache) { 48 + cookieCache = Object.fromEntries( 49 + document.cookie.split('; ').map(c => c.split('=')) 50 + ) 51 + } 52 + return cookieCache[name] 53 + } 54 + ``` 55 + 56 + **Important (invalidate on external changes):** 57 + 58 + If storage can change externally (another tab, server-set cookies), invalidate cache: 59 + 60 + ```typescript 61 + window.addEventListener('storage', (e) => { 62 + if (e.key) storageCache.delete(e.key) 63 + }) 64 + 65 + document.addEventListener('visibilitychange', () => { 66 + if (document.visibilityState === 'visible') { 67 + storageCache.clear() 68 + } 69 + }) 70 + ```
+32
.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md
··· 1 + --- 2 + title: Combine Multiple Array Iterations 3 + impact: LOW-MEDIUM 4 + impactDescription: reduces iterations 5 + tags: javascript, arrays, loops, performance 6 + --- 7 + 8 + ## Combine Multiple Array Iterations 9 + 10 + Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop. 11 + 12 + **Incorrect (3 iterations):** 13 + 14 + ```typescript 15 + const admins = users.filter(u => u.isAdmin) 16 + const testers = users.filter(u => u.isTester) 17 + const inactive = users.filter(u => !u.isActive) 18 + ``` 19 + 20 + **Correct (1 iteration):** 21 + 22 + ```typescript 23 + const admins: User[] = [] 24 + const testers: User[] = [] 25 + const inactive: User[] = [] 26 + 27 + for (const user of users) { 28 + if (user.isAdmin) admins.push(user) 29 + if (user.isTester) testers.push(user) 30 + if (!user.isActive) inactive.push(user) 31 + } 32 + ```
+50
.agents/skills/vercel-react-best-practices/rules/js-early-exit.md
··· 1 + --- 2 + title: Early Return from Functions 3 + impact: LOW-MEDIUM 4 + impactDescription: avoids unnecessary computation 5 + tags: javascript, functions, optimization, early-return 6 + --- 7 + 8 + ## Early Return from Functions 9 + 10 + Return early when result is determined to skip unnecessary processing. 11 + 12 + **Incorrect (processes all items even after finding answer):** 13 + 14 + ```typescript 15 + function validateUsers(users: User[]) { 16 + let hasError = false 17 + let errorMessage = '' 18 + 19 + for (const user of users) { 20 + if (!user.email) { 21 + hasError = true 22 + errorMessage = 'Email required' 23 + } 24 + if (!user.name) { 25 + hasError = true 26 + errorMessage = 'Name required' 27 + } 28 + // Continues checking all users even after error found 29 + } 30 + 31 + return hasError ? { valid: false, error: errorMessage } : { valid: true } 32 + } 33 + ``` 34 + 35 + **Correct (returns immediately on first error):** 36 + 37 + ```typescript 38 + function validateUsers(users: User[]) { 39 + for (const user of users) { 40 + if (!user.email) { 41 + return { valid: false, error: 'Email required' } 42 + } 43 + if (!user.name) { 44 + return { valid: false, error: 'Name required' } 45 + } 46 + } 47 + 48 + return { valid: true } 49 + } 50 + ```
+45
.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md
··· 1 + --- 2 + title: Hoist RegExp Creation 3 + impact: LOW-MEDIUM 4 + impactDescription: avoids recreation 5 + tags: javascript, regexp, optimization, memoization 6 + --- 7 + 8 + ## Hoist RegExp Creation 9 + 10 + Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`. 11 + 12 + **Incorrect (new RegExp every render):** 13 + 14 + ```tsx 15 + function Highlighter({ text, query }: Props) { 16 + const regex = new RegExp(`(${query})`, 'gi') 17 + const parts = text.split(regex) 18 + return <>{parts.map((part, i) => ...)}</> 19 + } 20 + ``` 21 + 22 + **Correct (memoize or hoist):** 23 + 24 + ```tsx 25 + const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ 26 + 27 + function Highlighter({ text, query }: Props) { 28 + const regex = useMemo( 29 + () => new RegExp(`(${escapeRegex(query)})`, 'gi'), 30 + [query] 31 + ) 32 + const parts = text.split(regex) 33 + return <>{parts.map((part, i) => ...)}</> 34 + } 35 + ``` 36 + 37 + **Warning (global regex has mutable state):** 38 + 39 + Global regex (`/g`) has mutable `lastIndex` state: 40 + 41 + ```typescript 42 + const regex = /foo/g 43 + regex.test('foo') // true, lastIndex = 3 44 + regex.test('foo') // false, lastIndex = 0 45 + ```
+37
.agents/skills/vercel-react-best-practices/rules/js-index-maps.md
··· 1 + --- 2 + title: Build Index Maps for Repeated Lookups 3 + impact: LOW-MEDIUM 4 + impactDescription: 1M ops to 2K ops 5 + tags: javascript, map, indexing, optimization, performance 6 + --- 7 + 8 + ## Build Index Maps for Repeated Lookups 9 + 10 + Multiple `.find()` calls by the same key should use a Map. 11 + 12 + **Incorrect (O(n) per lookup):** 13 + 14 + ```typescript 15 + function processOrders(orders: Order[], users: User[]) { 16 + return orders.map(order => ({ 17 + ...order, 18 + user: users.find(u => u.id === order.userId) 19 + })) 20 + } 21 + ``` 22 + 23 + **Correct (O(1) per lookup):** 24 + 25 + ```typescript 26 + function processOrders(orders: Order[], users: User[]) { 27 + const userById = new Map(users.map(u => [u.id, u])) 28 + 29 + return orders.map(order => ({ 30 + ...order, 31 + user: userById.get(order.userId) 32 + })) 33 + } 34 + ``` 35 + 36 + Build map once (O(n)), then all lookups are O(1). 37 + For 1000 orders × 1000 users: 1M ops → 2K ops.
+49
.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md
··· 1 + --- 2 + title: Early Length Check for Array Comparisons 3 + impact: MEDIUM-HIGH 4 + impactDescription: avoids expensive operations when lengths differ 5 + tags: javascript, arrays, performance, optimization, comparison 6 + --- 7 + 8 + ## Early Length Check for Array Comparisons 9 + 10 + When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal. 11 + 12 + In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops). 13 + 14 + **Incorrect (always runs expensive comparison):** 15 + 16 + ```typescript 17 + function hasChanges(current: string[], original: string[]) { 18 + // Always sorts and joins, even when lengths differ 19 + return current.sort().join() !== original.sort().join() 20 + } 21 + ``` 22 + 23 + Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings. 24 + 25 + **Correct (O(1) length check first):** 26 + 27 + ```typescript 28 + function hasChanges(current: string[], original: string[]) { 29 + // Early return if lengths differ 30 + if (current.length !== original.length) { 31 + return true 32 + } 33 + // Only sort when lengths match 34 + const currentSorted = current.toSorted() 35 + const originalSorted = original.toSorted() 36 + for (let i = 0; i < currentSorted.length; i++) { 37 + if (currentSorted[i] !== originalSorted[i]) { 38 + return true 39 + } 40 + } 41 + return false 42 + } 43 + ``` 44 + 45 + This new approach is more efficient because: 46 + - It avoids the overhead of sorting and joining the arrays when lengths differ 47 + - It avoids consuming memory for the joined strings (especially important for large arrays) 48 + - It avoids mutating the original arrays 49 + - It returns early when a difference is found
+82
.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md
··· 1 + --- 2 + title: Use Loop for Min/Max Instead of Sort 3 + impact: LOW 4 + impactDescription: O(n) instead of O(n log n) 5 + tags: javascript, arrays, performance, sorting, algorithms 6 + --- 7 + 8 + ## Use Loop for Min/Max Instead of Sort 9 + 10 + Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower. 11 + 12 + **Incorrect (O(n log n) - sort to find latest):** 13 + 14 + ```typescript 15 + interface Project { 16 + id: string 17 + name: string 18 + updatedAt: number 19 + } 20 + 21 + function getLatestProject(projects: Project[]) { 22 + const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt) 23 + return sorted[0] 24 + } 25 + ``` 26 + 27 + Sorts the entire array just to find the maximum value. 28 + 29 + **Incorrect (O(n log n) - sort for oldest and newest):** 30 + 31 + ```typescript 32 + function getOldestAndNewest(projects: Project[]) { 33 + const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt) 34 + return { oldest: sorted[0], newest: sorted[sorted.length - 1] } 35 + } 36 + ``` 37 + 38 + Still sorts unnecessarily when only min/max are needed. 39 + 40 + **Correct (O(n) - single loop):** 41 + 42 + ```typescript 43 + function getLatestProject(projects: Project[]) { 44 + if (projects.length === 0) return null 45 + 46 + let latest = projects[0] 47 + 48 + for (let i = 1; i < projects.length; i++) { 49 + if (projects[i].updatedAt > latest.updatedAt) { 50 + latest = projects[i] 51 + } 52 + } 53 + 54 + return latest 55 + } 56 + 57 + function getOldestAndNewest(projects: Project[]) { 58 + if (projects.length === 0) return { oldest: null, newest: null } 59 + 60 + let oldest = projects[0] 61 + let newest = projects[0] 62 + 63 + for (let i = 1; i < projects.length; i++) { 64 + if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i] 65 + if (projects[i].updatedAt > newest.updatedAt) newest = projects[i] 66 + } 67 + 68 + return { oldest, newest } 69 + } 70 + ``` 71 + 72 + Single pass through the array, no copying, no sorting. 73 + 74 + **Alternative (Math.min/Math.max for small arrays):** 75 + 76 + ```typescript 77 + const numbers = [5, 2, 8, 1, 9] 78 + const min = Math.min(...numbers) 79 + const max = Math.max(...numbers) 80 + ``` 81 + 82 + This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.
+24
.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md
··· 1 + --- 2 + title: Use Set/Map for O(1) Lookups 3 + impact: LOW-MEDIUM 4 + impactDescription: O(n) to O(1) 5 + tags: javascript, set, map, data-structures, performance 6 + --- 7 + 8 + ## Use Set/Map for O(1) Lookups 9 + 10 + Convert arrays to Set/Map for repeated membership checks. 11 + 12 + **Incorrect (O(n) per check):** 13 + 14 + ```typescript 15 + const allowedIds = ['a', 'b', 'c', ...] 16 + items.filter(item => allowedIds.includes(item.id)) 17 + ``` 18 + 19 + **Correct (O(1) per check):** 20 + 21 + ```typescript 22 + const allowedIds = new Set(['a', 'b', 'c', ...]) 23 + items.filter(item => allowedIds.has(item.id)) 24 + ```
+57
.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md
··· 1 + --- 2 + title: Use toSorted() Instead of sort() for Immutability 3 + impact: MEDIUM-HIGH 4 + impactDescription: prevents mutation bugs in React state 5 + tags: javascript, arrays, immutability, react, state, mutation 6 + --- 7 + 8 + ## Use toSorted() Instead of sort() for Immutability 9 + 10 + `.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation. 11 + 12 + **Incorrect (mutates original array):** 13 + 14 + ```typescript 15 + function UserList({ users }: { users: User[] }) { 16 + // Mutates the users prop array! 17 + const sorted = useMemo( 18 + () => users.sort((a, b) => a.name.localeCompare(b.name)), 19 + [users] 20 + ) 21 + return <div>{sorted.map(renderUser)}</div> 22 + } 23 + ``` 24 + 25 + **Correct (creates new array):** 26 + 27 + ```typescript 28 + function UserList({ users }: { users: User[] }) { 29 + // Creates new sorted array, original unchanged 30 + const sorted = useMemo( 31 + () => users.toSorted((a, b) => a.name.localeCompare(b.name)), 32 + [users] 33 + ) 34 + return <div>{sorted.map(renderUser)}</div> 35 + } 36 + ``` 37 + 38 + **Why this matters in React:** 39 + 40 + 1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only 41 + 2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior 42 + 43 + **Browser support (fallback for older browsers):** 44 + 45 + `.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator: 46 + 47 + ```typescript 48 + // Fallback for older browsers 49 + const sorted = [...items].sort((a, b) => a.value - b.value) 50 + ``` 51 + 52 + **Other immutable array methods:** 53 + 54 + - `.toSorted()` - immutable sort 55 + - `.toReversed()` - immutable reverse 56 + - `.toSpliced()` - immutable splice 57 + - `.with()` - immutable element replacement
+26
.agents/skills/vercel-react-best-practices/rules/rendering-activity.md
··· 1 + --- 2 + title: Use Activity Component for Show/Hide 3 + impact: MEDIUM 4 + impactDescription: preserves state/DOM 5 + tags: rendering, activity, visibility, state-preservation 6 + --- 7 + 8 + ## Use Activity Component for Show/Hide 9 + 10 + Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility. 11 + 12 + **Usage:** 13 + 14 + ```tsx 15 + import { Activity } from 'react' 16 + 17 + function Dropdown({ isOpen }: Props) { 18 + return ( 19 + <Activity mode={isOpen ? 'visible' : 'hidden'}> 20 + <ExpensiveMenu /> 21 + </Activity> 22 + ) 23 + } 24 + ``` 25 + 26 + Avoids expensive re-renders and state loss.
+47
.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md
··· 1 + --- 2 + title: Animate SVG Wrapper Instead of SVG Element 3 + impact: LOW 4 + impactDescription: enables hardware acceleration 5 + tags: rendering, svg, css, animation, performance 6 + --- 7 + 8 + ## Animate SVG Wrapper Instead of SVG Element 9 + 10 + Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead. 11 + 12 + **Incorrect (animating SVG directly - no hardware acceleration):** 13 + 14 + ```tsx 15 + function LoadingSpinner() { 16 + return ( 17 + <svg 18 + className="animate-spin" 19 + width="24" 20 + height="24" 21 + viewBox="0 0 24 24" 22 + > 23 + <circle cx="12" cy="12" r="10" stroke="currentColor" /> 24 + </svg> 25 + ) 26 + } 27 + ``` 28 + 29 + **Correct (animating wrapper div - hardware accelerated):** 30 + 31 + ```tsx 32 + function LoadingSpinner() { 33 + return ( 34 + <div className="animate-spin"> 35 + <svg 36 + width="24" 37 + height="24" 38 + viewBox="0 0 24 24" 39 + > 40 + <circle cx="12" cy="12" r="10" stroke="currentColor" /> 41 + </svg> 42 + </div> 43 + ) 44 + } 45 + ``` 46 + 47 + This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
+40
.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md
··· 1 + --- 2 + title: Use Explicit Conditional Rendering 3 + impact: LOW 4 + impactDescription: prevents rendering 0 or NaN 5 + tags: rendering, conditional, jsx, falsy-values 6 + --- 7 + 8 + ## Use Explicit Conditional Rendering 9 + 10 + Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render. 11 + 12 + **Incorrect (renders "0" when count is 0):** 13 + 14 + ```tsx 15 + function Badge({ count }: { count: number }) { 16 + return ( 17 + <div> 18 + {count && <span className="badge">{count}</span>} 19 + </div> 20 + ) 21 + } 22 + 23 + // When count = 0, renders: <div>0</div> 24 + // When count = 5, renders: <div><span class="badge">5</span></div> 25 + ``` 26 + 27 + **Correct (renders nothing when count is 0):** 28 + 29 + ```tsx 30 + function Badge({ count }: { count: number }) { 31 + return ( 32 + <div> 33 + {count > 0 ? <span className="badge">{count}</span> : null} 34 + </div> 35 + ) 36 + } 37 + 38 + // When count = 0, renders: <div></div> 39 + // When count = 5, renders: <div><span class="badge">5</span></div> 40 + ```
+38
.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md
··· 1 + --- 2 + title: CSS content-visibility for Long Lists 3 + impact: HIGH 4 + impactDescription: faster initial render 5 + tags: rendering, css, content-visibility, long-lists 6 + --- 7 + 8 + ## CSS content-visibility for Long Lists 9 + 10 + Apply `content-visibility: auto` to defer off-screen rendering. 11 + 12 + **CSS:** 13 + 14 + ```css 15 + .message-item { 16 + content-visibility: auto; 17 + contain-intrinsic-size: 0 80px; 18 + } 19 + ``` 20 + 21 + **Example:** 22 + 23 + ```tsx 24 + function MessageList({ messages }: { messages: Message[] }) { 25 + return ( 26 + <div className="overflow-y-auto h-screen"> 27 + {messages.map(msg => ( 28 + <div key={msg.id} className="message-item"> 29 + <Avatar user={msg.author} /> 30 + <div>{msg.content}</div> 31 + </div> 32 + ))} 33 + </div> 34 + ) 35 + } 36 + ``` 37 + 38 + For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
+46
.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md
··· 1 + --- 2 + title: Hoist Static JSX Elements 3 + impact: LOW 4 + impactDescription: avoids re-creation 5 + tags: rendering, jsx, static, optimization 6 + --- 7 + 8 + ## Hoist Static JSX Elements 9 + 10 + Extract static JSX outside components to avoid re-creation. 11 + 12 + **Incorrect (recreates element every render):** 13 + 14 + ```tsx 15 + function LoadingSkeleton() { 16 + return <div className="animate-pulse h-20 bg-gray-200" /> 17 + } 18 + 19 + function Container() { 20 + return ( 21 + <div> 22 + {loading && <LoadingSkeleton />} 23 + </div> 24 + ) 25 + } 26 + ``` 27 + 28 + **Correct (reuses same element):** 29 + 30 + ```tsx 31 + const loadingSkeleton = ( 32 + <div className="animate-pulse h-20 bg-gray-200" /> 33 + ) 34 + 35 + function Container() { 36 + return ( 37 + <div> 38 + {loading && loadingSkeleton} 39 + </div> 40 + ) 41 + } 42 + ``` 43 + 44 + This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render. 45 + 46 + **Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
+82
.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md
··· 1 + --- 2 + title: Prevent Hydration Mismatch Without Flickering 3 + impact: MEDIUM 4 + impactDescription: avoids visual flicker and hydration errors 5 + tags: rendering, ssr, hydration, localStorage, flicker 6 + --- 7 + 8 + ## Prevent Hydration Mismatch Without Flickering 9 + 10 + When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates. 11 + 12 + **Incorrect (breaks SSR):** 13 + 14 + ```tsx 15 + function ThemeWrapper({ children }: { children: ReactNode }) { 16 + // localStorage is not available on server - throws error 17 + const theme = localStorage.getItem('theme') || 'light' 18 + 19 + return ( 20 + <div className={theme}> 21 + {children} 22 + </div> 23 + ) 24 + } 25 + ``` 26 + 27 + Server-side rendering will fail because `localStorage` is undefined. 28 + 29 + **Incorrect (visual flickering):** 30 + 31 + ```tsx 32 + function ThemeWrapper({ children }: { children: ReactNode }) { 33 + const [theme, setTheme] = useState('light') 34 + 35 + useEffect(() => { 36 + // Runs after hydration - causes visible flash 37 + const stored = localStorage.getItem('theme') 38 + if (stored) { 39 + setTheme(stored) 40 + } 41 + }, []) 42 + 43 + return ( 44 + <div className={theme}> 45 + {children} 46 + </div> 47 + ) 48 + } 49 + ``` 50 + 51 + Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content. 52 + 53 + **Correct (no flicker, no hydration mismatch):** 54 + 55 + ```tsx 56 + function ThemeWrapper({ children }: { children: ReactNode }) { 57 + return ( 58 + <> 59 + <div id="theme-wrapper"> 60 + {children} 61 + </div> 62 + <script 63 + dangerouslySetInnerHTML={{ 64 + __html: ` 65 + (function() { 66 + try { 67 + var theme = localStorage.getItem('theme') || 'light'; 68 + var el = document.getElementById('theme-wrapper'); 69 + if (el) el.className = theme; 70 + } catch (e) {} 71 + })(); 72 + `, 73 + }} 74 + /> 75 + </> 76 + ) 77 + } 78 + ``` 79 + 80 + The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch. 81 + 82 + This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
+30
.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md
··· 1 + --- 2 + title: Suppress Expected Hydration Mismatches 3 + impact: LOW-MEDIUM 4 + impactDescription: avoids noisy hydration warnings for known differences 5 + tags: rendering, hydration, ssr, nextjs 6 + --- 7 + 8 + ## Suppress Expected Hydration Mismatches 9 + 10 + In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it. 11 + 12 + **Incorrect (known mismatch warnings):** 13 + 14 + ```tsx 15 + function Timestamp() { 16 + return <span>{new Date().toLocaleString()}</span> 17 + } 18 + ``` 19 + 20 + **Correct (suppress expected mismatch only):** 21 + 22 + ```tsx 23 + function Timestamp() { 24 + return ( 25 + <span suppressHydrationWarning> 26 + {new Date().toLocaleString()} 27 + </span> 28 + ) 29 + } 30 + ```
+28
.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md
··· 1 + --- 2 + title: Optimize SVG Precision 3 + impact: LOW 4 + impactDescription: reduces file size 5 + tags: rendering, svg, optimization, svgo 6 + --- 7 + 8 + ## Optimize SVG Precision 9 + 10 + Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered. 11 + 12 + **Incorrect (excessive precision):** 13 + 14 + ```svg 15 + <path d="M 10.293847 20.847362 L 30.938472 40.192837" /> 16 + ``` 17 + 18 + **Correct (1 decimal place):** 19 + 20 + ```svg 21 + <path d="M 10.3 20.8 L 30.9 40.2" /> 22 + ``` 23 + 24 + **Automate with SVGO:** 25 + 26 + ```bash 27 + npx svgo --precision=1 --multipass icon.svg 28 + ```
+75
.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md
··· 1 + --- 2 + title: Use useTransition Over Manual Loading States 3 + impact: LOW 4 + impactDescription: reduces re-renders and improves code clarity 5 + tags: rendering, transitions, useTransition, loading, state 6 + --- 7 + 8 + ## Use useTransition Over Manual Loading States 9 + 10 + Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions. 11 + 12 + **Incorrect (manual loading state):** 13 + 14 + ```tsx 15 + function SearchResults() { 16 + const [query, setQuery] = useState('') 17 + const [results, setResults] = useState([]) 18 + const [isLoading, setIsLoading] = useState(false) 19 + 20 + const handleSearch = async (value: string) => { 21 + setIsLoading(true) 22 + setQuery(value) 23 + const data = await fetchResults(value) 24 + setResults(data) 25 + setIsLoading(false) 26 + } 27 + 28 + return ( 29 + <> 30 + <input onChange={(e) => handleSearch(e.target.value)} /> 31 + {isLoading && <Spinner />} 32 + <ResultsList results={results} /> 33 + </> 34 + ) 35 + } 36 + ``` 37 + 38 + **Correct (useTransition with built-in pending state):** 39 + 40 + ```tsx 41 + import { useTransition, useState } from 'react' 42 + 43 + function SearchResults() { 44 + const [query, setQuery] = useState('') 45 + const [results, setResults] = useState([]) 46 + const [isPending, startTransition] = useTransition() 47 + 48 + const handleSearch = (value: string) => { 49 + setQuery(value) // Update input immediately 50 + 51 + startTransition(async () => { 52 + // Fetch and update results 53 + const data = await fetchResults(value) 54 + setResults(data) 55 + }) 56 + } 57 + 58 + return ( 59 + <> 60 + <input onChange={(e) => handleSearch(e.target.value)} /> 61 + {isPending && <Spinner />} 62 + <ResultsList results={results} /> 63 + </> 64 + ) 65 + } 66 + ``` 67 + 68 + **Benefits:** 69 + 70 + - **Automatic pending state**: No need to manually manage `setIsLoading(true/false)` 71 + - **Error resilience**: Pending state correctly resets even if the transition throws 72 + - **Better responsiveness**: Keeps the UI responsive during updates 73 + - **Interrupt handling**: New transitions automatically cancel pending ones 74 + 75 + Reference: [useTransition](https://react.dev/reference/react/useTransition)
+39
.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md
··· 1 + --- 2 + title: Defer State Reads to Usage Point 3 + impact: MEDIUM 4 + impactDescription: avoids unnecessary subscriptions 5 + tags: rerender, searchParams, localStorage, optimization 6 + --- 7 + 8 + ## Defer State Reads to Usage Point 9 + 10 + Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks. 11 + 12 + **Incorrect (subscribes to all searchParams changes):** 13 + 14 + ```tsx 15 + function ShareButton({ chatId }: { chatId: string }) { 16 + const searchParams = useSearchParams() 17 + 18 + const handleShare = () => { 19 + const ref = searchParams.get('ref') 20 + shareChat(chatId, { ref }) 21 + } 22 + 23 + return <button onClick={handleShare}>Share</button> 24 + } 25 + ``` 26 + 27 + **Correct (reads on demand, no subscription):** 28 + 29 + ```tsx 30 + function ShareButton({ chatId }: { chatId: string }) { 31 + const handleShare = () => { 32 + const params = new URLSearchParams(window.location.search) 33 + const ref = params.get('ref') 34 + shareChat(chatId, { ref }) 35 + } 36 + 37 + return <button onClick={handleShare}>Share</button> 38 + } 39 + ```
+45
.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md
··· 1 + --- 2 + title: Narrow Effect Dependencies 3 + impact: LOW 4 + impactDescription: minimizes effect re-runs 5 + tags: rerender, useEffect, dependencies, optimization 6 + --- 7 + 8 + ## Narrow Effect Dependencies 9 + 10 + Specify primitive dependencies instead of objects to minimize effect re-runs. 11 + 12 + **Incorrect (re-runs on any user field change):** 13 + 14 + ```tsx 15 + useEffect(() => { 16 + console.log(user.id) 17 + }, [user]) 18 + ``` 19 + 20 + **Correct (re-runs only when id changes):** 21 + 22 + ```tsx 23 + useEffect(() => { 24 + console.log(user.id) 25 + }, [user.id]) 26 + ``` 27 + 28 + **For derived state, compute outside effect:** 29 + 30 + ```tsx 31 + // Incorrect: runs on width=767, 766, 765... 32 + useEffect(() => { 33 + if (width < 768) { 34 + enableMobileMode() 35 + } 36 + }, [width]) 37 + 38 + // Correct: runs only on boolean transition 39 + const isMobile = width < 768 40 + useEffect(() => { 41 + if (isMobile) { 42 + enableMobileMode() 43 + } 44 + }, [isMobile]) 45 + ```
+40
.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md
··· 1 + --- 2 + title: Calculate Derived State During Rendering 3 + impact: MEDIUM 4 + impactDescription: avoids redundant renders and state drift 5 + tags: rerender, derived-state, useEffect, state 6 + --- 7 + 8 + ## Calculate Derived State During Rendering 9 + 10 + If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead. 11 + 12 + **Incorrect (redundant state and effect):** 13 + 14 + ```tsx 15 + function Form() { 16 + const [firstName, setFirstName] = useState('First') 17 + const [lastName, setLastName] = useState('Last') 18 + const [fullName, setFullName] = useState('') 19 + 20 + useEffect(() => { 21 + setFullName(firstName + ' ' + lastName) 22 + }, [firstName, lastName]) 23 + 24 + return <p>{fullName}</p> 25 + } 26 + ``` 27 + 28 + **Correct (derive during render):** 29 + 30 + ```tsx 31 + function Form() { 32 + const [firstName, setFirstName] = useState('First') 33 + const [lastName, setLastName] = useState('Last') 34 + const fullName = firstName + ' ' + lastName 35 + 36 + return <p>{fullName}</p> 37 + } 38 + ``` 39 + 40 + References: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
+29
.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md
··· 1 + --- 2 + title: Subscribe to Derived State 3 + impact: MEDIUM 4 + impactDescription: reduces re-render frequency 5 + tags: rerender, derived-state, media-query, optimization 6 + --- 7 + 8 + ## Subscribe to Derived State 9 + 10 + Subscribe to derived boolean state instead of continuous values to reduce re-render frequency. 11 + 12 + **Incorrect (re-renders on every pixel change):** 13 + 14 + ```tsx 15 + function Sidebar() { 16 + const width = useWindowWidth() // updates continuously 17 + const isMobile = width < 768 18 + return <nav className={isMobile ? 'mobile' : 'desktop'} /> 19 + } 20 + ``` 21 + 22 + **Correct (re-renders only when boolean changes):** 23 + 24 + ```tsx 25 + function Sidebar() { 26 + const isMobile = useMediaQuery('(max-width: 767px)') 27 + return <nav className={isMobile ? 'mobile' : 'desktop'} /> 28 + } 29 + ```
+74
.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md
··· 1 + --- 2 + title: Use Functional setState Updates 3 + impact: MEDIUM 4 + impactDescription: prevents stale closures and unnecessary callback recreations 5 + tags: react, hooks, useState, useCallback, callbacks, closures 6 + --- 7 + 8 + ## Use Functional setState Updates 9 + 10 + 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. 11 + 12 + **Incorrect (requires state as dependency):** 13 + 14 + ```tsx 15 + function TodoList() { 16 + const [items, setItems] = useState(initialItems) 17 + 18 + // Callback must depend on items, recreated on every items change 19 + const addItems = useCallback((newItems: Item[]) => { 20 + setItems([...items, ...newItems]) 21 + }, [items]) // ❌ items dependency causes recreations 22 + 23 + // Risk of stale closure if dependency is forgotten 24 + const removeItem = useCallback((id: string) => { 25 + setItems(items.filter(item => item.id !== id)) 26 + }, []) // ❌ Missing items dependency - will use stale items! 27 + 28 + return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} /> 29 + } 30 + ``` 31 + 32 + 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. 33 + 34 + **Correct (stable callbacks, no stale closures):** 35 + 36 + ```tsx 37 + function TodoList() { 38 + const [items, setItems] = useState(initialItems) 39 + 40 + // Stable callback, never recreated 41 + const addItems = useCallback((newItems: Item[]) => { 42 + setItems(curr => [...curr, ...newItems]) 43 + }, []) // ✅ No dependencies needed 44 + 45 + // Always uses latest state, no stale closure risk 46 + const removeItem = useCallback((id: string) => { 47 + setItems(curr => curr.filter(item => item.id !== id)) 48 + }, []) // ✅ Safe and stable 49 + 50 + return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} /> 51 + } 52 + ``` 53 + 54 + **Benefits:** 55 + 56 + 1. **Stable callback references** - Callbacks don't need to be recreated when state changes 57 + 2. **No stale closures** - Always operates on the latest state value 58 + 3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks 59 + 4. **Prevents bugs** - Eliminates the most common source of React closure bugs 60 + 61 + **When to use functional updates:** 62 + 63 + - Any setState that depends on the current state value 64 + - Inside useCallback/useMemo when state is needed 65 + - Event handlers that reference state 66 + - Async operations that update state 67 + 68 + **When direct updates are fine:** 69 + 70 + - Setting state to a static value: `setCount(0)` 71 + - Setting state from props/arguments only: `setName(newName)` 72 + - State doesn't depend on previous value 73 + 74 + **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.
+58
.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md
··· 1 + --- 2 + title: Use Lazy State Initialization 3 + impact: MEDIUM 4 + impactDescription: wasted computation on every render 5 + tags: react, hooks, useState, performance, initialization 6 + --- 7 + 8 + ## Use Lazy State Initialization 9 + 10 + 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. 11 + 12 + **Incorrect (runs on every render):** 13 + 14 + ```tsx 15 + function FilteredList({ items }: { items: Item[] }) { 16 + // buildSearchIndex() runs on EVERY render, even after initialization 17 + const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items)) 18 + const [query, setQuery] = useState('') 19 + 20 + // When query changes, buildSearchIndex runs again unnecessarily 21 + return <SearchResults index={searchIndex} query={query} /> 22 + } 23 + 24 + function UserProfile() { 25 + // JSON.parse runs on every render 26 + const [settings, setSettings] = useState( 27 + JSON.parse(localStorage.getItem('settings') || '{}') 28 + ) 29 + 30 + return <SettingsForm settings={settings} onChange={setSettings} /> 31 + } 32 + ``` 33 + 34 + **Correct (runs only once):** 35 + 36 + ```tsx 37 + function FilteredList({ items }: { items: Item[] }) { 38 + // buildSearchIndex() runs ONLY on initial render 39 + const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items)) 40 + const [query, setQuery] = useState('') 41 + 42 + return <SearchResults index={searchIndex} query={query} /> 43 + } 44 + 45 + function UserProfile() { 46 + // JSON.parse runs only on initial render 47 + const [settings, setSettings] = useState(() => { 48 + const stored = localStorage.getItem('settings') 49 + return stored ? JSON.parse(stored) : {} 50 + }) 51 + 52 + return <SettingsForm settings={settings} onChange={setSettings} /> 53 + } 54 + ``` 55 + 56 + Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations. 57 + 58 + For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
+38
.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md
··· 1 + --- 2 + 3 + title: Extract Default Non-primitive Parameter Value from Memoized Component to Constant 4 + impact: MEDIUM 5 + impactDescription: restores memoization by using a constant for default value 6 + tags: rerender, memo, optimization 7 + 8 + --- 9 + 10 + ## Extract Default Non-primitive Parameter Value from Memoized Component to Constant 11 + 12 + 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()`. 13 + 14 + To address this issue, extract the default value into a constant. 15 + 16 + **Incorrect (`onClick` has different values on every rerender):** 17 + 18 + ```tsx 19 + const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) { 20 + // ... 21 + }) 22 + 23 + // Used without optional onClick 24 + <UserAvatar /> 25 + ``` 26 + 27 + **Correct (stable default value):** 28 + 29 + ```tsx 30 + const NOOP = () => {}; 31 + 32 + const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) { 33 + // ... 34 + }) 35 + 36 + // Used without optional onClick 37 + <UserAvatar /> 38 + ```
+44
.agents/skills/vercel-react-best-practices/rules/rerender-memo.md
··· 1 + --- 2 + title: Extract to Memoized Components 3 + impact: MEDIUM 4 + impactDescription: enables early returns 5 + tags: rerender, memo, useMemo, optimization 6 + --- 7 + 8 + ## Extract to Memoized Components 9 + 10 + Extract expensive work into memoized components to enable early returns before computation. 11 + 12 + **Incorrect (computes avatar even when loading):** 13 + 14 + ```tsx 15 + function Profile({ user, loading }: Props) { 16 + const avatar = useMemo(() => { 17 + const id = computeAvatarId(user) 18 + return <Avatar id={id} /> 19 + }, [user]) 20 + 21 + if (loading) return <Skeleton /> 22 + return <div>{avatar}</div> 23 + } 24 + ``` 25 + 26 + **Correct (skips computation when loading):** 27 + 28 + ```tsx 29 + const UserAvatar = memo(function UserAvatar({ user }: { user: User }) { 30 + const id = useMemo(() => computeAvatarId(user), [user]) 31 + return <Avatar id={id} /> 32 + }) 33 + 34 + function Profile({ user, loading }: Props) { 35 + if (loading) return <Skeleton /> 36 + return ( 37 + <div> 38 + <UserAvatar user={user} /> 39 + </div> 40 + ) 41 + } 42 + ``` 43 + 44 + **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.
+45
.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md
··· 1 + --- 2 + title: Put Interaction Logic in Event Handlers 3 + impact: MEDIUM 4 + impactDescription: avoids effect re-runs and duplicate side effects 5 + tags: rerender, useEffect, events, side-effects, dependencies 6 + --- 7 + 8 + ## Put Interaction Logic in Event Handlers 9 + 10 + If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action. 11 + 12 + **Incorrect (event modeled as state + effect):** 13 + 14 + ```tsx 15 + function Form() { 16 + const [submitted, setSubmitted] = useState(false) 17 + const theme = useContext(ThemeContext) 18 + 19 + useEffect(() => { 20 + if (submitted) { 21 + post('/api/register') 22 + showToast('Registered', theme) 23 + } 24 + }, [submitted, theme]) 25 + 26 + return <button onClick={() => setSubmitted(true)}>Submit</button> 27 + } 28 + ``` 29 + 30 + **Correct (do it in the handler):** 31 + 32 + ```tsx 33 + function Form() { 34 + const theme = useContext(ThemeContext) 35 + 36 + function handleSubmit() { 37 + post('/api/register') 38 + showToast('Registered', theme) 39 + } 40 + 41 + return <button onClick={handleSubmit}>Submit</button> 42 + } 43 + ``` 44 + 45 + Reference: [Should this code move to an event handler?](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
+35
.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md
··· 1 + --- 2 + title: Do not wrap a simple expression with a primitive result type in useMemo 3 + impact: LOW-MEDIUM 4 + impactDescription: wasted computation on every render 5 + tags: rerender, useMemo, optimization 6 + --- 7 + 8 + ## Do not wrap a simple expression with a primitive result type in useMemo 9 + 10 + When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`. 11 + Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself. 12 + 13 + **Incorrect:** 14 + 15 + ```tsx 16 + function Header({ user, notifications }: Props) { 17 + const isLoading = useMemo(() => { 18 + return user.isLoading || notifications.isLoading 19 + }, [user.isLoading, notifications.isLoading]) 20 + 21 + if (isLoading) return <Skeleton /> 22 + // return some markup 23 + } 24 + ``` 25 + 26 + **Correct:** 27 + 28 + ```tsx 29 + function Header({ user, notifications }: Props) { 30 + const isLoading = user.isLoading || notifications.isLoading 31 + 32 + if (isLoading) return <Skeleton /> 33 + // return some markup 34 + } 35 + ```
+40
.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md
··· 1 + --- 2 + title: Use Transitions for Non-Urgent Updates 3 + impact: MEDIUM 4 + impactDescription: maintains UI responsiveness 5 + tags: rerender, transitions, startTransition, performance 6 + --- 7 + 8 + ## Use Transitions for Non-Urgent Updates 9 + 10 + Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness. 11 + 12 + **Incorrect (blocks UI on every scroll):** 13 + 14 + ```tsx 15 + function ScrollTracker() { 16 + const [scrollY, setScrollY] = useState(0) 17 + useEffect(() => { 18 + const handler = () => setScrollY(window.scrollY) 19 + window.addEventListener('scroll', handler, { passive: true }) 20 + return () => window.removeEventListener('scroll', handler) 21 + }, []) 22 + } 23 + ``` 24 + 25 + **Correct (non-blocking updates):** 26 + 27 + ```tsx 28 + import { startTransition } from 'react' 29 + 30 + function ScrollTracker() { 31 + const [scrollY, setScrollY] = useState(0) 32 + useEffect(() => { 33 + const handler = () => { 34 + startTransition(() => setScrollY(window.scrollY)) 35 + } 36 + window.addEventListener('scroll', handler, { passive: true }) 37 + return () => window.removeEventListener('scroll', handler) 38 + }, []) 39 + } 40 + ```
+73
.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md
··· 1 + --- 2 + title: Use useRef for Transient Values 3 + impact: MEDIUM 4 + impactDescription: avoids unnecessary re-renders on frequent updates 5 + tags: rerender, useref, state, performance 6 + --- 7 + 8 + ## Use useRef for Transient Values 9 + 10 + When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render. 11 + 12 + **Incorrect (renders every update):** 13 + 14 + ```tsx 15 + function Tracker() { 16 + const [lastX, setLastX] = useState(0) 17 + 18 + useEffect(() => { 19 + const onMove = (e: MouseEvent) => setLastX(e.clientX) 20 + window.addEventListener('mousemove', onMove) 21 + return () => window.removeEventListener('mousemove', onMove) 22 + }, []) 23 + 24 + return ( 25 + <div 26 + style={{ 27 + position: 'fixed', 28 + top: 0, 29 + left: lastX, 30 + width: 8, 31 + height: 8, 32 + background: 'black', 33 + }} 34 + /> 35 + ) 36 + } 37 + ``` 38 + 39 + **Correct (no re-render for tracking):** 40 + 41 + ```tsx 42 + function Tracker() { 43 + const lastXRef = useRef(0) 44 + const dotRef = useRef<HTMLDivElement>(null) 45 + 46 + useEffect(() => { 47 + const onMove = (e: MouseEvent) => { 48 + lastXRef.current = e.clientX 49 + const node = dotRef.current 50 + if (node) { 51 + node.style.transform = `translateX(${e.clientX}px)` 52 + } 53 + } 54 + window.addEventListener('mousemove', onMove) 55 + return () => window.removeEventListener('mousemove', onMove) 56 + }, []) 57 + 58 + return ( 59 + <div 60 + ref={dotRef} 61 + style={{ 62 + position: 'fixed', 63 + top: 0, 64 + left: 0, 65 + width: 8, 66 + height: 8, 67 + background: 'black', 68 + transform: 'translateX(0px)', 69 + }} 70 + /> 71 + ) 72 + } 73 + ```
+73
.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md
··· 1 + --- 2 + title: Use after() for Non-Blocking Operations 3 + impact: MEDIUM 4 + impactDescription: faster response times 5 + tags: server, async, logging, analytics, side-effects 6 + --- 7 + 8 + ## Use after() for Non-Blocking Operations 9 + 10 + 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. 11 + 12 + **Incorrect (blocks response):** 13 + 14 + ```tsx 15 + import { logUserAction } from '@/app/utils' 16 + 17 + export async function POST(request: Request) { 18 + // Perform mutation 19 + await updateDatabase(request) 20 + 21 + // Logging blocks the response 22 + const userAgent = request.headers.get('user-agent') || 'unknown' 23 + await logUserAction({ userAgent }) 24 + 25 + return new Response(JSON.stringify({ status: 'success' }), { 26 + status: 200, 27 + headers: { 'Content-Type': 'application/json' } 28 + }) 29 + } 30 + ``` 31 + 32 + **Correct (non-blocking):** 33 + 34 + ```tsx 35 + import { after } from 'next/server' 36 + import { headers, cookies } from 'next/headers' 37 + import { logUserAction } from '@/app/utils' 38 + 39 + export async function POST(request: Request) { 40 + // Perform mutation 41 + await updateDatabase(request) 42 + 43 + // Log after response is sent 44 + after(async () => { 45 + const userAgent = (await headers()).get('user-agent') || 'unknown' 46 + const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous' 47 + 48 + logUserAction({ sessionCookie, userAgent }) 49 + }) 50 + 51 + return new Response(JSON.stringify({ status: 'success' }), { 52 + status: 200, 53 + headers: { 'Content-Type': 'application/json' } 54 + }) 55 + } 56 + ``` 57 + 58 + The response is sent immediately while logging happens in the background. 59 + 60 + **Common use cases:** 61 + 62 + - Analytics tracking 63 + - Audit logging 64 + - Sending notifications 65 + - Cache invalidation 66 + - Cleanup tasks 67 + 68 + **Important notes:** 69 + 70 + - `after()` runs even if the response fails or redirects 71 + - Works in Server Actions, Route Handlers, and Server Components 72 + 73 + Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
+96
.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md
··· 1 + --- 2 + title: Authenticate Server Actions Like API Routes 3 + impact: CRITICAL 4 + impactDescription: prevents unauthorized access to server mutations 5 + tags: server, server-actions, authentication, security, authorization 6 + --- 7 + 8 + ## Authenticate Server Actions Like API Routes 9 + 10 + **Impact: CRITICAL (prevents unauthorized access to server mutations)** 11 + 12 + 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. 13 + 14 + 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." 15 + 16 + **Incorrect (no authentication check):** 17 + 18 + ```typescript 19 + 'use server' 20 + 21 + export async function deleteUser(userId: string) { 22 + // Anyone can call this! No auth check 23 + await db.user.delete({ where: { id: userId } }) 24 + return { success: true } 25 + } 26 + ``` 27 + 28 + **Correct (authentication inside the action):** 29 + 30 + ```typescript 31 + 'use server' 32 + 33 + import { verifySession } from '@/lib/auth' 34 + import { unauthorized } from '@/lib/errors' 35 + 36 + export async function deleteUser(userId: string) { 37 + // Always check auth inside the action 38 + const session = await verifySession() 39 + 40 + if (!session) { 41 + throw unauthorized('Must be logged in') 42 + } 43 + 44 + // Check authorization too 45 + if (session.user.role !== 'admin' && session.user.id !== userId) { 46 + throw unauthorized('Cannot delete other users') 47 + } 48 + 49 + await db.user.delete({ where: { id: userId } }) 50 + return { success: true } 51 + } 52 + ``` 53 + 54 + **With input validation:** 55 + 56 + ```typescript 57 + 'use server' 58 + 59 + import { verifySession } from '@/lib/auth' 60 + import { z } from 'zod' 61 + 62 + const updateProfileSchema = z.object({ 63 + userId: z.string().uuid(), 64 + name: z.string().min(1).max(100), 65 + email: z.string().email() 66 + }) 67 + 68 + export async function updateProfile(data: unknown) { 69 + // Validate input first 70 + const validated = updateProfileSchema.parse(data) 71 + 72 + // Then authenticate 73 + const session = await verifySession() 74 + if (!session) { 75 + throw new Error('Unauthorized') 76 + } 77 + 78 + // Then authorize 79 + if (session.user.id !== validated.userId) { 80 + throw new Error('Can only update own profile') 81 + } 82 + 83 + // Finally perform the mutation 84 + await db.user.update({ 85 + where: { id: validated.userId }, 86 + data: { 87 + name: validated.name, 88 + email: validated.email 89 + } 90 + }) 91 + 92 + return { success: true } 93 + } 94 + ``` 95 + 96 + Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
+41
.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md
··· 1 + --- 2 + title: Cross-Request LRU Caching 3 + impact: HIGH 4 + impactDescription: caches across requests 5 + tags: server, cache, lru, cross-request 6 + --- 7 + 8 + ## Cross-Request LRU Caching 9 + 10 + `React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache. 11 + 12 + **Implementation:** 13 + 14 + ```typescript 15 + import { LRUCache } from 'lru-cache' 16 + 17 + const cache = new LRUCache<string, any>({ 18 + max: 1000, 19 + ttl: 5 * 60 * 1000 // 5 minutes 20 + }) 21 + 22 + export async function getUser(id: string) { 23 + const cached = cache.get(id) 24 + if (cached) return cached 25 + 26 + const user = await db.user.findUnique({ where: { id } }) 27 + cache.set(id, user) 28 + return user 29 + } 30 + 31 + // Request 1: DB query, result cached 32 + // Request 2: cache hit, no DB query 33 + ``` 34 + 35 + Use when sequential user actions hit multiple endpoints needing the same data within seconds. 36 + 37 + **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. 38 + 39 + **In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching. 40 + 41 + Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
+76
.agents/skills/vercel-react-best-practices/rules/server-cache-react.md
··· 1 + --- 2 + title: Per-Request Deduplication with React.cache() 3 + impact: MEDIUM 4 + impactDescription: deduplicates within request 5 + tags: server, cache, react-cache, deduplication 6 + --- 7 + 8 + ## Per-Request Deduplication with React.cache() 9 + 10 + Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most. 11 + 12 + **Usage:** 13 + 14 + ```typescript 15 + import { cache } from 'react' 16 + 17 + export const getCurrentUser = cache(async () => { 18 + const session = await auth() 19 + if (!session?.user?.id) return null 20 + return await db.user.findUnique({ 21 + where: { id: session.user.id } 22 + }) 23 + }) 24 + ``` 25 + 26 + Within a single request, multiple calls to `getCurrentUser()` execute the query only once. 27 + 28 + **Avoid inline objects as arguments:** 29 + 30 + `React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits. 31 + 32 + **Incorrect (always cache miss):** 33 + 34 + ```typescript 35 + const getUser = cache(async (params: { uid: number }) => { 36 + return await db.user.findUnique({ where: { id: params.uid } }) 37 + }) 38 + 39 + // Each call creates new object, never hits cache 40 + getUser({ uid: 1 }) 41 + getUser({ uid: 1 }) // Cache miss, runs query again 42 + ``` 43 + 44 + **Correct (cache hit):** 45 + 46 + ```typescript 47 + const getUser = cache(async (uid: number) => { 48 + return await db.user.findUnique({ where: { id: uid } }) 49 + }) 50 + 51 + // Primitive args use value equality 52 + getUser(1) 53 + getUser(1) // Cache hit, returns cached result 54 + ``` 55 + 56 + If you must pass objects, pass the same reference: 57 + 58 + ```typescript 59 + const params = { uid: 1 } 60 + getUser(params) // Query runs 61 + getUser(params) // Cache hit (same reference) 62 + ``` 63 + 64 + **Next.js-Specific Note:** 65 + 66 + 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: 67 + 68 + - Database queries (Prisma, Drizzle, etc.) 69 + - Heavy computations 70 + - Authentication checks 71 + - File system operations 72 + - Any non-fetch async work 73 + 74 + Use `React.cache()` to deduplicate these operations across your component tree. 75 + 76 + Reference: [React.cache documentation](https://react.dev/reference/react/cache)
+65
.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md
··· 1 + --- 2 + title: Avoid Duplicate Serialization in RSC Props 3 + impact: LOW 4 + impactDescription: reduces network payload by avoiding duplicate serialization 5 + tags: server, rsc, serialization, props, client-components 6 + --- 7 + 8 + ## Avoid Duplicate Serialization in RSC Props 9 + 10 + **Impact: LOW (reduces network payload by avoiding duplicate serialization)** 11 + 12 + RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server. 13 + 14 + **Incorrect (duplicates array):** 15 + 16 + ```tsx 17 + // RSC: sends 6 strings (2 arrays × 3 items) 18 + <ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} /> 19 + ``` 20 + 21 + **Correct (sends 3 strings):** 22 + 23 + ```tsx 24 + // RSC: send once 25 + <ClientList usernames={usernames} /> 26 + 27 + // Client: transform there 28 + 'use client' 29 + const sorted = useMemo(() => [...usernames].sort(), [usernames]) 30 + ``` 31 + 32 + **Nested deduplication behavior:** 33 + 34 + Deduplication works recursively. Impact varies by data type: 35 + 36 + - `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated 37 + - `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference 38 + 39 + ```tsx 40 + // string[] - duplicates everything 41 + usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings 42 + 43 + // object[] - duplicates array structure only 44 + users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4) 45 + ``` 46 + 47 + **Operations breaking deduplication (create new references):** 48 + 49 + - Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]` 50 + - Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())` 51 + 52 + **More examples:** 53 + 54 + ```tsx 55 + // ❌ Bad 56 + <C users={users} active={users.filter(u => u.active)} /> 57 + <C product={product} productName={product.name} /> 58 + 59 + // ✅ Good 60 + <C users={users} /> 61 + <C product={product} /> 62 + // Do filtering/destructuring in client 63 + ``` 64 + 65 + **Exception:** Pass derived data when transformation is expensive or client doesn't need original.
+83
.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md
··· 1 + --- 2 + title: Parallel Data Fetching with Component Composition 3 + impact: CRITICAL 4 + impactDescription: eliminates server-side waterfalls 5 + tags: server, rsc, parallel-fetching, composition 6 + --- 7 + 8 + ## Parallel Data Fetching with Component Composition 9 + 10 + React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching. 11 + 12 + **Incorrect (Sidebar waits for Page's fetch to complete):** 13 + 14 + ```tsx 15 + export default async function Page() { 16 + const header = await fetchHeader() 17 + return ( 18 + <div> 19 + <div>{header}</div> 20 + <Sidebar /> 21 + </div> 22 + ) 23 + } 24 + 25 + async function Sidebar() { 26 + const items = await fetchSidebarItems() 27 + return <nav>{items.map(renderItem)}</nav> 28 + } 29 + ``` 30 + 31 + **Correct (both fetch simultaneously):** 32 + 33 + ```tsx 34 + async function Header() { 35 + const data = await fetchHeader() 36 + return <div>{data}</div> 37 + } 38 + 39 + async function Sidebar() { 40 + const items = await fetchSidebarItems() 41 + return <nav>{items.map(renderItem)}</nav> 42 + } 43 + 44 + export default function Page() { 45 + return ( 46 + <div> 47 + <Header /> 48 + <Sidebar /> 49 + </div> 50 + ) 51 + } 52 + ``` 53 + 54 + **Alternative with children prop:** 55 + 56 + ```tsx 57 + async function Header() { 58 + const data = await fetchHeader() 59 + return <div>{data}</div> 60 + } 61 + 62 + async function Sidebar() { 63 + const items = await fetchSidebarItems() 64 + return <nav>{items.map(renderItem)}</nav> 65 + } 66 + 67 + function Layout({ children }: { children: ReactNode }) { 68 + return ( 69 + <div> 70 + <Header /> 71 + {children} 72 + </div> 73 + ) 74 + } 75 + 76 + export default function Page() { 77 + return ( 78 + <Layout> 79 + <Sidebar /> 80 + </Layout> 81 + ) 82 + } 83 + ```
+38
.agents/skills/vercel-react-best-practices/rules/server-serialization.md
··· 1 + --- 2 + title: Minimize Serialization at RSC Boundaries 3 + impact: HIGH 4 + impactDescription: reduces data transfer size 5 + tags: server, rsc, serialization, props 6 + --- 7 + 8 + ## Minimize Serialization at RSC Boundaries 9 + 10 + 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. 11 + 12 + **Incorrect (serializes all 50 fields):** 13 + 14 + ```tsx 15 + async function Page() { 16 + const user = await fetchUser() // 50 fields 17 + return <Profile user={user} /> 18 + } 19 + 20 + 'use client' 21 + function Profile({ user }: { user: User }) { 22 + return <div>{user.name}</div> // uses 1 field 23 + } 24 + ``` 25 + 26 + **Correct (serializes only 1 field):** 27 + 28 + ```tsx 29 + async function Page() { 30 + const user = await fetchUser() 31 + return <Profile name={user.name} /> 32 + } 33 + 34 + 'use client' 35 + function Profile({ name }: { name: string }) { 36 + return <div>{name}</div> 37 + } 38 + ```
+39
.agents/skills/web-design-guidelines/SKILL.md
··· 1 + --- 2 + name: web-design-guidelines 3 + description: Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices". 4 + metadata: 5 + author: vercel 6 + version: "1.0.0" 7 + argument-hint: <file-or-pattern> 8 + --- 9 + 10 + # Web Interface Guidelines 11 + 12 + Review files for compliance with Web Interface Guidelines. 13 + 14 + ## How It Works 15 + 16 + 1. Fetch the latest guidelines from the source URL below 17 + 2. Read the specified files (or prompt user for files/pattern) 18 + 3. Check against all rules in the fetched guidelines 19 + 4. Output findings in the terse `file:line` format 20 + 21 + ## Guidelines Source 22 + 23 + Fetch fresh guidelines before each review: 24 + 25 + ``` 26 + https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md 27 + ``` 28 + 29 + Use WebFetch to retrieve the latest rules. The fetched content contains all the rules and output format instructions. 30 + 31 + ## Usage 32 + 33 + When a user provides a file or pattern argument: 34 + 1. Fetch guidelines from the source URL above 35 + 2. Read the specified files 36 + 3. Apply all rules from the fetched guidelines 37 + 4. Output findings using the format specified in the guidelines 38 + 39 + If no files specified, ask the user which files to review.
+1
.claude/skills/copywriting
··· 1 + ../../.agents/skills/copywriting
+1
.claude/skills/find-skills
··· 1 + ../../.agents/skills/find-skills
+1
.claude/skills/hono
··· 1 + ../../.agents/skills/hono
+1
.claude/skills/skill-creator
··· 1 + ../../.agents/skills/skill-creator
+1
.claude/skills/vercel-composition-patterns
··· 1 + ../../.agents/skills/vercel-composition-patterns
+1
.claude/skills/vercel-react-best-practices
··· 1 + ../../.agents/skills/vercel-react-best-practices
+1
.claude/skills/web-design-guidelines
··· 1 + ../../.agents/skills/web-design-guidelines