dev-booster 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/README.md +203 -0
  2. package/bin/dev-booster.js +4 -0
  3. package/package.json +41 -0
  4. package/src/index.js +78 -0
  5. package/src/sync-template.js +55 -0
  6. package/template/.devbooster/MANIFEST.md +177 -0
  7. package/template/.devbooster/boosters/accessibility.md +63 -0
  8. package/template/.devbooster/boosters/advisor.md +68 -0
  9. package/template/.devbooster/boosters/atomic.md +58 -0
  10. package/template/.devbooster/boosters/backend.md +75 -0
  11. package/template/.devbooster/boosters/changelog.md +126 -0
  12. package/template/.devbooster/boosters/create.md +85 -0
  13. package/template/.devbooster/boosters/debug.md +55 -0
  14. package/template/.devbooster/boosters/deploy.md +11 -0
  15. package/template/.devbooster/boosters/design.md +56 -0
  16. package/template/.devbooster/boosters/discovery.md +75 -0
  17. package/template/.devbooster/boosters/documentation.md +106 -0
  18. package/template/.devbooster/boosters/frontend.md +82 -0
  19. package/template/.devbooster/boosters/i18n.md +63 -0
  20. package/template/.devbooster/boosters/implementation.md +82 -0
  21. package/template/.devbooster/boosters/investigation.md +96 -0
  22. package/template/.devbooster/boosters/mobile.md +56 -0
  23. package/template/.devbooster/boosters/performance.md +70 -0
  24. package/template/.devbooster/boosters/planning.md +92 -0
  25. package/template/.devbooster/boosters/refactor.md +17 -0
  26. package/template/.devbooster/boosters/review.md +88 -0
  27. package/template/.devbooster/boosters/security.md +60 -0
  28. package/template/.devbooster/boosters/seo.md +59 -0
  29. package/template/.devbooster/boosters/templates/implementation_heavy.md +130 -0
  30. package/template/.devbooster/boosters/templates/implementation_simple.md +109 -0
  31. package/template/.devbooster/boosters/templates/implementation_standard.md +134 -0
  32. package/template/.devbooster/boosters/testing.md +76 -0
  33. package/template/.devbooster/hub/personas/agent_backend-specialist.md +263 -0
  34. package/template/.devbooster/hub/personas/agent_code-archaeologist.md +106 -0
  35. package/template/.devbooster/hub/personas/agent_database-architect.md +226 -0
  36. package/template/.devbooster/hub/personas/agent_debugger.md +225 -0
  37. package/template/.devbooster/hub/personas/agent_devops-engineer.md +242 -0
  38. package/template/.devbooster/hub/personas/agent_documentation-writer.md +104 -0
  39. package/template/.devbooster/hub/personas/agent_explorer-agent.md +73 -0
  40. package/template/.devbooster/hub/personas/agent_frontend-specialist.md +593 -0
  41. package/template/.devbooster/hub/personas/agent_game-developer.md +162 -0
  42. package/template/.devbooster/hub/personas/agent_mobile-developer.md +377 -0
  43. package/template/.devbooster/hub/personas/agent_orchestrator.md +416 -0
  44. package/template/.devbooster/hub/personas/agent_penetration-tester.md +188 -0
  45. package/template/.devbooster/hub/personas/agent_performance-optimizer.md +187 -0
  46. package/template/.devbooster/hub/personas/agent_product-manager.md +112 -0
  47. package/template/.devbooster/hub/personas/agent_product-owner.md +95 -0
  48. package/template/.devbooster/hub/personas/agent_project-planner.md +406 -0
  49. package/template/.devbooster/hub/personas/agent_qa-automation-engineer.md +103 -0
  50. package/template/.devbooster/hub/personas/agent_security-auditor.md +170 -0
  51. package/template/.devbooster/hub/personas/agent_seo-specialist.md +111 -0
  52. package/template/.devbooster/hub/personas/agent_test-engineer.md +158 -0
  53. package/template/.devbooster/hub/personas/backend-specialist.md +263 -0
  54. package/template/.devbooster/hub/personas/brainstorm.md +113 -0
  55. package/template/.devbooster/hub/personas/code-archaeologist.md +106 -0
  56. package/template/.devbooster/hub/personas/create.md +59 -0
  57. package/template/.devbooster/hub/personas/database-architect.md +226 -0
  58. package/template/.devbooster/hub/personas/debug.md +103 -0
  59. package/template/.devbooster/hub/personas/debugger.md +225 -0
  60. package/template/.devbooster/hub/personas/deploy.md +176 -0
  61. package/template/.devbooster/hub/personas/devops-engineer.md +242 -0
  62. package/template/.devbooster/hub/personas/documentation-writer.md +104 -0
  63. package/template/.devbooster/hub/personas/enhance.md +63 -0
  64. package/template/.devbooster/hub/personas/explorer-agent.md +73 -0
  65. package/template/.devbooster/hub/personas/frontend-specialist.md +593 -0
  66. package/template/.devbooster/hub/personas/game-developer.md +162 -0
  67. package/template/.devbooster/hub/personas/mobile-developer.md +377 -0
  68. package/template/.devbooster/hub/personas/orchestrate.md +237 -0
  69. package/template/.devbooster/hub/personas/orchestrator.md +416 -0
  70. package/template/.devbooster/hub/personas/penetration-tester.md +188 -0
  71. package/template/.devbooster/hub/personas/performance-optimizer.md +187 -0
  72. package/template/.devbooster/hub/personas/plan.md +89 -0
  73. package/template/.devbooster/hub/personas/preview.md +81 -0
  74. package/template/.devbooster/hub/personas/product-manager.md +112 -0
  75. package/template/.devbooster/hub/personas/product-owner.md +95 -0
  76. package/template/.devbooster/hub/personas/project-planner.md +406 -0
  77. package/template/.devbooster/hub/personas/qa-automation-engineer.md +103 -0
  78. package/template/.devbooster/hub/personas/security-auditor.md +170 -0
  79. package/template/.devbooster/hub/personas/seo-specialist.md +111 -0
  80. package/template/.devbooster/hub/personas/skill_api-patterns.md +567 -0
  81. package/template/.devbooster/hub/personas/skill_app-builder.md +1855 -0
  82. package/template/.devbooster/hub/personas/skill_architecture.md +417 -0
  83. package/template/.devbooster/hub/personas/skill_bash-linux.md +199 -0
  84. package/template/.devbooster/hub/personas/skill_behavioral-modes.md +242 -0
  85. package/template/.devbooster/hub/personas/skill_brainstorming.md +519 -0
  86. package/template/.devbooster/hub/personas/skill_clean-code.md +201 -0
  87. package/template/.devbooster/hub/personas/skill_code-review-checklist.md +109 -0
  88. package/template/.devbooster/hub/personas/skill_database-design.md +340 -0
  89. package/template/.devbooster/hub/personas/skill_deployment-procedures.md +241 -0
  90. package/template/.devbooster/hub/personas/skill_doc.md +177 -0
  91. package/template/.devbooster/hub/personas/skill_documentation-templates.md +194 -0
  92. package/template/.devbooster/hub/personas/skill_frontend-design.md +3670 -0
  93. package/template/.devbooster/hub/personas/skill_game-development.md +167 -0
  94. package/template/.devbooster/hub/personas/skill_geo-fundamentals.md +156 -0
  95. package/template/.devbooster/hub/personas/skill_i18n-localization.md +154 -0
  96. package/template/.devbooster/hub/personas/skill_intelligent-routing.md +335 -0
  97. package/template/.devbooster/hub/personas/skill_lint-and-validate.md +45 -0
  98. package/template/.devbooster/hub/personas/skill_mcp-builder.md +176 -0
  99. package/template/.devbooster/hub/personas/skill_mobile-design.md +6150 -0
  100. package/template/.devbooster/hub/personas/skill_nextjs-react-expert.md +3487 -0
  101. package/template/.devbooster/hub/personas/skill_nodejs-best-practices.md +333 -0
  102. package/template/.devbooster/hub/personas/skill_parallel-agents.md +175 -0
  103. package/template/.devbooster/hub/personas/skill_performance-profiling.md +143 -0
  104. package/template/.devbooster/hub/personas/skill_plan-writing.md +152 -0
  105. package/template/.devbooster/hub/personas/skill_powershell-windows.md +167 -0
  106. package/template/.devbooster/hub/personas/skill_python-patterns.md +441 -0
  107. package/template/.devbooster/hub/personas/skill_red-team-tactics.md +199 -0
  108. package/template/.devbooster/hub/personas/skill_rust-pro.md +176 -0
  109. package/template/.devbooster/hub/personas/skill_seo-fundamentals.md +129 -0
  110. package/template/.devbooster/hub/personas/skill_server-management.md +161 -0
  111. package/template/.devbooster/hub/personas/skill_systematic-debugging.md +109 -0
  112. package/template/.devbooster/hub/personas/skill_tailwind-patterns.md +269 -0
  113. package/template/.devbooster/hub/personas/skill_tdd-workflow.md +149 -0
  114. package/template/.devbooster/hub/personas/skill_testing-patterns.md +178 -0
  115. package/template/.devbooster/hub/personas/skill_vulnerability-scanner.md +403 -0
  116. package/template/.devbooster/hub/personas/skill_web-design-guidelines.md +57 -0
  117. package/template/.devbooster/hub/personas/skill_webapp-testing.md +187 -0
  118. package/template/.devbooster/hub/personas/status.md +86 -0
  119. package/template/.devbooster/hub/personas/test-engineer.md +158 -0
  120. package/template/.devbooster/hub/personas/test.md +144 -0
  121. package/template/.devbooster/hub/personas/ui-ux-pro-max.md +296 -0
  122. package/template/.devbooster/hub/scripts/accessibility_checker.py +183 -0
  123. package/template/.devbooster/hub/scripts/api_validator.py +211 -0
  124. package/template/.devbooster/hub/scripts/auto_preview.py +148 -0
  125. package/template/.devbooster/hub/scripts/checklist.py +217 -0
  126. package/template/.devbooster/hub/scripts/convert_rules.py +222 -0
  127. package/template/.devbooster/hub/scripts/geo_checker.py +289 -0
  128. package/template/.devbooster/hub/scripts/i18n_checker.py +241 -0
  129. package/template/.devbooster/hub/scripts/lighthouse_audit.py +76 -0
  130. package/template/.devbooster/hub/scripts/lint_runner.py +184 -0
  131. package/template/.devbooster/hub/scripts/mobile_audit.py +670 -0
  132. package/template/.devbooster/hub/scripts/playwright_runner.py +173 -0
  133. package/template/.devbooster/hub/scripts/react_performance_checker.py +252 -0
  134. package/template/.devbooster/hub/scripts/schema_validator.py +172 -0
  135. package/template/.devbooster/hub/scripts/security_scan.py +458 -0
  136. package/template/.devbooster/hub/scripts/seo_checker.py +219 -0
  137. package/template/.devbooster/hub/scripts/session_manager.py +120 -0
  138. package/template/.devbooster/hub/scripts/test_runner.py +219 -0
  139. package/template/.devbooster/hub/scripts/type_coverage.py +173 -0
  140. package/template/.devbooster/hub/scripts/ux_audit.py +722 -0
  141. package/template/.devbooster/hub/scripts/verify_all.py +327 -0
  142. package/template/.devbooster/hub/skills/api-patterns/SKILL.md +81 -0
  143. package/template/.devbooster/hub/skills/api-patterns/api-style.md +42 -0
  144. package/template/.devbooster/hub/skills/api-patterns/auth.md +24 -0
  145. package/template/.devbooster/hub/skills/api-patterns/documentation.md +26 -0
  146. package/template/.devbooster/hub/skills/api-patterns/graphql.md +41 -0
  147. package/template/.devbooster/hub/skills/api-patterns/rate-limiting.md +31 -0
  148. package/template/.devbooster/hub/skills/api-patterns/response.md +37 -0
  149. package/template/.devbooster/hub/skills/api-patterns/rest.md +40 -0
  150. package/template/.devbooster/hub/skills/api-patterns/scripts/api_validator.py +211 -0
  151. package/template/.devbooster/hub/skills/api-patterns/security-testing.md +122 -0
  152. package/template/.devbooster/hub/skills/api-patterns/trpc.md +41 -0
  153. package/template/.devbooster/hub/skills/api-patterns/versioning.md +22 -0
  154. package/template/.devbooster/hub/skills/app-builder/SKILL.md +75 -0
  155. package/template/.devbooster/hub/skills/app-builder/agent-coordination.md +71 -0
  156. package/template/.devbooster/hub/skills/app-builder/feature-building.md +53 -0
  157. package/template/.devbooster/hub/skills/app-builder/project-detection.md +34 -0
  158. package/template/.devbooster/hub/skills/app-builder/scaffolding.md +118 -0
  159. package/template/.devbooster/hub/skills/app-builder/tech-stack.md +40 -0
  160. package/template/.devbooster/hub/skills/app-builder/templates/SKILL.md +39 -0
  161. package/template/.devbooster/hub/skills/app-builder/templates/astro-static/TEMPLATE.md +76 -0
  162. package/template/.devbooster/hub/skills/app-builder/templates/chrome-extension/TEMPLATE.md +92 -0
  163. package/template/.devbooster/hub/skills/app-builder/templates/cli-tool/TEMPLATE.md +88 -0
  164. package/template/.devbooster/hub/skills/app-builder/templates/electron-desktop/TEMPLATE.md +88 -0
  165. package/template/.devbooster/hub/skills/app-builder/templates/express-api/TEMPLATE.md +83 -0
  166. package/template/.devbooster/hub/skills/app-builder/templates/flutter-app/TEMPLATE.md +90 -0
  167. package/template/.devbooster/hub/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +90 -0
  168. package/template/.devbooster/hub/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +122 -0
  169. package/template/.devbooster/hub/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +122 -0
  170. package/template/.devbooster/hub/skills/app-builder/templates/nextjs-static/TEMPLATE.md +169 -0
  171. package/template/.devbooster/hub/skills/app-builder/templates/nuxt-app/TEMPLATE.md +134 -0
  172. package/template/.devbooster/hub/skills/app-builder/templates/python-fastapi/TEMPLATE.md +83 -0
  173. package/template/.devbooster/hub/skills/app-builder/templates/react-native-app/TEMPLATE.md +119 -0
  174. package/template/.devbooster/hub/skills/architecture/SKILL.md +55 -0
  175. package/template/.devbooster/hub/skills/architecture/context-discovery.md +43 -0
  176. package/template/.devbooster/hub/skills/architecture/examples.md +94 -0
  177. package/template/.devbooster/hub/skills/architecture/pattern-selection.md +68 -0
  178. package/template/.devbooster/hub/skills/architecture/patterns-reference.md +50 -0
  179. package/template/.devbooster/hub/skills/architecture/trade-off-analysis.md +77 -0
  180. package/template/.devbooster/hub/skills/bash-linux/SKILL.md +199 -0
  181. package/template/.devbooster/hub/skills/behavioral-modes/SKILL.md +242 -0
  182. package/template/.devbooster/hub/skills/brainstorming/SKILL.md +163 -0
  183. package/template/.devbooster/hub/skills/brainstorming/dynamic-questioning.md +350 -0
  184. package/template/.devbooster/hub/skills/clean-code/SKILL.md +201 -0
  185. package/template/.devbooster/hub/skills/code-review-checklist/SKILL.md +109 -0
  186. package/template/.devbooster/hub/skills/database-design/SKILL.md +52 -0
  187. package/template/.devbooster/hub/skills/database-design/database-selection.md +43 -0
  188. package/template/.devbooster/hub/skills/database-design/indexing.md +39 -0
  189. package/template/.devbooster/hub/skills/database-design/migrations.md +48 -0
  190. package/template/.devbooster/hub/skills/database-design/optimization.md +36 -0
  191. package/template/.devbooster/hub/skills/database-design/orm-selection.md +30 -0
  192. package/template/.devbooster/hub/skills/database-design/schema-design.md +56 -0
  193. package/template/.devbooster/hub/skills/database-design/scripts/schema_validator.py +172 -0
  194. package/template/.devbooster/hub/skills/deployment-procedures/SKILL.md +241 -0
  195. package/template/.devbooster/hub/skills/doc.md +177 -0
  196. package/template/.devbooster/hub/skills/documentation-templates/SKILL.md +194 -0
  197. package/template/.devbooster/hub/skills/frontend-design/SKILL.md +418 -0
  198. package/template/.devbooster/hub/skills/frontend-design/animation-guide.md +331 -0
  199. package/template/.devbooster/hub/skills/frontend-design/anti-generic-guide.md +252 -0
  200. package/template/.devbooster/hub/skills/frontend-design/color-system.md +311 -0
  201. package/template/.devbooster/hub/skills/frontend-design/decision-trees.md +418 -0
  202. package/template/.devbooster/hub/skills/frontend-design/motion-graphics.md +306 -0
  203. package/template/.devbooster/hub/skills/frontend-design/scripts/accessibility_checker.py +183 -0
  204. package/template/.devbooster/hub/skills/frontend-design/scripts/ux_audit.py +722 -0
  205. package/template/.devbooster/hub/skills/frontend-design/typography-system.md +345 -0
  206. package/template/.devbooster/hub/skills/frontend-design/ux-psychology.md +1116 -0
  207. package/template/.devbooster/hub/skills/frontend-design/visual-effects.md +383 -0
  208. package/template/.devbooster/hub/skills/game-development/2d-games/SKILL.md +119 -0
  209. package/template/.devbooster/hub/skills/game-development/3d-games/SKILL.md +135 -0
  210. package/template/.devbooster/hub/skills/game-development/SKILL.md +167 -0
  211. package/template/.devbooster/hub/skills/game-development/game-art/SKILL.md +185 -0
  212. package/template/.devbooster/hub/skills/game-development/game-audio/SKILL.md +190 -0
  213. package/template/.devbooster/hub/skills/game-development/game-design/SKILL.md +129 -0
  214. package/template/.devbooster/hub/skills/game-development/mobile-games/SKILL.md +108 -0
  215. package/template/.devbooster/hub/skills/game-development/multiplayer/SKILL.md +132 -0
  216. package/template/.devbooster/hub/skills/game-development/pc-games/SKILL.md +144 -0
  217. package/template/.devbooster/hub/skills/game-development/vr-ar/SKILL.md +123 -0
  218. package/template/.devbooster/hub/skills/game-development/web-games/SKILL.md +150 -0
  219. package/template/.devbooster/hub/skills/geo-fundamentals/SKILL.md +156 -0
  220. package/template/.devbooster/hub/skills/geo-fundamentals/scripts/geo_checker.py +289 -0
  221. package/template/.devbooster/hub/skills/i18n-localization/SKILL.md +154 -0
  222. package/template/.devbooster/hub/skills/i18n-localization/scripts/i18n_checker.py +241 -0
  223. package/template/.devbooster/hub/skills/intelligent-routing/SKILL.md +335 -0
  224. package/template/.devbooster/hub/skills/lint-and-validate/SKILL.md +45 -0
  225. package/template/.devbooster/hub/skills/lint-and-validate/scripts/lint_runner.py +184 -0
  226. package/template/.devbooster/hub/skills/lint-and-validate/scripts/type_coverage.py +173 -0
  227. package/template/.devbooster/hub/skills/mcp-builder/SKILL.md +176 -0
  228. package/template/.devbooster/hub/skills/mobile-design/SKILL.md +394 -0
  229. package/template/.devbooster/hub/skills/mobile-design/decision-trees.md +516 -0
  230. package/template/.devbooster/hub/skills/mobile-design/mobile-backend.md +491 -0
  231. package/template/.devbooster/hub/skills/mobile-design/mobile-color-system.md +420 -0
  232. package/template/.devbooster/hub/skills/mobile-design/mobile-debugging.md +122 -0
  233. package/template/.devbooster/hub/skills/mobile-design/mobile-design-thinking.md +357 -0
  234. package/template/.devbooster/hub/skills/mobile-design/mobile-navigation.md +458 -0
  235. package/template/.devbooster/hub/skills/mobile-design/mobile-performance.md +767 -0
  236. package/template/.devbooster/hub/skills/mobile-design/mobile-testing.md +356 -0
  237. package/template/.devbooster/hub/skills/mobile-design/mobile-typography.md +433 -0
  238. package/template/.devbooster/hub/skills/mobile-design/platform-android.md +666 -0
  239. package/template/.devbooster/hub/skills/mobile-design/platform-ios.md +561 -0
  240. package/template/.devbooster/hub/skills/mobile-design/scripts/mobile_audit.py +670 -0
  241. package/template/.devbooster/hub/skills/mobile-design/touch-psychology.md +537 -0
  242. package/template/.devbooster/hub/skills/nextjs-react-expert/1-async-eliminating-waterfalls.md +312 -0
  243. package/template/.devbooster/hub/skills/nextjs-react-expert/2-bundle-bundle-size-optimization.md +240 -0
  244. package/template/.devbooster/hub/skills/nextjs-react-expert/3-server-server-side-performance.md +490 -0
  245. package/template/.devbooster/hub/skills/nextjs-react-expert/4-client-client-side-data-fetching.md +264 -0
  246. package/template/.devbooster/hub/skills/nextjs-react-expert/5-rerender-re-render-optimization.md +581 -0
  247. package/template/.devbooster/hub/skills/nextjs-react-expert/6-rendering-rendering-performance.md +432 -0
  248. package/template/.devbooster/hub/skills/nextjs-react-expert/7-js-javascript-performance.md +684 -0
  249. package/template/.devbooster/hub/skills/nextjs-react-expert/8-advanced-advanced-patterns.md +150 -0
  250. package/template/.devbooster/hub/skills/nextjs-react-expert/SKILL.md +286 -0
  251. package/template/.devbooster/hub/skills/nextjs-react-expert/scripts/convert_rules.py +222 -0
  252. package/template/.devbooster/hub/skills/nextjs-react-expert/scripts/react_performance_checker.py +252 -0
  253. package/template/.devbooster/hub/skills/nodejs-best-practices/SKILL.md +333 -0
  254. package/template/.devbooster/hub/skills/parallel-agents/SKILL.md +175 -0
  255. package/template/.devbooster/hub/skills/performance-profiling/SKILL.md +143 -0
  256. package/template/.devbooster/hub/skills/performance-profiling/scripts/lighthouse_audit.py +76 -0
  257. package/template/.devbooster/hub/skills/plan-writing/SKILL.md +152 -0
  258. package/template/.devbooster/hub/skills/powershell-windows/SKILL.md +167 -0
  259. package/template/.devbooster/hub/skills/python-patterns/SKILL.md +441 -0
  260. package/template/.devbooster/hub/skills/red-team-tactics/SKILL.md +199 -0
  261. package/template/.devbooster/hub/skills/rust-pro/SKILL.md +176 -0
  262. package/template/.devbooster/hub/skills/seo-fundamentals/SKILL.md +129 -0
  263. package/template/.devbooster/hub/skills/seo-fundamentals/scripts/seo_checker.py +219 -0
  264. package/template/.devbooster/hub/skills/server-management/SKILL.md +161 -0
  265. package/template/.devbooster/hub/skills/systematic-debugging/SKILL.md +109 -0
  266. package/template/.devbooster/hub/skills/tailwind-patterns/SKILL.md +269 -0
  267. package/template/.devbooster/hub/skills/tdd-workflow/SKILL.md +149 -0
  268. package/template/.devbooster/hub/skills/testing-patterns/SKILL.md +178 -0
  269. package/template/.devbooster/hub/skills/testing-patterns/scripts/test_runner.py +219 -0
  270. package/template/.devbooster/hub/skills/vulnerability-scanner/SKILL.md +276 -0
  271. package/template/.devbooster/hub/skills/vulnerability-scanner/checklists.md +121 -0
  272. package/template/.devbooster/hub/skills/vulnerability-scanner/scripts/security_scan.py +458 -0
  273. package/template/.devbooster/hub/skills/web-design-guidelines/SKILL.md +57 -0
  274. package/template/.devbooster/hub/skills/webapp-testing/SKILL.md +187 -0
  275. package/template/.devbooster/hub/skills/webapp-testing/scripts/playwright_runner.py +173 -0
  276. package/template/.devbooster/hub/workflows/brainstorm.md +113 -0
  277. package/template/.devbooster/hub/workflows/create.md +59 -0
  278. package/template/.devbooster/hub/workflows/debug.md +103 -0
  279. package/template/.devbooster/hub/workflows/deploy.md +176 -0
  280. package/template/.devbooster/hub/workflows/enhance.md +63 -0
  281. package/template/.devbooster/hub/workflows/orchestrate.md +237 -0
  282. package/template/.devbooster/hub/workflows/plan.md +89 -0
  283. package/template/.devbooster/hub/workflows/preview.md +81 -0
  284. package/template/.devbooster/hub/workflows/status.md +86 -0
  285. package/template/.devbooster/hub/workflows/test.md +144 -0
  286. package/template/.devbooster/hub/workflows/ui-ux-pro-max.md +296 -0
  287. package/template/.devbooster/rules/BACKEND.md +72 -0
  288. package/template/.devbooster/rules/COMERCIAL.md +69 -0
  289. package/template/.devbooster/rules/FRONTEND.md +76 -0
  290. package/template/.devbooster/rules/GUIDE.md +111 -0
  291. package/template/.devbooster/rules/PROJECT.md +83 -0
  292. package/template/.devbooster/rules/PROTOCOL.md +42 -0
  293. package/template/.devbooster/rules/USER_PREFERENCES.md +33 -0
  294. package/template/DEVBOOSTER_INIT.md +42 -0
@@ -0,0 +1,3487 @@
1
+ ---
2
+ name: react-best-practices
3
+ description: React and Next.js performance optimization from Vercel Engineering. Use when building React components, optimizing performance, eliminating waterfalls, reducing bundle size, reviewing code for performance issues, or implementing server/client-side optimizations.
4
+ allowed-tools: Read, Write, Edit, Glob, Grep, Bash
5
+ ---
6
+
7
+ # Next.js & React Performance Expert
8
+
9
+ > **From Vercel Engineering** - 57 optimization rules prioritized by impact
10
+ > **Philosophy:** Eliminate waterfalls first, optimize bundles second, then micro-optimize.
11
+
12
+ ---
13
+
14
+ ## 🎯 Selective Reading Rule (MANDATORY)
15
+
16
+ **Read ONLY sections relevant to your task!** Check the content map below and load what you need.
17
+
18
+ > 🔴 **For performance reviews: Start with CRITICAL sections (1-2), then move to HIGH/MEDIUM.**
19
+
20
+ ---
21
+
22
+ ## 📑 Content Map
23
+
24
+ | File | Impact | Rules | When to Read |
25
+ | --------------------------------------- | ------------------ | -------- | --------------------------------------------------------------- |
26
+ | `1-async-eliminating-waterfalls.md` | 🔴 **CRITICAL** | 5 rules | Slow page loads, sequential API calls, data fetching waterfalls |
27
+ | `2-bundle-bundle-size-optimization.md` | 🔴 **CRITICAL** | 5 rules | Large bundle size, slow Time to Interactive, First Load issues |
28
+ | `3-server-server-side-performance.md` | 🟠 **HIGH** | 7 rules | Slow SSR, API route optimization, server-side waterfalls |
29
+ | `4-client-client-side-data-fetching.md` | 🟡 **MEDIUM-HIGH** | 4 rules | Client data management, SWR patterns, deduplication |
30
+ | `5-rerender-re-render-optimization.md` | 🟡 **MEDIUM** | 12 rules | Excessive re-renders, React performance, memoization |
31
+ | `6-rendering-rendering-performance.md` | 🟡 **MEDIUM** | 9 rules | Rendering bottlenecks, virtualization, image optimization |
32
+ | `7-js-javascript-performance.md` | ⚪ **LOW-MEDIUM** | 12 rules | Micro-optimizations, caching, loop performance |
33
+ | `8-advanced-advanced-patterns.md` | 🔵 **VARIABLE** | 3 rules | Advanced React patterns, useLatest, init-once |
34
+
35
+ **Total: 57 rules across 8 categories**
36
+
37
+ ---
38
+
39
+ ## 🚀 Quick Decision Tree
40
+
41
+ **What's your performance issue?**
42
+
43
+ ```
44
+ 🐌 Slow page loads / Long Time to Interactive
45
+ → Read Section 1: Eliminating Waterfalls
46
+ → Read Section 2: Bundle Size Optimization
47
+
48
+ 📦 Large bundle size (> 200KB)
49
+ → Read Section 2: Bundle Size Optimization
50
+ → Check: Dynamic imports, barrel imports, tree-shaking
51
+
52
+ 🖥️ Slow Server-Side Rendering
53
+ → Read Section 3: Server-Side Performance
54
+ → Check: Parallel data fetching, streaming
55
+
56
+ 🔄 Too many re-renders / UI lag
57
+ → Read Section 5: Re-render Optimization
58
+ → Check: React.memo, useMemo, useCallback
59
+
60
+ 🎨 Rendering performance issues
61
+ → Read Section 6: Rendering Performance
62
+ → Check: Virtualization, layout thrashing
63
+
64
+ 🌐 Client-side data fetching problems
65
+ → Read Section 4: Client-Side Data Fetching
66
+ → Check: SWR deduplication, localStorage
67
+
68
+ ✨ Need advanced patterns
69
+ → Read Section 8: Advanced Patterns
70
+ ```
71
+
72
+ ---
73
+
74
+ ## 📊 Impact Priority Guide
75
+
76
+ **Use this order when doing comprehensive optimization:**
77
+
78
+ ```
79
+ 1️⃣ CRITICAL (Biggest Gains - Do First):
80
+ ├─ Section 1: Eliminating Waterfalls
81
+ │ └─ Each waterfall adds full network latency (100-500ms+)
82
+ └─ Section 2: Bundle Size Optimization
83
+ └─ Affects Time to Interactive and Largest Contentful Paint
84
+
85
+ 2️⃣ HIGH (Significant Impact - Do Second):
86
+ └─ Section 3: Server-Side Performance
87
+ └─ Eliminates server-side waterfalls, faster response times
88
+
89
+ 3️⃣ MEDIUM (Moderate Gains - Do Third):
90
+ ├─ Section 4: Client-Side Data Fetching
91
+ ├─ Section 5: Re-render Optimization
92
+ └─ Section 6: Rendering Performance
93
+
94
+ 4️⃣ LOW (Polish - Do Last):
95
+ ├─ Section 7: JavaScript Performance
96
+ └─ Section 8: Advanced Patterns
97
+ ```
98
+
99
+ ---
100
+
101
+ ## 🔗 Related Skills
102
+
103
+ | Need | Skill |
104
+ | ----------------------- | --------------------------------- |
105
+ | API design patterns | `@[skills/api-patterns]` |
106
+ | Database optimization | `@[skills/database-design]` |
107
+ | Testing strategies | `@[skills/testing-patterns]` |
108
+ | UI/UX design principles | `@[skills/frontend-design]` |
109
+ | TypeScript patterns | `@[skills/typescript-expert]` |
110
+ | Deployment & DevOps | `@[skills/deployment-procedures]` |
111
+
112
+ ---
113
+
114
+ ## ✅ Performance Review Checklist
115
+
116
+ Before shipping to production:
117
+
118
+ **Critical (Must Fix):**
119
+
120
+ - [ ] No sequential data fetching (waterfalls eliminated)
121
+ - [ ] Bundle size < 200KB for main bundle
122
+ - [ ] No barrel imports in app code
123
+ - [ ] Dynamic imports used for large components
124
+ - [ ] Parallel data fetching where possible
125
+
126
+ **High Priority:**
127
+
128
+ - [ ] Server components used where appropriate
129
+ - [ ] API routes optimized (no N+1 queries)
130
+ - [ ] Suspense boundaries for data fetching
131
+ - [ ] Static generation used where possible
132
+
133
+ **Medium Priority:**
134
+
135
+ - [ ] Expensive computations memoized
136
+ - [ ] List rendering virtualized (if > 100 items)
137
+ - [ ] Images optimized with next/image
138
+ - [ ] No unnecessary re-renders
139
+
140
+ **Low Priority (Polish):**
141
+
142
+ - [ ] Hot path loops optimized
143
+ - [ ] RegExp patterns hoisted
144
+ - [ ] Property access cached in loops
145
+
146
+ ---
147
+
148
+ ## ❌ Anti-Patterns (Common Mistakes)
149
+
150
+ **DON'T:**
151
+
152
+ - ❌ Use sequential `await` for independent operations
153
+ - ❌ Import entire libraries when you need one function
154
+ - ❌ Use barrel exports (`index.ts` re-exports) in app code
155
+ - ❌ Skip dynamic imports for large components/libraries
156
+ - ❌ Fetch data in useEffect without deduplication
157
+ - ❌ Forget to memoize expensive computations
158
+ - ❌ Use client components when server components work
159
+
160
+ **DO:**
161
+
162
+ - ✅ Fetch data in parallel with `Promise.all()`
163
+ - ✅ Use dynamic imports: `const Comp = dynamic(() => import('./Heavy'))`
164
+ - ✅ Import directly: `import { specific } from 'library/specific'`
165
+ - ✅ Use Suspense boundaries for better UX
166
+ - ✅ Leverage React Server Components
167
+ - ✅ Measure performance before optimizing
168
+ - ✅ Use Next.js built-in optimizations (next/image, next/font)
169
+
170
+ ---
171
+
172
+ ## 🎯 How to Use This Skill
173
+
174
+ ### For New Features:
175
+
176
+ 1. Check **Section 1 & 2** while building (prevent waterfalls, keep bundle small)
177
+ 2. Use server components by default (Section 3)
178
+ 3. Apply memoization for expensive operations (Section 5)
179
+
180
+ ### For Performance Reviews:
181
+
182
+ 1. Start with **Section 1** (waterfalls = biggest impact)
183
+ 2. Then **Section 2** (bundle size)
184
+ 3. Then **Section 3** (server-side)
185
+ 4. Finally other sections as needed
186
+
187
+ ### For Debugging Slow Performance:
188
+
189
+ 1. Identify the symptom (slow load, lag, etc.)
190
+ 2. Use Quick Decision Tree above
191
+ 3. Read relevant section
192
+ 4. Apply fixes in priority order
193
+
194
+ ---
195
+
196
+ ## 📚 Learning Path
197
+
198
+ **Beginner (Focus on Critical):**
199
+ → Section 1: Eliminating Waterfalls
200
+ → Section 2: Bundle Size Optimization
201
+
202
+ **Intermediate (Add High Priority):**
203
+ → Section 3: Server-Side Performance
204
+ → Section 5: Re-render Optimization
205
+
206
+ **Advanced (Full Optimization):**
207
+ → All sections + Section 8: Advanced Patterns
208
+
209
+ ---
210
+
211
+ ## 🔍 Validation Script
212
+
213
+ | Script | Purpose | Command |
214
+ | -------------------------------------- | --------------------------- | ------------------------------------------------------------ |
215
+ | `scripts/react_performance_checker.py` | Automated performance audit | `python scripts/react_performance_checker.py <project_path>` |
216
+
217
+ ---
218
+
219
+ ## 📖 Section Details
220
+
221
+ ### Section 1: Eliminating Waterfalls (CRITICAL)
222
+
223
+ **Impact:** Each waterfall adds 100-500ms+ latency
224
+ **Key Concepts:** Parallel fetching, Promise.all(), Suspense boundaries, preloading
225
+
226
+ ### Section 2: Bundle Size Optimization (CRITICAL)
227
+
228
+ **Impact:** Directly affects Time to Interactive, Largest Contentful Paint
229
+ **Key Concepts:** Dynamic imports, tree-shaking, barrel import avoidance
230
+
231
+ ### Section 3: Server-Side Performance (HIGH)
232
+
233
+ **Impact:** Faster server responses, better SEO
234
+ **Key Concepts:** Parallel server fetching, streaming, API route optimization
235
+
236
+ ### Section 4: Client-Side Data Fetching (MEDIUM-HIGH)
237
+
238
+ **Impact:** Reduces redundant requests, better UX
239
+ **Key Concepts:** SWR deduplication, localStorage caching, event listeners
240
+
241
+ ### Section 5: Re-render Optimization (MEDIUM)
242
+
243
+ **Impact:** Smoother UI, less wasted computation
244
+ **Key Concepts:** React.memo, useMemo, useCallback, component structure
245
+
246
+ ### Section 6: Rendering Performance (MEDIUM)
247
+
248
+ **Impact:** Better rendering efficiency
249
+ **Key Concepts:** Virtualization, image optimization, layout thrashing
250
+
251
+ ### Section 7: JavaScript Performance (LOW-MEDIUM)
252
+
253
+ **Impact:** Incremental improvements in hot paths
254
+ **Key Concepts:** Loop optimization, caching, RegExp hoisting
255
+
256
+ ### Section 8: Advanced Patterns (VARIABLE)
257
+
258
+ **Impact:** Specific use cases
259
+ **Key Concepts:** useLatest hook, init-once patterns, event handler refs
260
+
261
+ ---
262
+
263
+ ## 🎓 Best Practices Summary
264
+
265
+ **Golden Rules:**
266
+
267
+ 1. **Measure first** - Use React DevTools Profiler, Chrome DevTools
268
+ 2. **Biggest impact first** - Waterfalls → Bundle → Server → Micro
269
+ 3. **Don't over-optimize** - Focus on real bottlenecks
270
+ 4. **Use platform features** - Next.js has optimizations built-in
271
+ 5. **Think about users** - Real-world conditions matter
272
+
273
+ **Performance Mindset:**
274
+
275
+ - Every `await` in sequence = potential waterfall
276
+ - Every `import` = potential bundle bloat
277
+ - Every re-render = wasted computation (if unnecessary)
278
+ - Server components = less JavaScript to ship
279
+ - Measure, don't guess
280
+
281
+ ---
282
+
283
+ **Source:** Vercel Engineering
284
+ **Date:** January 2026
285
+ **Version:** 1.0.0
286
+ **Total Rules:** 57 across 8 categories
287
+
288
+
289
+
290
+ ---
291
+ # Content from 1-async-eliminating-waterfalls.md
292
+
293
+ # 1. Eliminating Waterfalls
294
+
295
+ > **Impact:** CRITICAL
296
+ > **Focus:** Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
297
+
298
+ ---
299
+
300
+ ## Overview
301
+
302
+ This section contains **5 rules** focused on eliminating waterfalls.
303
+
304
+ ---
305
+
306
+ ## Rule 1.1: Defer Await Until Needed
307
+
308
+ **Impact:** HIGH
309
+ **Tags:** async, await, conditional, optimization
310
+
311
+ ## Defer Await Until Needed
312
+
313
+ Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
314
+
315
+ **Incorrect (blocks both branches):**
316
+
317
+ ```typescript
318
+ async function handleRequest(userId: string, skipProcessing: boolean) {
319
+ const userData = await fetchUserData(userId)
320
+
321
+ if (skipProcessing) {
322
+ // Returns immediately but still waited for userData
323
+ return { skipped: true }
324
+ }
325
+
326
+ // Only this branch uses userData
327
+ return processUserData(userData)
328
+ }
329
+ ```
330
+
331
+ **Correct (only blocks when needed):**
332
+
333
+ ```typescript
334
+ async function handleRequest(userId: string, skipProcessing: boolean) {
335
+ if (skipProcessing) {
336
+ // Returns immediately without waiting
337
+ return { skipped: true }
338
+ }
339
+
340
+ // Fetch only when needed
341
+ const userData = await fetchUserData(userId)
342
+ return processUserData(userData)
343
+ }
344
+ ```
345
+
346
+ **Another example (early return optimization):**
347
+
348
+ ```typescript
349
+ // Incorrect: always fetches permissions
350
+ async function updateResource(resourceId: string, userId: string) {
351
+ const permissions = await fetchPermissions(userId)
352
+ const resource = await getResource(resourceId)
353
+
354
+ if (!resource) {
355
+ return { error: 'Not found' }
356
+ }
357
+
358
+ if (!permissions.canEdit) {
359
+ return { error: 'Forbidden' }
360
+ }
361
+
362
+ return await updateResourceData(resource, permissions)
363
+ }
364
+
365
+ // Correct: fetches only when needed
366
+ async function updateResource(resourceId: string, userId: string) {
367
+ const resource = await getResource(resourceId)
368
+
369
+ if (!resource) {
370
+ return { error: 'Not found' }
371
+ }
372
+
373
+ const permissions = await fetchPermissions(userId)
374
+
375
+ if (!permissions.canEdit) {
376
+ return { error: 'Forbidden' }
377
+ }
378
+
379
+ return await updateResourceData(resource, permissions)
380
+ }
381
+ ```
382
+
383
+ This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
384
+
385
+ ---
386
+
387
+ ## Rule 1.2: Dependency-Based Parallelization
388
+
389
+ **Impact:** CRITICAL
390
+ **Tags:** async, parallelization, dependencies, better-all
391
+
392
+ ## Dependency-Based Parallelization
393
+
394
+ For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
395
+
396
+ **Incorrect (profile waits for config unnecessarily):**
397
+
398
+ ```typescript
399
+ const [user, config] = await Promise.all([
400
+ fetchUser(),
401
+ fetchConfig()
402
+ ])
403
+ const profile = await fetchProfile(user.id)
404
+ ```
405
+
406
+ **Correct (config and profile run in parallel):**
407
+
408
+ ```typescript
409
+ import { all } from 'better-all'
410
+
411
+ const { user, config, profile } = await all({
412
+ async user() { return fetchUser() },
413
+ async config() { return fetchConfig() },
414
+ async profile() {
415
+ return fetchProfile((await this.$.user).id)
416
+ }
417
+ })
418
+ ```
419
+
420
+ **Alternative without extra dependencies:**
421
+
422
+ We can also create all the promises first, and do `Promise.all()` at the end.
423
+
424
+ ```typescript
425
+ const userPromise = fetchUser()
426
+ const profilePromise = userPromise.then(user => fetchProfile(user.id))
427
+
428
+ const [user, config, profile] = await Promise.all([
429
+ userPromise,
430
+ fetchConfig(),
431
+ profilePromise
432
+ ])
433
+ ```
434
+
435
+ Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
436
+
437
+ ---
438
+
439
+ ## Rule 1.3: Prevent Waterfall Chains in API Routes
440
+
441
+ **Impact:** CRITICAL
442
+ **Tags:** api-routes, server-actions, waterfalls, parallelization
443
+
444
+ ## Prevent Waterfall Chains in API Routes
445
+
446
+ In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
447
+
448
+ **Incorrect (config waits for auth, data waits for both):**
449
+
450
+ ```typescript
451
+ export async function GET(request: Request) {
452
+ const session = await auth()
453
+ const config = await fetchConfig()
454
+ const data = await fetchData(session.user.id)
455
+ return Response.json({ data, config })
456
+ }
457
+ ```
458
+
459
+ **Correct (auth and config start immediately):**
460
+
461
+ ```typescript
462
+ export async function GET(request: Request) {
463
+ const sessionPromise = auth()
464
+ const configPromise = fetchConfig()
465
+ const session = await sessionPromise
466
+ const [config, data] = await Promise.all([
467
+ configPromise,
468
+ fetchData(session.user.id)
469
+ ])
470
+ return Response.json({ data, config })
471
+ }
472
+ ```
473
+
474
+ For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
475
+
476
+ ---
477
+
478
+ ## Rule 1.4: Promise.all() for Independent Operations
479
+
480
+ **Impact:** CRITICAL
481
+ **Tags:** async, parallelization, promises, waterfalls
482
+
483
+ ## Promise.all() for Independent Operations
484
+
485
+ When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
486
+
487
+ **Incorrect (sequential execution, 3 round trips):**
488
+
489
+ ```typescript
490
+ const user = await fetchUser()
491
+ const posts = await fetchPosts()
492
+ const comments = await fetchComments()
493
+ ```
494
+
495
+ **Correct (parallel execution, 1 round trip):**
496
+
497
+ ```typescript
498
+ const [user, posts, comments] = await Promise.all([
499
+ fetchUser(),
500
+ fetchPosts(),
501
+ fetchComments()
502
+ ])
503
+ ```
504
+
505
+ ---
506
+
507
+ ## Rule 1.5: Strategic Suspense Boundaries
508
+
509
+ **Impact:** HIGH
510
+ **Tags:** async, suspense, streaming, layout-shift
511
+
512
+ ## Strategic Suspense Boundaries
513
+
514
+ Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
515
+
516
+ **Incorrect (wrapper blocked by data fetching):**
517
+
518
+ ```tsx
519
+ async function Page() {
520
+ const data = await fetchData() // Blocks entire page
521
+
522
+ return (
523
+ <div>
524
+ <div>Sidebar</div>
525
+ <div>Header</div>
526
+ <div>
527
+ <DataDisplay data={data} />
528
+ </div>
529
+ <div>Footer</div>
530
+ </div>
531
+ )
532
+ }
533
+ ```
534
+
535
+ The entire layout waits for data even though only the middle section needs it.
536
+
537
+ **Correct (wrapper shows immediately, data streams in):**
538
+
539
+ ```tsx
540
+ function Page() {
541
+ return (
542
+ <div>
543
+ <div>Sidebar</div>
544
+ <div>Header</div>
545
+ <div>
546
+ <Suspense fallback={<Skeleton />}>
547
+ <DataDisplay />
548
+ </Suspense>
549
+ </div>
550
+ <div>Footer</div>
551
+ </div>
552
+ )
553
+ }
554
+
555
+ async function DataDisplay() {
556
+ const data = await fetchData() // Only blocks this component
557
+ return <div>{data.content}</div>
558
+ }
559
+ ```
560
+
561
+ Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
562
+
563
+ **Alternative (share promise across components):**
564
+
565
+ ```tsx
566
+ function Page() {
567
+ // Start fetch immediately, but don't await
568
+ const dataPromise = fetchData()
569
+
570
+ return (
571
+ <div>
572
+ <div>Sidebar</div>
573
+ <div>Header</div>
574
+ <Suspense fallback={<Skeleton />}>
575
+ <DataDisplay dataPromise={dataPromise} />
576
+ <DataSummary dataPromise={dataPromise} />
577
+ </Suspense>
578
+ <div>Footer</div>
579
+ </div>
580
+ )
581
+ }
582
+
583
+ function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
584
+ const data = use(dataPromise) // Unwraps the promise
585
+ return <div>{data.content}</div>
586
+ }
587
+
588
+ function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
589
+ const data = use(dataPromise) // Reuses the same promise
590
+ return <div>{data.summary}</div>
591
+ }
592
+ ```
593
+
594
+ Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
595
+
596
+ **When NOT to use this pattern:**
597
+
598
+ - Critical data needed for layout decisions (affects positioning)
599
+ - SEO-critical content above the fold
600
+ - Small, fast queries where suspense overhead isn't worth it
601
+ - When you want to avoid layout shift (loading → content jump)
602
+
603
+ **Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
604
+
605
+
606
+
607
+
608
+ ---
609
+ # Content from 2-bundle-bundle-size-optimization.md
610
+
611
+ # 2. Bundle Size Optimization
612
+
613
+ > **Impact:** CRITICAL
614
+ > **Focus:** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
615
+
616
+ ---
617
+
618
+ ## Overview
619
+
620
+ This section contains **5 rules** focused on bundle size optimization.
621
+
622
+ ---
623
+
624
+ ## Rule 2.1: Avoid Barrel File Imports
625
+
626
+ **Impact:** CRITICAL
627
+ **Tags:** bundle, imports, tree-shaking, barrel-files, performance
628
+
629
+ ## Avoid Barrel File Imports
630
+
631
+ 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'`).
632
+
633
+ 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.
634
+
635
+ **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.
636
+
637
+ **Incorrect (imports entire library):**
638
+
639
+ ```tsx
640
+ import { Check, X, Menu } from 'lucide-react'
641
+ // Loads 1,583 modules, takes ~2.8s extra in dev
642
+ // Runtime cost: 200-800ms on every cold start
643
+
644
+ import { Button, TextField } from '@mui/material'
645
+ // Loads 2,225 modules, takes ~4.2s extra in dev
646
+ ```
647
+
648
+ **Correct (imports only what you need):**
649
+
650
+ ```tsx
651
+ import Check from 'lucide-react/dist/esm/icons/check'
652
+ import X from 'lucide-react/dist/esm/icons/x'
653
+ import Menu from 'lucide-react/dist/esm/icons/menu'
654
+ // Loads only 3 modules (~2KB vs ~1MB)
655
+
656
+ import Button from '@mui/material/Button'
657
+ import TextField from '@mui/material/TextField'
658
+ // Loads only what you use
659
+ ```
660
+
661
+ **Alternative (Next.js 13.5+):**
662
+
663
+ ```js
664
+ // next.config.js - use optimizePackageImports
665
+ module.exports = {
666
+ experimental: {
667
+ optimizePackageImports: ['lucide-react', '@mui/material']
668
+ }
669
+ }
670
+
671
+ // Then you can keep the ergonomic barrel imports:
672
+ import { Check, X, Menu } from 'lucide-react'
673
+ // Automatically transformed to direct imports at build time
674
+ ```
675
+
676
+ Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
677
+
678
+ 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`.
679
+
680
+ Reference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
681
+
682
+ ---
683
+
684
+ ## Rule 2.2: Conditional Module Loading
685
+
686
+ **Impact:** HIGH
687
+ **Tags:** bundle, conditional-loading, lazy-loading
688
+
689
+ ## Conditional Module Loading
690
+
691
+ Load large data or modules only when a feature is activated.
692
+
693
+ **Example (lazy-load animation frames):**
694
+
695
+ ```tsx
696
+ function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {
697
+ const [frames, setFrames] = useState<Frame[] | null>(null)
698
+
699
+ useEffect(() => {
700
+ if (enabled && !frames && typeof window !== 'undefined') {
701
+ import('./animation-frames.js')
702
+ .then(mod => setFrames(mod.frames))
703
+ .catch(() => setEnabled(false))
704
+ }
705
+ }, [enabled, frames, setEnabled])
706
+
707
+ if (!frames) return <Skeleton />
708
+ return <Canvas frames={frames} />
709
+ }
710
+ ```
711
+
712
+ The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
713
+
714
+ ---
715
+
716
+ ## Rule 2.3: Defer Non-Critical Third-Party Libraries
717
+
718
+ **Impact:** MEDIUM
719
+ **Tags:** bundle, third-party, analytics, defer
720
+
721
+ ## Defer Non-Critical Third-Party Libraries
722
+
723
+ Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
724
+
725
+ **Incorrect (blocks initial bundle):**
726
+
727
+ ```tsx
728
+ import { Analytics } from '@vercel/analytics/react'
729
+
730
+ export default function RootLayout({ children }) {
731
+ return (
732
+ <html>
733
+ <body>
734
+ {children}
735
+ <Analytics />
736
+ </body>
737
+ </html>
738
+ )
739
+ }
740
+ ```
741
+
742
+ **Correct (loads after hydration):**
743
+
744
+ ```tsx
745
+ import dynamic from 'next/dynamic'
746
+
747
+ const Analytics = dynamic(
748
+ () => import('@vercel/analytics/react').then(m => m.Analytics),
749
+ { ssr: false }
750
+ )
751
+
752
+ export default function RootLayout({ children }) {
753
+ return (
754
+ <html>
755
+ <body>
756
+ {children}
757
+ <Analytics />
758
+ </body>
759
+ </html>
760
+ )
761
+ }
762
+ ```
763
+
764
+ ---
765
+
766
+ ## Rule 2.4: Dynamic Imports for Heavy Components
767
+
768
+ **Impact:** CRITICAL
769
+ **Tags:** bundle, dynamic-import, code-splitting, next-dynamic
770
+
771
+ ## Dynamic Imports for Heavy Components
772
+
773
+ Use `next/dynamic` to lazy-load large components not needed on initial render.
774
+
775
+ **Incorrect (Monaco bundles with main chunk ~300KB):**
776
+
777
+ ```tsx
778
+ import { MonacoEditor } from './monaco-editor'
779
+
780
+ function CodePanel({ code }: { code: string }) {
781
+ return <MonacoEditor value={code} />
782
+ }
783
+ ```
784
+
785
+ **Correct (Monaco loads on demand):**
786
+
787
+ ```tsx
788
+ import dynamic from 'next/dynamic'
789
+
790
+ const MonacoEditor = dynamic(
791
+ () => import('./monaco-editor').then(m => m.MonacoEditor),
792
+ { ssr: false }
793
+ )
794
+
795
+ function CodePanel({ code }: { code: string }) {
796
+ return <MonacoEditor value={code} />
797
+ }
798
+ ```
799
+
800
+ ---
801
+
802
+ ## Rule 2.5: Preload Based on User Intent
803
+
804
+ **Impact:** MEDIUM
805
+ **Tags:** bundle, preload, user-intent, hover
806
+
807
+ ## Preload Based on User Intent
808
+
809
+ Preload heavy bundles before they're needed to reduce perceived latency.
810
+
811
+ **Example (preload on hover/focus):**
812
+
813
+ ```tsx
814
+ function EditorButton({ onClick }: { onClick: () => void }) {
815
+ const preload = () => {
816
+ if (typeof window !== 'undefined') {
817
+ void import('./monaco-editor')
818
+ }
819
+ }
820
+
821
+ return (
822
+ <button
823
+ onMouseEnter={preload}
824
+ onFocus={preload}
825
+ onClick={onClick}
826
+ >
827
+ Open Editor
828
+ </button>
829
+ )
830
+ }
831
+ ```
832
+
833
+ **Example (preload when feature flag is enabled):**
834
+
835
+ ```tsx
836
+ function FlagsProvider({ children, flags }: Props) {
837
+ useEffect(() => {
838
+ if (flags.editorEnabled && typeof window !== 'undefined') {
839
+ void import('./monaco-editor').then(mod => mod.init())
840
+ }
841
+ }, [flags.editorEnabled])
842
+
843
+ return <FlagsContext.Provider value={flags}>
844
+ {children}
845
+ </FlagsContext.Provider>
846
+ }
847
+ ```
848
+
849
+ The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
850
+
851
+
852
+
853
+
854
+ ---
855
+ # Content from 3-server-server-side-performance.md
856
+
857
+ # 3. Server-Side Performance
858
+
859
+ > **Impact:** HIGH
860
+ > **Focus:** Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
861
+
862
+ ---
863
+
864
+ ## Overview
865
+
866
+ This section contains **7 rules** focused on server-side performance.
867
+
868
+ ---
869
+
870
+ ## Rule 3.1: Authenticate Server Actions Like API Routes
871
+
872
+ **Impact:** CRITICAL
873
+ **Tags:** server, server-actions, authentication, security, authorization
874
+
875
+ ## Authenticate Server Actions Like API Routes
876
+
877
+ **Impact: CRITICAL (prevents unauthorized access to server mutations)**
878
+
879
+ 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.
880
+
881
+ 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."
882
+
883
+ **Incorrect (no authentication check):**
884
+
885
+ ```typescript
886
+ 'use server'
887
+
888
+ export async function deleteUser(userId: string) {
889
+ // Anyone can call this! No auth check
890
+ await db.user.delete({ where: { id: userId } })
891
+ return { success: true }
892
+ }
893
+ ```
894
+
895
+ **Correct (authentication inside the action):**
896
+
897
+ ```typescript
898
+ 'use server'
899
+
900
+ import { verifySession } from '@/lib/auth'
901
+ import { unauthorized } from '@/lib/errors'
902
+
903
+ export async function deleteUser(userId: string) {
904
+ // Always check auth inside the action
905
+ const session = await verifySession()
906
+
907
+ if (!session) {
908
+ throw unauthorized('Must be logged in')
909
+ }
910
+
911
+ // Check authorization too
912
+ if (session.user.role !== 'admin' && session.user.id !== userId) {
913
+ throw unauthorized('Cannot delete other users')
914
+ }
915
+
916
+ await db.user.delete({ where: { id: userId } })
917
+ return { success: true }
918
+ }
919
+ ```
920
+
921
+ **With input validation:**
922
+
923
+ ```typescript
924
+ 'use server'
925
+
926
+ import { verifySession } from '@/lib/auth'
927
+ import { z } from 'zod'
928
+
929
+ const updateProfileSchema = z.object({
930
+ userId: z.string().uuid(),
931
+ name: z.string().min(1).max(100),
932
+ email: z.string().email()
933
+ })
934
+
935
+ export async function updateProfile(data: unknown) {
936
+ // Validate input first
937
+ const validated = updateProfileSchema.parse(data)
938
+
939
+ // Then authenticate
940
+ const session = await verifySession()
941
+ if (!session) {
942
+ throw new Error('Unauthorized')
943
+ }
944
+
945
+ // Then authorize
946
+ if (session.user.id !== validated.userId) {
947
+ throw new Error('Can only update own profile')
948
+ }
949
+
950
+ // Finally perform the mutation
951
+ await db.user.update({
952
+ where: { id: validated.userId },
953
+ data: {
954
+ name: validated.name,
955
+ email: validated.email
956
+ }
957
+ })
958
+
959
+ return { success: true }
960
+ }
961
+ ```
962
+
963
+ Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
964
+
965
+ ---
966
+
967
+ ## Rule 3.2: Avoid Duplicate Serialization in RSC Props
968
+
969
+ **Impact:** LOW
970
+ **Tags:** server, rsc, serialization, props, client-components
971
+
972
+ ## Avoid Duplicate Serialization in RSC Props
973
+
974
+ **Impact: LOW (reduces network payload by avoiding duplicate serialization)**
975
+
976
+ 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.
977
+
978
+ **Incorrect (duplicates array):**
979
+
980
+ ```tsx
981
+ // RSC: sends 6 strings (2 arrays × 3 items)
982
+ <ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
983
+ ```
984
+
985
+ **Correct (sends 3 strings):**
986
+
987
+ ```tsx
988
+ // RSC: send once
989
+ <ClientList usernames={usernames} />
990
+
991
+ // Client: transform there
992
+ 'use client'
993
+ const sorted = useMemo(() => [...usernames].sort(), [usernames])
994
+ ```
995
+
996
+ **Nested deduplication behavior:**
997
+
998
+ Deduplication works recursively. Impact varies by data type:
999
+
1000
+ - `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
1001
+ - `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
1002
+
1003
+ ```tsx
1004
+ // string[] - duplicates everything
1005
+ usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
1006
+
1007
+ // object[] - duplicates array structure only
1008
+ users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
1009
+ ```
1010
+
1011
+ **Operations breaking deduplication (create new references):**
1012
+
1013
+ - Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
1014
+ - Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
1015
+
1016
+ **More examples:**
1017
+
1018
+ ```tsx
1019
+ // ❌ Bad
1020
+ <C users={users} active={users.filter(u => u.active)} />
1021
+ <C product={product} productName={product.name} />
1022
+
1023
+ // ✅ Good
1024
+ <C users={users} />
1025
+ <C product={product} />
1026
+ // Do filtering/destructuring in client
1027
+ ```
1028
+
1029
+ **Exception:** Pass derived data when transformation is expensive or client doesn't need original.
1030
+
1031
+ ---
1032
+
1033
+ ## Rule 3.3: Cross-Request LRU Caching
1034
+
1035
+ **Impact:** HIGH
1036
+ **Tags:** server, cache, lru, cross-request
1037
+
1038
+ ## Cross-Request LRU Caching
1039
+
1040
+ `React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
1041
+
1042
+ **Implementation:**
1043
+
1044
+ ```typescript
1045
+ import { LRUCache } from 'lru-cache'
1046
+
1047
+ const cache = new LRUCache<string, any>({
1048
+ max: 1000,
1049
+ ttl: 5 * 60 * 1000 // 5 minutes
1050
+ })
1051
+
1052
+ export async function getUser(id: string) {
1053
+ const cached = cache.get(id)
1054
+ if (cached) return cached
1055
+
1056
+ const user = await db.user.findUnique({ where: { id } })
1057
+ cache.set(id, user)
1058
+ return user
1059
+ }
1060
+
1061
+ // Request 1: DB query, result cached
1062
+ // Request 2: cache hit, no DB query
1063
+ ```
1064
+
1065
+ Use when sequential user actions hit multiple endpoints needing the same data within seconds.
1066
+
1067
+ **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.
1068
+
1069
+ **In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
1070
+
1071
+ Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
1072
+
1073
+ ---
1074
+
1075
+ ## Rule 3.4: Minimize Serialization at RSC Boundaries
1076
+
1077
+ **Impact:** HIGH
1078
+ **Tags:** server, rsc, serialization, props
1079
+
1080
+ ## Minimize Serialization at RSC Boundaries
1081
+
1082
+ 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.
1083
+
1084
+ **Incorrect (serializes all 50 fields):**
1085
+
1086
+ ```tsx
1087
+ async function Page() {
1088
+ const user = await fetchUser() // 50 fields
1089
+ return <Profile user={user} />
1090
+ }
1091
+
1092
+ 'use client'
1093
+ function Profile({ user }: { user: User }) {
1094
+ return <div>{user.name}</div> // uses 1 field
1095
+ }
1096
+ ```
1097
+
1098
+ **Correct (serializes only 1 field):**
1099
+
1100
+ ```tsx
1101
+ async function Page() {
1102
+ const user = await fetchUser()
1103
+ return <Profile name={user.name} />
1104
+ }
1105
+
1106
+ 'use client'
1107
+ function Profile({ name }: { name: string }) {
1108
+ return <div>{name}</div>
1109
+ }
1110
+ ```
1111
+
1112
+ ---
1113
+
1114
+ ## Rule 3.5: Parallel Data Fetching with Component Composition
1115
+
1116
+ **Impact:** CRITICAL
1117
+ **Tags:** server, rsc, parallel-fetching, composition
1118
+
1119
+ ## Parallel Data Fetching with Component Composition
1120
+
1121
+ React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
1122
+
1123
+ **Incorrect (Sidebar waits for Page's fetch to complete):**
1124
+
1125
+ ```tsx
1126
+ export default async function Page() {
1127
+ const header = await fetchHeader()
1128
+ return (
1129
+ <div>
1130
+ <div>{header}</div>
1131
+ <Sidebar />
1132
+ </div>
1133
+ )
1134
+ }
1135
+
1136
+ async function Sidebar() {
1137
+ const items = await fetchSidebarItems()
1138
+ return <nav>{items.map(renderItem)}</nav>
1139
+ }
1140
+ ```
1141
+
1142
+ **Correct (both fetch simultaneously):**
1143
+
1144
+ ```tsx
1145
+ async function Header() {
1146
+ const data = await fetchHeader()
1147
+ return <div>{data}</div>
1148
+ }
1149
+
1150
+ async function Sidebar() {
1151
+ const items = await fetchSidebarItems()
1152
+ return <nav>{items.map(renderItem)}</nav>
1153
+ }
1154
+
1155
+ export default function Page() {
1156
+ return (
1157
+ <div>
1158
+ <Header />
1159
+ <Sidebar />
1160
+ </div>
1161
+ )
1162
+ }
1163
+ ```
1164
+
1165
+ **Alternative with children prop:**
1166
+
1167
+ ```tsx
1168
+ async function Header() {
1169
+ const data = await fetchHeader()
1170
+ return <div>{data}</div>
1171
+ }
1172
+
1173
+ async function Sidebar() {
1174
+ const items = await fetchSidebarItems()
1175
+ return <nav>{items.map(renderItem)}</nav>
1176
+ }
1177
+
1178
+ function Layout({ children }: { children: ReactNode }) {
1179
+ return (
1180
+ <div>
1181
+ <Header />
1182
+ {children}
1183
+ </div>
1184
+ )
1185
+ }
1186
+
1187
+ export default function Page() {
1188
+ return (
1189
+ <Layout>
1190
+ <Sidebar />
1191
+ </Layout>
1192
+ )
1193
+ }
1194
+ ```
1195
+
1196
+ ---
1197
+
1198
+ ## Rule 3.6: Per-Request Deduplication with React.cache()
1199
+
1200
+ **Impact:** MEDIUM
1201
+ **Tags:** server, cache, react-cache, deduplication
1202
+
1203
+ ## Per-Request Deduplication with React.cache()
1204
+
1205
+ Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
1206
+
1207
+ **Usage:**
1208
+
1209
+ ```typescript
1210
+ import { cache } from 'react'
1211
+
1212
+ export const getCurrentUser = cache(async () => {
1213
+ const session = await auth()
1214
+ if (!session?.user?.id) return null
1215
+ return await db.user.findUnique({
1216
+ where: { id: session.user.id }
1217
+ })
1218
+ })
1219
+ ```
1220
+
1221
+ Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
1222
+
1223
+ **Avoid inline objects as arguments:**
1224
+
1225
+ `React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.
1226
+
1227
+ **Incorrect (always cache miss):**
1228
+
1229
+ ```typescript
1230
+ const getUser = cache(async (params: { uid: number }) => {
1231
+ return await db.user.findUnique({ where: { id: params.uid } })
1232
+ })
1233
+
1234
+ // Each call creates new object, never hits cache
1235
+ getUser({ uid: 1 })
1236
+ getUser({ uid: 1 }) // Cache miss, runs query again
1237
+ ```
1238
+
1239
+ **Correct (cache hit):**
1240
+
1241
+ ```typescript
1242
+ const getUser = cache(async (uid: number) => {
1243
+ return await db.user.findUnique({ where: { id: uid } })
1244
+ })
1245
+
1246
+ // Primitive args use value equality
1247
+ getUser(1)
1248
+ getUser(1) // Cache hit, returns cached result
1249
+ ```
1250
+
1251
+ If you must pass objects, pass the same reference:
1252
+
1253
+ ```typescript
1254
+ const params = { uid: 1 }
1255
+ getUser(params) // Query runs
1256
+ getUser(params) // Cache hit (same reference)
1257
+ ```
1258
+
1259
+ **Next.js-Specific Note:**
1260
+
1261
+ 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:
1262
+
1263
+ - Database queries (Prisma, Drizzle, etc.)
1264
+ - Heavy computations
1265
+ - Authentication checks
1266
+ - File system operations
1267
+ - Any non-fetch async work
1268
+
1269
+ Use `React.cache()` to deduplicate these operations across your component tree.
1270
+
1271
+ Reference: [React.cache documentation](https://react.dev/reference/react/cache)
1272
+
1273
+ ---
1274
+
1275
+ ## Rule 3.7: Use after() for Non-Blocking Operations
1276
+
1277
+ **Impact:** MEDIUM
1278
+ **Tags:** server, async, logging, analytics, side-effects
1279
+
1280
+ ## Use after() for Non-Blocking Operations
1281
+
1282
+ 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.
1283
+
1284
+ **Incorrect (blocks response):**
1285
+
1286
+ ```tsx
1287
+ import { logUserAction } from '@/app/utils'
1288
+
1289
+ export async function POST(request: Request) {
1290
+ // Perform mutation
1291
+ await updateDatabase(request)
1292
+
1293
+ // Logging blocks the response
1294
+ const userAgent = request.headers.get('user-agent') || 'unknown'
1295
+ await logUserAction({ userAgent })
1296
+
1297
+ return new Response(JSON.stringify({ status: 'success' }), {
1298
+ status: 200,
1299
+ headers: { 'Content-Type': 'application/json' }
1300
+ })
1301
+ }
1302
+ ```
1303
+
1304
+ **Correct (non-blocking):**
1305
+
1306
+ ```tsx
1307
+ import { after } from 'next/server'
1308
+ import { headers, cookies } from 'next/headers'
1309
+ import { logUserAction } from '@/app/utils'
1310
+
1311
+ export async function POST(request: Request) {
1312
+ // Perform mutation
1313
+ await updateDatabase(request)
1314
+
1315
+ // Log after response is sent
1316
+ after(async () => {
1317
+ const userAgent = (await headers()).get('user-agent') || 'unknown'
1318
+ const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
1319
+
1320
+ logUserAction({ sessionCookie, userAgent })
1321
+ })
1322
+
1323
+ return new Response(JSON.stringify({ status: 'success' }), {
1324
+ status: 200,
1325
+ headers: { 'Content-Type': 'application/json' }
1326
+ })
1327
+ }
1328
+ ```
1329
+
1330
+ The response is sent immediately while logging happens in the background.
1331
+
1332
+ **Common use cases:**
1333
+
1334
+ - Analytics tracking
1335
+ - Audit logging
1336
+ - Sending notifications
1337
+ - Cache invalidation
1338
+ - Cleanup tasks
1339
+
1340
+ **Important notes:**
1341
+
1342
+ - `after()` runs even if the response fails or redirects
1343
+ - Works in Server Actions, Route Handlers, and Server Components
1344
+
1345
+ Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
1346
+
1347
+
1348
+
1349
+
1350
+ ---
1351
+ # Content from 4-client-client-side-data-fetching.md
1352
+
1353
+ # 4. Client-Side Data Fetching
1354
+
1355
+ > **Impact:** MEDIUM-HIGH
1356
+ > **Focus:** Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
1357
+
1358
+ ---
1359
+
1360
+ ## Overview
1361
+
1362
+ This section contains **4 rules** focused on client-side data fetching.
1363
+
1364
+ ---
1365
+
1366
+ ## Rule 4.1: Deduplicate Global Event Listeners
1367
+
1368
+ **Impact:** LOW
1369
+ **Tags:** client, swr, event-listeners, subscription
1370
+
1371
+ ## Deduplicate Global Event Listeners
1372
+
1373
+ Use `useSWRSubscription()` to share global event listeners across component instances.
1374
+
1375
+ **Incorrect (N instances = N listeners):**
1376
+
1377
+ ```tsx
1378
+ function useKeyboardShortcut(key: string, callback: () => void) {
1379
+ useEffect(() => {
1380
+ const handler = (e: KeyboardEvent) => {
1381
+ if (e.metaKey && e.key === key) {
1382
+ callback()
1383
+ }
1384
+ }
1385
+ window.addEventListener('keydown', handler)
1386
+ return () => window.removeEventListener('keydown', handler)
1387
+ }, [key, callback])
1388
+ }
1389
+ ```
1390
+
1391
+ When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
1392
+
1393
+ **Correct (N instances = 1 listener):**
1394
+
1395
+ ```tsx
1396
+ import useSWRSubscription from 'swr/subscription'
1397
+
1398
+ // Module-level Map to track callbacks per key
1399
+ const keyCallbacks = new Map<string, Set<() => void>>()
1400
+
1401
+ function useKeyboardShortcut(key: string, callback: () => void) {
1402
+ // Register this callback in the Map
1403
+ useEffect(() => {
1404
+ if (!keyCallbacks.has(key)) {
1405
+ keyCallbacks.set(key, new Set())
1406
+ }
1407
+ keyCallbacks.get(key)!.add(callback)
1408
+
1409
+ return () => {
1410
+ const set = keyCallbacks.get(key)
1411
+ if (set) {
1412
+ set.delete(callback)
1413
+ if (set.size === 0) {
1414
+ keyCallbacks.delete(key)
1415
+ }
1416
+ }
1417
+ }
1418
+ }, [key, callback])
1419
+
1420
+ useSWRSubscription('global-keydown', () => {
1421
+ const handler = (e: KeyboardEvent) => {
1422
+ if (e.metaKey && keyCallbacks.has(e.key)) {
1423
+ keyCallbacks.get(e.key)!.forEach(cb => cb())
1424
+ }
1425
+ }
1426
+ window.addEventListener('keydown', handler)
1427
+ return () => window.removeEventListener('keydown', handler)
1428
+ })
1429
+ }
1430
+
1431
+ function Profile() {
1432
+ // Multiple shortcuts will share the same listener
1433
+ useKeyboardShortcut('p', () => { /* ... */ })
1434
+ useKeyboardShortcut('k', () => { /* ... */ })
1435
+ // ...
1436
+ }
1437
+ ```
1438
+
1439
+ ---
1440
+
1441
+ ## Rule 4.2: Use Passive Event Listeners for Scrolling Performance
1442
+
1443
+ **Impact:** MEDIUM
1444
+ **Tags:** client, event-listeners, scrolling, performance, touch, wheel
1445
+
1446
+ ## Use Passive Event Listeners for Scrolling Performance
1447
+
1448
+ 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.
1449
+
1450
+ **Incorrect:**
1451
+
1452
+ ```typescript
1453
+ useEffect(() => {
1454
+ const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
1455
+ const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
1456
+
1457
+ document.addEventListener('touchstart', handleTouch)
1458
+ document.addEventListener('wheel', handleWheel)
1459
+
1460
+ return () => {
1461
+ document.removeEventListener('touchstart', handleTouch)
1462
+ document.removeEventListener('wheel', handleWheel)
1463
+ }
1464
+ }, [])
1465
+ ```
1466
+
1467
+ **Correct:**
1468
+
1469
+ ```typescript
1470
+ useEffect(() => {
1471
+ const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
1472
+ const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
1473
+
1474
+ document.addEventListener('touchstart', handleTouch, { passive: true })
1475
+ document.addEventListener('wheel', handleWheel, { passive: true })
1476
+
1477
+ return () => {
1478
+ document.removeEventListener('touchstart', handleTouch)
1479
+ document.removeEventListener('wheel', handleWheel)
1480
+ }
1481
+ }, [])
1482
+ ```
1483
+
1484
+ **Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
1485
+
1486
+ **Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.
1487
+
1488
+ ---
1489
+
1490
+ ## Rule 4.3: Use SWR for Automatic Deduplication
1491
+
1492
+ **Impact:** MEDIUM-HIGH
1493
+ **Tags:** client, swr, deduplication, data-fetching
1494
+
1495
+ ## Use SWR for Automatic Deduplication
1496
+
1497
+ SWR enables request deduplication, caching, and revalidation across component instances.
1498
+
1499
+ **Incorrect (no deduplication, each instance fetches):**
1500
+
1501
+ ```tsx
1502
+ function UserList() {
1503
+ const [users, setUsers] = useState([])
1504
+ useEffect(() => {
1505
+ fetch('/api/users')
1506
+ .then(r => r.json())
1507
+ .then(setUsers)
1508
+ }, [])
1509
+ }
1510
+ ```
1511
+
1512
+ **Correct (multiple instances share one request):**
1513
+
1514
+ ```tsx
1515
+ import useSWR from 'swr'
1516
+
1517
+ function UserList() {
1518
+ const { data: users } = useSWR('/api/users', fetcher)
1519
+ }
1520
+ ```
1521
+
1522
+ **For immutable data:**
1523
+
1524
+ ```tsx
1525
+ import { useImmutableSWR } from '@/lib/swr'
1526
+
1527
+ function StaticContent() {
1528
+ const { data } = useImmutableSWR('/api/config', fetcher)
1529
+ }
1530
+ ```
1531
+
1532
+ **For mutations:**
1533
+
1534
+ ```tsx
1535
+ import { useSWRMutation } from 'swr/mutation'
1536
+
1537
+ function UpdateButton() {
1538
+ const { trigger } = useSWRMutation('/api/user', updateUser)
1539
+ return <button onClick={() => trigger()}>Update</button>
1540
+ }
1541
+ ```
1542
+
1543
+ Reference: [https://swr.vercel.app](https://swr.vercel.app)
1544
+
1545
+ ---
1546
+
1547
+ ## Rule 4.4: Version and Minimize localStorage Data
1548
+
1549
+ **Impact:** MEDIUM
1550
+ **Tags:** client, localStorage, storage, versioning, data-minimization
1551
+
1552
+ ## Version and Minimize localStorage Data
1553
+
1554
+ Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.
1555
+
1556
+ **Incorrect:**
1557
+
1558
+ ```typescript
1559
+ // No version, stores everything, no error handling
1560
+ localStorage.setItem('userConfig', JSON.stringify(fullUserObject))
1561
+ const data = localStorage.getItem('userConfig')
1562
+ ```
1563
+
1564
+ **Correct:**
1565
+
1566
+ ```typescript
1567
+ const VERSION = 'v2'
1568
+
1569
+ function saveConfig(config: { theme: string; language: string }) {
1570
+ try {
1571
+ localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
1572
+ } catch {
1573
+ // Throws in incognito/private browsing, quota exceeded, or disabled
1574
+ }
1575
+ }
1576
+
1577
+ function loadConfig() {
1578
+ try {
1579
+ const data = localStorage.getItem(`userConfig:${VERSION}`)
1580
+ return data ? JSON.parse(data) : null
1581
+ } catch {
1582
+ return null
1583
+ }
1584
+ }
1585
+
1586
+ // Migration from v1 to v2
1587
+ function migrate() {
1588
+ try {
1589
+ const v1 = localStorage.getItem('userConfig:v1')
1590
+ if (v1) {
1591
+ const old = JSON.parse(v1)
1592
+ saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
1593
+ localStorage.removeItem('userConfig:v1')
1594
+ }
1595
+ } catch {}
1596
+ }
1597
+ ```
1598
+
1599
+ **Store minimal fields from server responses:**
1600
+
1601
+ ```typescript
1602
+ // User object has 20+ fields, only store what UI needs
1603
+ function cachePrefs(user: FullUser) {
1604
+ try {
1605
+ localStorage.setItem('prefs:v1', JSON.stringify({
1606
+ theme: user.preferences.theme,
1607
+ notifications: user.preferences.notifications
1608
+ }))
1609
+ } catch {}
1610
+ }
1611
+ ```
1612
+
1613
+ **Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.
1614
+
1615
+ **Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.
1616
+
1617
+
1618
+
1619
+
1620
+ ---
1621
+ # Content from 5-rerender-re-render-optimization.md
1622
+
1623
+ # 5. Re-render Optimization
1624
+
1625
+ > **Impact:** MEDIUM
1626
+ > **Focus:** Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
1627
+
1628
+ ---
1629
+
1630
+ ## Overview
1631
+
1632
+ This section contains **12 rules** focused on re-render optimization.
1633
+
1634
+ ---
1635
+
1636
+ ## Rule 5.1: Calculate Derived State During Rendering
1637
+
1638
+ **Impact:** MEDIUM
1639
+ **Tags:** rerender, derived-state, useEffect, state
1640
+
1641
+ ## Calculate Derived State During Rendering
1642
+
1643
+ 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.
1644
+
1645
+ **Incorrect (redundant state and effect):**
1646
+
1647
+ ```tsx
1648
+ function Form() {
1649
+ const [firstName, setFirstName] = useState('First')
1650
+ const [lastName, setLastName] = useState('Last')
1651
+ const [fullName, setFullName] = useState('')
1652
+
1653
+ useEffect(() => {
1654
+ setFullName(firstName + ' ' + lastName)
1655
+ }, [firstName, lastName])
1656
+
1657
+ return <p>{fullName}</p>
1658
+ }
1659
+ ```
1660
+
1661
+ **Correct (derive during render):**
1662
+
1663
+ ```tsx
1664
+ function Form() {
1665
+ const [firstName, setFirstName] = useState('First')
1666
+ const [lastName, setLastName] = useState('Last')
1667
+ const fullName = firstName + ' ' + lastName
1668
+
1669
+ return <p>{fullName}</p>
1670
+ }
1671
+ ```
1672
+
1673
+ References: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
1674
+
1675
+ ---
1676
+
1677
+ ## Rule 5.2: Defer State Reads to Usage Point
1678
+
1679
+ **Impact:** MEDIUM
1680
+ **Tags:** rerender, searchParams, localStorage, optimization
1681
+
1682
+ ## Defer State Reads to Usage Point
1683
+
1684
+ Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
1685
+
1686
+ **Incorrect (subscribes to all searchParams changes):**
1687
+
1688
+ ```tsx
1689
+ function ShareButton({ chatId }: { chatId: string }) {
1690
+ const searchParams = useSearchParams()
1691
+
1692
+ const handleShare = () => {
1693
+ const ref = searchParams.get('ref')
1694
+ shareChat(chatId, { ref })
1695
+ }
1696
+
1697
+ return <button onClick={handleShare}>Share</button>
1698
+ }
1699
+ ```
1700
+
1701
+ **Correct (reads on demand, no subscription):**
1702
+
1703
+ ```tsx
1704
+ function ShareButton({ chatId }: { chatId: string }) {
1705
+ const handleShare = () => {
1706
+ const params = new URLSearchParams(window.location.search)
1707
+ const ref = params.get('ref')
1708
+ shareChat(chatId, { ref })
1709
+ }
1710
+
1711
+ return <button onClick={handleShare}>Share</button>
1712
+ }
1713
+ ```
1714
+
1715
+ ---
1716
+
1717
+ ## Rule 5.3: Do not wrap a simple expression with a primitive result type in useMemo
1718
+
1719
+ **Impact:** LOW-MEDIUM
1720
+ **Tags:** rerender, useMemo, optimization
1721
+
1722
+ ## Do not wrap a simple expression with a primitive result type in useMemo
1723
+
1724
+ 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`.
1725
+ Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
1726
+
1727
+ **Incorrect:**
1728
+
1729
+ ```tsx
1730
+ function Header({ user, notifications }: Props) {
1731
+ const isLoading = useMemo(() => {
1732
+ return user.isLoading || notifications.isLoading
1733
+ }, [user.isLoading, notifications.isLoading])
1734
+
1735
+ if (isLoading) return <Skeleton />
1736
+ // return some markup
1737
+ }
1738
+ ```
1739
+
1740
+ **Correct:**
1741
+
1742
+ ```tsx
1743
+ function Header({ user, notifications }: Props) {
1744
+ const isLoading = user.isLoading || notifications.isLoading
1745
+
1746
+ if (isLoading) return <Skeleton />
1747
+ // return some markup
1748
+ }
1749
+ ```
1750
+
1751
+ ---
1752
+
1753
+ ## Rule 5.4: Extract Default Non-primitive Parameter Value from Memoized Component to Constant
1754
+
1755
+ **Impact:** MEDIUM
1756
+ **Tags:** rerender, memo, optimization
1757
+
1758
+ ## Extract Default Non-primitive Parameter Value from Memoized Component to Constant
1759
+
1760
+ 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()`.
1761
+
1762
+ To address this issue, extract the default value into a constant.
1763
+
1764
+ **Incorrect (`onClick` has different values on every rerender):**
1765
+
1766
+ ```tsx
1767
+ const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
1768
+ // ...
1769
+ })
1770
+
1771
+ // Used without optional onClick
1772
+ <UserAvatar />
1773
+ ```
1774
+
1775
+ **Correct (stable default value):**
1776
+
1777
+ ```tsx
1778
+ const NOOP = () => {};
1779
+
1780
+ const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
1781
+ // ...
1782
+ })
1783
+
1784
+ // Used without optional onClick
1785
+ <UserAvatar />
1786
+ ```
1787
+
1788
+ ---
1789
+
1790
+ ## Rule 5.5: Extract to Memoized Components
1791
+
1792
+ **Impact:** MEDIUM
1793
+ **Tags:** rerender, memo, useMemo, optimization
1794
+
1795
+ ## Extract to Memoized Components
1796
+
1797
+ Extract expensive work into memoized components to enable early returns before computation.
1798
+
1799
+ **Incorrect (computes avatar even when loading):**
1800
+
1801
+ ```tsx
1802
+ function Profile({ user, loading }: Props) {
1803
+ const avatar = useMemo(() => {
1804
+ const id = computeAvatarId(user)
1805
+ return <Avatar id={id} />
1806
+ }, [user])
1807
+
1808
+ if (loading) return <Skeleton />
1809
+ return <div>{avatar}</div>
1810
+ }
1811
+ ```
1812
+
1813
+ **Correct (skips computation when loading):**
1814
+
1815
+ ```tsx
1816
+ const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
1817
+ const id = useMemo(() => computeAvatarId(user), [user])
1818
+ return <Avatar id={id} />
1819
+ })
1820
+
1821
+ function Profile({ user, loading }: Props) {
1822
+ if (loading) return <Skeleton />
1823
+ return (
1824
+ <div>
1825
+ <UserAvatar user={user} />
1826
+ </div>
1827
+ )
1828
+ }
1829
+ ```
1830
+
1831
+ **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.
1832
+
1833
+ ---
1834
+
1835
+ ## Rule 5.6: Narrow Effect Dependencies
1836
+
1837
+ **Impact:** LOW
1838
+ **Tags:** rerender, useEffect, dependencies, optimization
1839
+
1840
+ ## Narrow Effect Dependencies
1841
+
1842
+ Specify primitive dependencies instead of objects to minimize effect re-runs.
1843
+
1844
+ **Incorrect (re-runs on any user field change):**
1845
+
1846
+ ```tsx
1847
+ useEffect(() => {
1848
+ console.log(user.id)
1849
+ }, [user])
1850
+ ```
1851
+
1852
+ **Correct (re-runs only when id changes):**
1853
+
1854
+ ```tsx
1855
+ useEffect(() => {
1856
+ console.log(user.id)
1857
+ }, [user.id])
1858
+ ```
1859
+
1860
+ **For derived state, compute outside effect:**
1861
+
1862
+ ```tsx
1863
+ // Incorrect: runs on width=767, 766, 765...
1864
+ useEffect(() => {
1865
+ if (width < 768) {
1866
+ enableMobileMode()
1867
+ }
1868
+ }, [width])
1869
+
1870
+ // Correct: runs only on boolean transition
1871
+ const isMobile = width < 768
1872
+ useEffect(() => {
1873
+ if (isMobile) {
1874
+ enableMobileMode()
1875
+ }
1876
+ }, [isMobile])
1877
+ ```
1878
+
1879
+ ---
1880
+
1881
+ ## Rule 5.7: Put Interaction Logic in Event Handlers
1882
+
1883
+ **Impact:** MEDIUM
1884
+ **Tags:** rerender, useEffect, events, side-effects, dependencies
1885
+
1886
+ ## Put Interaction Logic in Event Handlers
1887
+
1888
+ 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.
1889
+
1890
+ **Incorrect (event modeled as state + effect):**
1891
+
1892
+ ```tsx
1893
+ function Form() {
1894
+ const [submitted, setSubmitted] = useState(false)
1895
+ const theme = useContext(ThemeContext)
1896
+
1897
+ useEffect(() => {
1898
+ if (submitted) {
1899
+ post('/api/register')
1900
+ showToast('Registered', theme)
1901
+ }
1902
+ }, [submitted, theme])
1903
+
1904
+ return <button onClick={() => setSubmitted(true)}>Submit</button>
1905
+ }
1906
+ ```
1907
+
1908
+ **Correct (do it in the handler):**
1909
+
1910
+ ```tsx
1911
+ function Form() {
1912
+ const theme = useContext(ThemeContext)
1913
+
1914
+ function handleSubmit() {
1915
+ post('/api/register')
1916
+ showToast('Registered', theme)
1917
+ }
1918
+
1919
+ return <button onClick={handleSubmit}>Submit</button>
1920
+ }
1921
+ ```
1922
+
1923
+ Reference: [Should this code move to an event handler?](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
1924
+
1925
+ ---
1926
+
1927
+ ## Rule 5.8: Subscribe to Derived State
1928
+
1929
+ **Impact:** MEDIUM
1930
+ **Tags:** rerender, derived-state, media-query, optimization
1931
+
1932
+ ## Subscribe to Derived State
1933
+
1934
+ Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
1935
+
1936
+ **Incorrect (re-renders on every pixel change):**
1937
+
1938
+ ```tsx
1939
+ function Sidebar() {
1940
+ const width = useWindowWidth() // updates continuously
1941
+ const isMobile = width < 768
1942
+ return <nav className={isMobile ? 'mobile' : 'desktop'} />
1943
+ }
1944
+ ```
1945
+
1946
+ **Correct (re-renders only when boolean changes):**
1947
+
1948
+ ```tsx
1949
+ function Sidebar() {
1950
+ const isMobile = useMediaQuery('(max-width: 767px)')
1951
+ return <nav className={isMobile ? 'mobile' : 'desktop'} />
1952
+ }
1953
+ ```
1954
+
1955
+ ---
1956
+
1957
+ ## Rule 5.9: Use Functional setState Updates
1958
+
1959
+ **Impact:** MEDIUM
1960
+ **Tags:** react, hooks, useState, useCallback, callbacks, closures
1961
+
1962
+ ## Use Functional setState Updates
1963
+
1964
+ 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.
1965
+
1966
+ **Incorrect (requires state as dependency):**
1967
+
1968
+ ```tsx
1969
+ function TodoList() {
1970
+ const [items, setItems] = useState(initialItems)
1971
+
1972
+ // Callback must depend on items, recreated on every items change
1973
+ const addItems = useCallback((newItems: Item[]) => {
1974
+ setItems([...items, ...newItems])
1975
+ }, [items]) // ❌ items dependency causes recreations
1976
+
1977
+ // Risk of stale closure if dependency is forgotten
1978
+ const removeItem = useCallback((id: string) => {
1979
+ setItems(items.filter(item => item.id !== id))
1980
+ }, []) // ❌ Missing items dependency - will use stale items!
1981
+
1982
+ return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
1983
+ }
1984
+ ```
1985
+
1986
+ 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.
1987
+
1988
+ **Correct (stable callbacks, no stale closures):**
1989
+
1990
+ ```tsx
1991
+ function TodoList() {
1992
+ const [items, setItems] = useState(initialItems)
1993
+
1994
+ // Stable callback, never recreated
1995
+ const addItems = useCallback((newItems: Item[]) => {
1996
+ setItems(curr => [...curr, ...newItems])
1997
+ }, []) // ✅ No dependencies needed
1998
+
1999
+ // Always uses latest state, no stale closure risk
2000
+ const removeItem = useCallback((id: string) => {
2001
+ setItems(curr => curr.filter(item => item.id !== id))
2002
+ }, []) // ✅ Safe and stable
2003
+
2004
+ return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
2005
+ }
2006
+ ```
2007
+
2008
+ **Benefits:**
2009
+
2010
+ 1. **Stable callback references** - Callbacks don't need to be recreated when state changes
2011
+ 2. **No stale closures** - Always operates on the latest state value
2012
+ 3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
2013
+ 4. **Prevents bugs** - Eliminates the most common source of React closure bugs
2014
+
2015
+ **When to use functional updates:**
2016
+
2017
+ - Any setState that depends on the current state value
2018
+ - Inside useCallback/useMemo when state is needed
2019
+ - Event handlers that reference state
2020
+ - Async operations that update state
2021
+
2022
+ **When direct updates are fine:**
2023
+
2024
+ - Setting state to a static value: `setCount(0)`
2025
+ - Setting state from props/arguments only: `setName(newName)`
2026
+ - State doesn't depend on previous value
2027
+
2028
+ **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.
2029
+
2030
+ ---
2031
+
2032
+ ## Rule 5.10: Use Lazy State Initialization
2033
+
2034
+ **Impact:** MEDIUM
2035
+ **Tags:** react, hooks, useState, performance, initialization
2036
+
2037
+ ## Use Lazy State Initialization
2038
+
2039
+ 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.
2040
+
2041
+ **Incorrect (runs on every render):**
2042
+
2043
+ ```tsx
2044
+ function FilteredList({ items }: { items: Item[] }) {
2045
+ // buildSearchIndex() runs on EVERY render, even after initialization
2046
+ const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
2047
+ const [query, setQuery] = useState('')
2048
+
2049
+ // When query changes, buildSearchIndex runs again unnecessarily
2050
+ return <SearchResults index={searchIndex} query={query} />
2051
+ }
2052
+
2053
+ function UserProfile() {
2054
+ // JSON.parse runs on every render
2055
+ const [settings, setSettings] = useState(
2056
+ JSON.parse(localStorage.getItem('settings') || '{}')
2057
+ )
2058
+
2059
+ return <SettingsForm settings={settings} onChange={setSettings} />
2060
+ }
2061
+ ```
2062
+
2063
+ **Correct (runs only once):**
2064
+
2065
+ ```tsx
2066
+ function FilteredList({ items }: { items: Item[] }) {
2067
+ // buildSearchIndex() runs ONLY on initial render
2068
+ const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
2069
+ const [query, setQuery] = useState('')
2070
+
2071
+ return <SearchResults index={searchIndex} query={query} />
2072
+ }
2073
+
2074
+ function UserProfile() {
2075
+ // JSON.parse runs only on initial render
2076
+ const [settings, setSettings] = useState(() => {
2077
+ const stored = localStorage.getItem('settings')
2078
+ return stored ? JSON.parse(stored) : {}
2079
+ })
2080
+
2081
+ return <SettingsForm settings={settings} onChange={setSettings} />
2082
+ }
2083
+ ```
2084
+
2085
+ Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
2086
+
2087
+ For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
2088
+
2089
+ ---
2090
+
2091
+ ## Rule 5.11: Use Transitions for Non-Urgent Updates
2092
+
2093
+ **Impact:** MEDIUM
2094
+ **Tags:** rerender, transitions, startTransition, performance
2095
+
2096
+ ## Use Transitions for Non-Urgent Updates
2097
+
2098
+ Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
2099
+
2100
+ **Incorrect (blocks UI on every scroll):**
2101
+
2102
+ ```tsx
2103
+ function ScrollTracker() {
2104
+ const [scrollY, setScrollY] = useState(0)
2105
+ useEffect(() => {
2106
+ const handler = () => setScrollY(window.scrollY)
2107
+ window.addEventListener('scroll', handler, { passive: true })
2108
+ return () => window.removeEventListener('scroll', handler)
2109
+ }, [])
2110
+ }
2111
+ ```
2112
+
2113
+ **Correct (non-blocking updates):**
2114
+
2115
+ ```tsx
2116
+ import { startTransition } from 'react'
2117
+
2118
+ function ScrollTracker() {
2119
+ const [scrollY, setScrollY] = useState(0)
2120
+ useEffect(() => {
2121
+ const handler = () => {
2122
+ startTransition(() => setScrollY(window.scrollY))
2123
+ }
2124
+ window.addEventListener('scroll', handler, { passive: true })
2125
+ return () => window.removeEventListener('scroll', handler)
2126
+ }, [])
2127
+ }
2128
+ ```
2129
+
2130
+ ---
2131
+
2132
+ ## Rule 5.12: Use useRef for Transient Values
2133
+
2134
+ **Impact:** MEDIUM
2135
+ **Tags:** rerender, useref, state, performance
2136
+
2137
+ ## Use useRef for Transient Values
2138
+
2139
+ 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.
2140
+
2141
+ **Incorrect (renders every update):**
2142
+
2143
+ ```tsx
2144
+ function Tracker() {
2145
+ const [lastX, setLastX] = useState(0)
2146
+
2147
+ useEffect(() => {
2148
+ const onMove = (e: MouseEvent) => setLastX(e.clientX)
2149
+ window.addEventListener('mousemove', onMove)
2150
+ return () => window.removeEventListener('mousemove', onMove)
2151
+ }, [])
2152
+
2153
+ return (
2154
+ <div
2155
+ style={{
2156
+ position: 'fixed',
2157
+ top: 0,
2158
+ left: lastX,
2159
+ width: 8,
2160
+ height: 8,
2161
+ background: 'black',
2162
+ }}
2163
+ />
2164
+ )
2165
+ }
2166
+ ```
2167
+
2168
+ **Correct (no re-render for tracking):**
2169
+
2170
+ ```tsx
2171
+ function Tracker() {
2172
+ const lastXRef = useRef(0)
2173
+ const dotRef = useRef<HTMLDivElement>(null)
2174
+
2175
+ useEffect(() => {
2176
+ const onMove = (e: MouseEvent) => {
2177
+ lastXRef.current = e.clientX
2178
+ const node = dotRef.current
2179
+ if (node) {
2180
+ node.style.transform = `translateX(${e.clientX}px)`
2181
+ }
2182
+ }
2183
+ window.addEventListener('mousemove', onMove)
2184
+ return () => window.removeEventListener('mousemove', onMove)
2185
+ }, [])
2186
+
2187
+ return (
2188
+ <div
2189
+ ref={dotRef}
2190
+ style={{
2191
+ position: 'fixed',
2192
+ top: 0,
2193
+ left: 0,
2194
+ width: 8,
2195
+ height: 8,
2196
+ background: 'black',
2197
+ transform: 'translateX(0px)',
2198
+ }}
2199
+ />
2200
+ )
2201
+ }
2202
+ ```
2203
+
2204
+
2205
+
2206
+
2207
+ ---
2208
+ # Content from 6-rendering-rendering-performance.md
2209
+
2210
+ # 6. Rendering Performance
2211
+
2212
+ > **Impact:** MEDIUM
2213
+ > **Focus:** Optimizing the rendering process reduces the work the browser needs to do.
2214
+
2215
+ ---
2216
+
2217
+ ## Overview
2218
+
2219
+ This section contains **9 rules** focused on rendering performance.
2220
+
2221
+ ---
2222
+
2223
+ ## Rule 6.1: Animate SVG Wrapper Instead of SVG Element
2224
+
2225
+ **Impact:** LOW
2226
+ **Tags:** rendering, svg, css, animation, performance
2227
+
2228
+ ## Animate SVG Wrapper Instead of SVG Element
2229
+
2230
+ Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.
2231
+
2232
+ **Incorrect (animating SVG directly - no hardware acceleration):**
2233
+
2234
+ ```tsx
2235
+ function LoadingSpinner() {
2236
+ return (
2237
+ <svg
2238
+ className="animate-spin"
2239
+ width="24"
2240
+ height="24"
2241
+ viewBox="0 0 24 24"
2242
+ >
2243
+ <circle cx="12" cy="12" r="10" stroke="currentColor" />
2244
+ </svg>
2245
+ )
2246
+ }
2247
+ ```
2248
+
2249
+ **Correct (animating wrapper div - hardware accelerated):**
2250
+
2251
+ ```tsx
2252
+ function LoadingSpinner() {
2253
+ return (
2254
+ <div className="animate-spin">
2255
+ <svg
2256
+ width="24"
2257
+ height="24"
2258
+ viewBox="0 0 24 24"
2259
+ >
2260
+ <circle cx="12" cy="12" r="10" stroke="currentColor" />
2261
+ </svg>
2262
+ </div>
2263
+ )
2264
+ }
2265
+ ```
2266
+
2267
+ 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.
2268
+
2269
+ ---
2270
+
2271
+ ## Rule 6.2: CSS content-visibility for Long Lists
2272
+
2273
+ **Impact:** HIGH
2274
+ **Tags:** rendering, css, content-visibility, long-lists
2275
+
2276
+ ## CSS content-visibility for Long Lists
2277
+
2278
+ Apply `content-visibility: auto` to defer off-screen rendering.
2279
+
2280
+ **CSS:**
2281
+
2282
+ ```css
2283
+ .message-item {
2284
+ content-visibility: auto;
2285
+ contain-intrinsic-size: 0 80px;
2286
+ }
2287
+ ```
2288
+
2289
+ **Example:**
2290
+
2291
+ ```tsx
2292
+ function MessageList({ messages }: { messages: Message[] }) {
2293
+ return (
2294
+ <div className="overflow-y-auto h-screen">
2295
+ {messages.map(msg => (
2296
+ <div key={msg.id} className="message-item">
2297
+ <Avatar user={msg.author} />
2298
+ <div>{msg.content}</div>
2299
+ </div>
2300
+ ))}
2301
+ </div>
2302
+ )
2303
+ }
2304
+ ```
2305
+
2306
+ For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
2307
+
2308
+ ---
2309
+
2310
+ ## Rule 6.3: Hoist Static JSX Elements
2311
+
2312
+ **Impact:** LOW
2313
+ **Tags:** rendering, jsx, static, optimization
2314
+
2315
+ ## Hoist Static JSX Elements
2316
+
2317
+ Extract static JSX outside components to avoid re-creation.
2318
+
2319
+ **Incorrect (recreates element every render):**
2320
+
2321
+ ```tsx
2322
+ function LoadingSkeleton() {
2323
+ return <div className="animate-pulse h-20 bg-gray-200" />
2324
+ }
2325
+
2326
+ function Container() {
2327
+ return (
2328
+ <div>
2329
+ {loading && <LoadingSkeleton />}
2330
+ </div>
2331
+ )
2332
+ }
2333
+ ```
2334
+
2335
+ **Correct (reuses same element):**
2336
+
2337
+ ```tsx
2338
+ const loadingSkeleton = (
2339
+ <div className="animate-pulse h-20 bg-gray-200" />
2340
+ )
2341
+
2342
+ function Container() {
2343
+ return (
2344
+ <div>
2345
+ {loading && loadingSkeleton}
2346
+ </div>
2347
+ )
2348
+ }
2349
+ ```
2350
+
2351
+ This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
2352
+
2353
+ **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.
2354
+
2355
+ ---
2356
+
2357
+ ## Rule 6.4: Optimize SVG Precision
2358
+
2359
+ **Impact:** LOW
2360
+ **Tags:** rendering, svg, optimization, svgo
2361
+
2362
+ ## Optimize SVG Precision
2363
+
2364
+ Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
2365
+
2366
+ **Incorrect (excessive precision):**
2367
+
2368
+ ```svg
2369
+ <path d="M 10.293847 20.847362 L 30.938472 40.192837" />
2370
+ ```
2371
+
2372
+ **Correct (1 decimal place):**
2373
+
2374
+ ```svg
2375
+ <path d="M 10.3 20.8 L 30.9 40.2" />
2376
+ ```
2377
+
2378
+ **Automate with SVGO:**
2379
+
2380
+ ```bash
2381
+ npx svgo --precision=1 --multipass icon.svg
2382
+ ```
2383
+
2384
+ ---
2385
+
2386
+ ## Rule 6.5: Prevent Hydration Mismatch Without Flickering
2387
+
2388
+ **Impact:** MEDIUM
2389
+ **Tags:** rendering, ssr, hydration, localStorage, flicker
2390
+
2391
+ ## Prevent Hydration Mismatch Without Flickering
2392
+
2393
+ 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.
2394
+
2395
+ **Incorrect (breaks SSR):**
2396
+
2397
+ ```tsx
2398
+ function ThemeWrapper({ children }: { children: ReactNode }) {
2399
+ // localStorage is not available on server - throws error
2400
+ const theme = localStorage.getItem('theme') || 'light'
2401
+
2402
+ return (
2403
+ <div className={theme}>
2404
+ {children}
2405
+ </div>
2406
+ )
2407
+ }
2408
+ ```
2409
+
2410
+ Server-side rendering will fail because `localStorage` is undefined.
2411
+
2412
+ **Incorrect (visual flickering):**
2413
+
2414
+ ```tsx
2415
+ function ThemeWrapper({ children }: { children: ReactNode }) {
2416
+ const [theme, setTheme] = useState('light')
2417
+
2418
+ useEffect(() => {
2419
+ // Runs after hydration - causes visible flash
2420
+ const stored = localStorage.getItem('theme')
2421
+ if (stored) {
2422
+ setTheme(stored)
2423
+ }
2424
+ }, [])
2425
+
2426
+ return (
2427
+ <div className={theme}>
2428
+ {children}
2429
+ </div>
2430
+ )
2431
+ }
2432
+ ```
2433
+
2434
+ Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
2435
+
2436
+ **Correct (no flicker, no hydration mismatch):**
2437
+
2438
+ ```tsx
2439
+ function ThemeWrapper({ children }: { children: ReactNode }) {
2440
+ return (
2441
+ <>
2442
+ <div id="theme-wrapper">
2443
+ {children}
2444
+ </div>
2445
+ <script
2446
+ dangerouslySetInnerHTML={{
2447
+ __html: `
2448
+ (function() {
2449
+ try {
2450
+ var theme = localStorage.getItem('theme') || 'light';
2451
+ var el = document.getElementById('theme-wrapper');
2452
+ if (el) el.className = theme;
2453
+ } catch (e) {}
2454
+ })();
2455
+ `,
2456
+ }}
2457
+ />
2458
+ </>
2459
+ )
2460
+ }
2461
+ ```
2462
+
2463
+ The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
2464
+
2465
+ 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.
2466
+
2467
+ ---
2468
+
2469
+ ## Rule 6.6: Suppress Expected Hydration Mismatches
2470
+
2471
+ **Impact:** LOW-MEDIUM
2472
+ **Tags:** rendering, hydration, ssr, nextjs
2473
+
2474
+ ## Suppress Expected Hydration Mismatches
2475
+
2476
+ 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.
2477
+
2478
+ **Incorrect (known mismatch warnings):**
2479
+
2480
+ ```tsx
2481
+ function Timestamp() {
2482
+ return <span>{new Date().toLocaleString()}</span>
2483
+ }
2484
+ ```
2485
+
2486
+ **Correct (suppress expected mismatch only):**
2487
+
2488
+ ```tsx
2489
+ function Timestamp() {
2490
+ return (
2491
+ <span suppressHydrationWarning>
2492
+ {new Date().toLocaleString()}
2493
+ </span>
2494
+ )
2495
+ }
2496
+ ```
2497
+
2498
+ ---
2499
+
2500
+ ## Rule 6.7: Use Activity Component for Show/Hide
2501
+
2502
+ **Impact:** MEDIUM
2503
+ **Tags:** rendering, activity, visibility, state-preservation
2504
+
2505
+ ## Use Activity Component for Show/Hide
2506
+
2507
+ Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.
2508
+
2509
+ **Usage:**
2510
+
2511
+ ```tsx
2512
+ import { Activity } from 'react'
2513
+
2514
+ function Dropdown({ isOpen }: Props) {
2515
+ return (
2516
+ <Activity mode={isOpen ? 'visible' : 'hidden'}>
2517
+ <ExpensiveMenu />
2518
+ </Activity>
2519
+ )
2520
+ }
2521
+ ```
2522
+
2523
+ Avoids expensive re-renders and state loss.
2524
+
2525
+ ---
2526
+
2527
+ ## Rule 6.8: Use Explicit Conditional Rendering
2528
+
2529
+ **Impact:** LOW
2530
+ **Tags:** rendering, conditional, jsx, falsy-values
2531
+
2532
+ ## Use Explicit Conditional Rendering
2533
+
2534
+ Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
2535
+
2536
+ **Incorrect (renders "0" when count is 0):**
2537
+
2538
+ ```tsx
2539
+ function Badge({ count }: { count: number }) {
2540
+ return (
2541
+ <div>
2542
+ {count && <span className="badge">{count}</span>}
2543
+ </div>
2544
+ )
2545
+ }
2546
+
2547
+ // When count = 0, renders: <div>0</div>
2548
+ // When count = 5, renders: <div><span class="badge">5</span></div>
2549
+ ```
2550
+
2551
+ **Correct (renders nothing when count is 0):**
2552
+
2553
+ ```tsx
2554
+ function Badge({ count }: { count: number }) {
2555
+ return (
2556
+ <div>
2557
+ {count > 0 ? <span className="badge">{count}</span> : null}
2558
+ </div>
2559
+ )
2560
+ }
2561
+
2562
+ // When count = 0, renders: <div></div>
2563
+ // When count = 5, renders: <div><span class="badge">5</span></div>
2564
+ ```
2565
+
2566
+ ---
2567
+
2568
+ ## Rule 6.9: Use useTransition Over Manual Loading States
2569
+
2570
+ **Impact:** LOW
2571
+ **Tags:** rendering, transitions, useTransition, loading, state
2572
+
2573
+ ## Use useTransition Over Manual Loading States
2574
+
2575
+ Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
2576
+
2577
+ **Incorrect (manual loading state):**
2578
+
2579
+ ```tsx
2580
+ function SearchResults() {
2581
+ const [query, setQuery] = useState('')
2582
+ const [results, setResults] = useState([])
2583
+ const [isLoading, setIsLoading] = useState(false)
2584
+
2585
+ const handleSearch = async (value: string) => {
2586
+ setIsLoading(true)
2587
+ setQuery(value)
2588
+ const data = await fetchResults(value)
2589
+ setResults(data)
2590
+ setIsLoading(false)
2591
+ }
2592
+
2593
+ return (
2594
+ <>
2595
+ <input onChange={(e) => handleSearch(e.target.value)} />
2596
+ {isLoading && <Spinner />}
2597
+ <ResultsList results={results} />
2598
+ </>
2599
+ )
2600
+ }
2601
+ ```
2602
+
2603
+ **Correct (useTransition with built-in pending state):**
2604
+
2605
+ ```tsx
2606
+ import { useTransition, useState } from 'react'
2607
+
2608
+ function SearchResults() {
2609
+ const [query, setQuery] = useState('')
2610
+ const [results, setResults] = useState([])
2611
+ const [isPending, startTransition] = useTransition()
2612
+
2613
+ const handleSearch = (value: string) => {
2614
+ setQuery(value) // Update input immediately
2615
+
2616
+ startTransition(async () => {
2617
+ // Fetch and update results
2618
+ const data = await fetchResults(value)
2619
+ setResults(data)
2620
+ })
2621
+ }
2622
+
2623
+ return (
2624
+ <>
2625
+ <input onChange={(e) => handleSearch(e.target.value)} />
2626
+ {isPending && <Spinner />}
2627
+ <ResultsList results={results} />
2628
+ </>
2629
+ )
2630
+ }
2631
+ ```
2632
+
2633
+ **Benefits:**
2634
+
2635
+ - **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
2636
+ - **Error resilience**: Pending state correctly resets even if the transition throws
2637
+ - **Better responsiveness**: Keeps the UI responsive during updates
2638
+ - **Interrupt handling**: New transitions automatically cancel pending ones
2639
+
2640
+ Reference: [useTransition](https://react.dev/reference/react/useTransition)
2641
+
2642
+
2643
+
2644
+
2645
+ ---
2646
+ # Content from 7-js-javascript-performance.md
2647
+
2648
+ # 7. JavaScript Performance
2649
+
2650
+ > **Impact:** LOW-MEDIUM
2651
+ > **Focus:** Micro-optimizations for hot paths can add up to meaningful improvements.
2652
+
2653
+ ---
2654
+
2655
+ ## Overview
2656
+
2657
+ This section contains **12 rules** focused on javascript performance.
2658
+
2659
+ ---
2660
+
2661
+ ## Rule 7.1: Avoid Layout Thrashing
2662
+
2663
+ **Impact:** MEDIUM
2664
+ **Tags:** javascript, dom, css, performance, reflow, layout-thrashing
2665
+
2666
+ ## Avoid Layout Thrashing
2667
+
2668
+ 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.
2669
+
2670
+ **This is OK (browser batches style changes):**
2671
+ ```typescript
2672
+ function updateElementStyles(element: HTMLElement) {
2673
+ // Each line invalidates style, but browser batches the recalculation
2674
+ element.style.width = '100px'
2675
+ element.style.height = '200px'
2676
+ element.style.backgroundColor = 'blue'
2677
+ element.style.border = '1px solid black'
2678
+ }
2679
+ ```
2680
+
2681
+ **Incorrect (interleaved reads and writes force reflows):**
2682
+ ```typescript
2683
+ function layoutThrashing(element: HTMLElement) {
2684
+ element.style.width = '100px'
2685
+ const width = element.offsetWidth // Forces reflow
2686
+ element.style.height = '200px'
2687
+ const height = element.offsetHeight // Forces another reflow
2688
+ }
2689
+ ```
2690
+
2691
+ **Correct (batch writes, then read once):**
2692
+ ```typescript
2693
+ function updateElementStyles(element: HTMLElement) {
2694
+ // Batch all writes together
2695
+ element.style.width = '100px'
2696
+ element.style.height = '200px'
2697
+ element.style.backgroundColor = 'blue'
2698
+ element.style.border = '1px solid black'
2699
+
2700
+ // Read after all writes are done (single reflow)
2701
+ const { width, height } = element.getBoundingClientRect()
2702
+ }
2703
+ ```
2704
+
2705
+ **Correct (batch reads, then writes):**
2706
+ ```typescript
2707
+ function avoidThrashing(element: HTMLElement) {
2708
+ // Read phase - all layout queries first
2709
+ const rect1 = element.getBoundingClientRect()
2710
+ const offsetWidth = element.offsetWidth
2711
+ const offsetHeight = element.offsetHeight
2712
+
2713
+ // Write phase - all style changes after
2714
+ element.style.width = '100px'
2715
+ element.style.height = '200px'
2716
+ }
2717
+ ```
2718
+
2719
+ **Better: use CSS classes**
2720
+ ```css
2721
+ .highlighted-box {
2722
+ width: 100px;
2723
+ height: 200px;
2724
+ background-color: blue;
2725
+ border: 1px solid black;
2726
+ }
2727
+ ```
2728
+ ```typescript
2729
+ function updateElementStyles(element: HTMLElement) {
2730
+ element.classList.add('highlighted-box')
2731
+
2732
+ const { width, height } = element.getBoundingClientRect()
2733
+ }
2734
+ ```
2735
+
2736
+ **React example:**
2737
+ ```tsx
2738
+ // Incorrect: interleaving style changes with layout queries
2739
+ function Box({ isHighlighted }: { isHighlighted: boolean }) {
2740
+ const ref = useRef<HTMLDivElement>(null)
2741
+
2742
+ useEffect(() => {
2743
+ if (ref.current && isHighlighted) {
2744
+ ref.current.style.width = '100px'
2745
+ const width = ref.current.offsetWidth // Forces layout
2746
+ ref.current.style.height = '200px'
2747
+ }
2748
+ }, [isHighlighted])
2749
+
2750
+ return <div ref={ref}>Content</div>
2751
+ }
2752
+
2753
+ // Correct: toggle class
2754
+ function Box({ isHighlighted }: { isHighlighted: boolean }) {
2755
+ return (
2756
+ <div className={isHighlighted ? 'highlighted-box' : ''}>
2757
+ Content
2758
+ </div>
2759
+ )
2760
+ }
2761
+ ```
2762
+
2763
+ 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.
2764
+
2765
+ See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
2766
+
2767
+ ---
2768
+
2769
+ ## Rule 7.2: Build Index Maps for Repeated Lookups
2770
+
2771
+ **Impact:** LOW-MEDIUM
2772
+ **Tags:** javascript, map, indexing, optimization, performance
2773
+
2774
+ ## Build Index Maps for Repeated Lookups
2775
+
2776
+ Multiple `.find()` calls by the same key should use a Map.
2777
+
2778
+ **Incorrect (O(n) per lookup):**
2779
+
2780
+ ```typescript
2781
+ function processOrders(orders: Order[], users: User[]) {
2782
+ return orders.map(order => ({
2783
+ ...order,
2784
+ user: users.find(u => u.id === order.userId)
2785
+ }))
2786
+ }
2787
+ ```
2788
+
2789
+ **Correct (O(1) per lookup):**
2790
+
2791
+ ```typescript
2792
+ function processOrders(orders: Order[], users: User[]) {
2793
+ const userById = new Map(users.map(u => [u.id, u]))
2794
+
2795
+ return orders.map(order => ({
2796
+ ...order,
2797
+ user: userById.get(order.userId)
2798
+ }))
2799
+ }
2800
+ ```
2801
+
2802
+ Build map once (O(n)), then all lookups are O(1).
2803
+ For 1000 orders × 1000 users: 1M ops → 2K ops.
2804
+
2805
+ ---
2806
+
2807
+ ## Rule 7.3: Cache Property Access in Loops
2808
+
2809
+ **Impact:** LOW-MEDIUM
2810
+ **Tags:** javascript, loops, optimization, caching
2811
+
2812
+ ## Cache Property Access in Loops
2813
+
2814
+ Cache object property lookups in hot paths.
2815
+
2816
+ **Incorrect (3 lookups × N iterations):**
2817
+
2818
+ ```typescript
2819
+ for (let i = 0; i < arr.length; i++) {
2820
+ process(obj.config.settings.value)
2821
+ }
2822
+ ```
2823
+
2824
+ **Correct (1 lookup total):**
2825
+
2826
+ ```typescript
2827
+ const value = obj.config.settings.value
2828
+ const len = arr.length
2829
+ for (let i = 0; i < len; i++) {
2830
+ process(value)
2831
+ }
2832
+ ```
2833
+
2834
+ ---
2835
+
2836
+ ## Rule 7.4: Cache Repeated Function Calls
2837
+
2838
+ **Impact:** MEDIUM
2839
+ **Tags:** javascript, cache, memoization, performance
2840
+
2841
+ ## Cache Repeated Function Calls
2842
+
2843
+ Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
2844
+
2845
+ **Incorrect (redundant computation):**
2846
+
2847
+ ```typescript
2848
+ function ProjectList({ projects }: { projects: Project[] }) {
2849
+ return (
2850
+ <div>
2851
+ {projects.map(project => {
2852
+ // slugify() called 100+ times for same project names
2853
+ const slug = slugify(project.name)
2854
+
2855
+ return <ProjectCard key={project.id} slug={slug} />
2856
+ })}
2857
+ </div>
2858
+ )
2859
+ }
2860
+ ```
2861
+
2862
+ **Correct (cached results):**
2863
+
2864
+ ```typescript
2865
+ // Module-level cache
2866
+ const slugifyCache = new Map<string, string>()
2867
+
2868
+ function cachedSlugify(text: string): string {
2869
+ if (slugifyCache.has(text)) {
2870
+ return slugifyCache.get(text)!
2871
+ }
2872
+ const result = slugify(text)
2873
+ slugifyCache.set(text, result)
2874
+ return result
2875
+ }
2876
+
2877
+ function ProjectList({ projects }: { projects: Project[] }) {
2878
+ return (
2879
+ <div>
2880
+ {projects.map(project => {
2881
+ // Computed only once per unique project name
2882
+ const slug = cachedSlugify(project.name)
2883
+
2884
+ return <ProjectCard key={project.id} slug={slug} />
2885
+ })}
2886
+ </div>
2887
+ )
2888
+ }
2889
+ ```
2890
+
2891
+ **Simpler pattern for single-value functions:**
2892
+
2893
+ ```typescript
2894
+ let isLoggedInCache: boolean | null = null
2895
+
2896
+ function isLoggedIn(): boolean {
2897
+ if (isLoggedInCache !== null) {
2898
+ return isLoggedInCache
2899
+ }
2900
+
2901
+ isLoggedInCache = document.cookie.includes('auth=')
2902
+ return isLoggedInCache
2903
+ }
2904
+
2905
+ // Clear cache when auth changes
2906
+ function onAuthChange() {
2907
+ isLoggedInCache = null
2908
+ }
2909
+ ```
2910
+
2911
+ Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
2912
+
2913
+ Reference: [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
2914
+
2915
+ ---
2916
+
2917
+ ## Rule 7.5: Cache Storage API Calls
2918
+
2919
+ **Impact:** LOW-MEDIUM
2920
+ **Tags:** javascript, localStorage, storage, caching, performance
2921
+
2922
+ ## Cache Storage API Calls
2923
+
2924
+ `localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
2925
+
2926
+ **Incorrect (reads storage on every call):**
2927
+
2928
+ ```typescript
2929
+ function getTheme() {
2930
+ return localStorage.getItem('theme') ?? 'light'
2931
+ }
2932
+ // Called 10 times = 10 storage reads
2933
+ ```
2934
+
2935
+ **Correct (Map cache):**
2936
+
2937
+ ```typescript
2938
+ const storageCache = new Map<string, string | null>()
2939
+
2940
+ function getLocalStorage(key: string) {
2941
+ if (!storageCache.has(key)) {
2942
+ storageCache.set(key, localStorage.getItem(key))
2943
+ }
2944
+ return storageCache.get(key)
2945
+ }
2946
+
2947
+ function setLocalStorage(key: string, value: string) {
2948
+ localStorage.setItem(key, value)
2949
+ storageCache.set(key, value) // keep cache in sync
2950
+ }
2951
+ ```
2952
+
2953
+ Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
2954
+
2955
+ **Cookie caching:**
2956
+
2957
+ ```typescript
2958
+ let cookieCache: Record<string, string> | null = null
2959
+
2960
+ function getCookie(name: string) {
2961
+ if (!cookieCache) {
2962
+ cookieCache = Object.fromEntries(
2963
+ document.cookie.split('; ').map(c => c.split('='))
2964
+ )
2965
+ }
2966
+ return cookieCache[name]
2967
+ }
2968
+ ```
2969
+
2970
+ **Important (invalidate on external changes):**
2971
+
2972
+ If storage can change externally (another tab, server-set cookies), invalidate cache:
2973
+
2974
+ ```typescript
2975
+ window.addEventListener('storage', (e) => {
2976
+ if (e.key) storageCache.delete(e.key)
2977
+ })
2978
+
2979
+ document.addEventListener('visibilitychange', () => {
2980
+ if (document.visibilityState === 'visible') {
2981
+ storageCache.clear()
2982
+ }
2983
+ })
2984
+ ```
2985
+
2986
+ ---
2987
+
2988
+ ## Rule 7.6: Combine Multiple Array Iterations
2989
+
2990
+ **Impact:** LOW-MEDIUM
2991
+ **Tags:** javascript, arrays, loops, performance
2992
+
2993
+ ## Combine Multiple Array Iterations
2994
+
2995
+ Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
2996
+
2997
+ **Incorrect (3 iterations):**
2998
+
2999
+ ```typescript
3000
+ const admins = users.filter(u => u.isAdmin)
3001
+ const testers = users.filter(u => u.isTester)
3002
+ const inactive = users.filter(u => !u.isActive)
3003
+ ```
3004
+
3005
+ **Correct (1 iteration):**
3006
+
3007
+ ```typescript
3008
+ const admins: User[] = []
3009
+ const testers: User[] = []
3010
+ const inactive: User[] = []
3011
+
3012
+ for (const user of users) {
3013
+ if (user.isAdmin) admins.push(user)
3014
+ if (user.isTester) testers.push(user)
3015
+ if (!user.isActive) inactive.push(user)
3016
+ }
3017
+ ```
3018
+
3019
+ ---
3020
+
3021
+ ## Rule 7.7: Early Length Check for Array Comparisons
3022
+
3023
+ **Impact:** MEDIUM-HIGH
3024
+ **Tags:** javascript, arrays, performance, optimization, comparison
3025
+
3026
+ ## Early Length Check for Array Comparisons
3027
+
3028
+ When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
3029
+
3030
+ In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
3031
+
3032
+ **Incorrect (always runs expensive comparison):**
3033
+
3034
+ ```typescript
3035
+ function hasChanges(current: string[], original: string[]) {
3036
+ // Always sorts and joins, even when lengths differ
3037
+ return current.sort().join() !== original.sort().join()
3038
+ }
3039
+ ```
3040
+
3041
+ 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.
3042
+
3043
+ **Correct (O(1) length check first):**
3044
+
3045
+ ```typescript
3046
+ function hasChanges(current: string[], original: string[]) {
3047
+ // Early return if lengths differ
3048
+ if (current.length !== original.length) {
3049
+ return true
3050
+ }
3051
+ // Only sort when lengths match
3052
+ const currentSorted = current.toSorted()
3053
+ const originalSorted = original.toSorted()
3054
+ for (let i = 0; i < currentSorted.length; i++) {
3055
+ if (currentSorted[i] !== originalSorted[i]) {
3056
+ return true
3057
+ }
3058
+ }
3059
+ return false
3060
+ }
3061
+ ```
3062
+
3063
+ This new approach is more efficient because:
3064
+ - It avoids the overhead of sorting and joining the arrays when lengths differ
3065
+ - It avoids consuming memory for the joined strings (especially important for large arrays)
3066
+ - It avoids mutating the original arrays
3067
+ - It returns early when a difference is found
3068
+
3069
+ ---
3070
+
3071
+ ## Rule 7.8: Early Return from Functions
3072
+
3073
+ **Impact:** LOW-MEDIUM
3074
+ **Tags:** javascript, functions, optimization, early-return
3075
+
3076
+ ## Early Return from Functions
3077
+
3078
+ Return early when result is determined to skip unnecessary processing.
3079
+
3080
+ **Incorrect (processes all items even after finding answer):**
3081
+
3082
+ ```typescript
3083
+ function validateUsers(users: User[]) {
3084
+ let hasError = false
3085
+ let errorMessage = ''
3086
+
3087
+ for (const user of users) {
3088
+ if (!user.email) {
3089
+ hasError = true
3090
+ errorMessage = 'Email required'
3091
+ }
3092
+ if (!user.name) {
3093
+ hasError = true
3094
+ errorMessage = 'Name required'
3095
+ }
3096
+ // Continues checking all users even after error found
3097
+ }
3098
+
3099
+ return hasError ? { valid: false, error: errorMessage } : { valid: true }
3100
+ }
3101
+ ```
3102
+
3103
+ **Correct (returns immediately on first error):**
3104
+
3105
+ ```typescript
3106
+ function validateUsers(users: User[]) {
3107
+ for (const user of users) {
3108
+ if (!user.email) {
3109
+ return { valid: false, error: 'Email required' }
3110
+ }
3111
+ if (!user.name) {
3112
+ return { valid: false, error: 'Name required' }
3113
+ }
3114
+ }
3115
+
3116
+ return { valid: true }
3117
+ }
3118
+ ```
3119
+
3120
+ ---
3121
+
3122
+ ## Rule 7.9: Hoist RegExp Creation
3123
+
3124
+ **Impact:** LOW-MEDIUM
3125
+ **Tags:** javascript, regexp, optimization, memoization
3126
+
3127
+ ## Hoist RegExp Creation
3128
+
3129
+ Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
3130
+
3131
+ **Incorrect (new RegExp every render):**
3132
+
3133
+ ```tsx
3134
+ function Highlighter({ text, query }: Props) {
3135
+ const regex = new RegExp(`(${query})`, 'gi')
3136
+ const parts = text.split(regex)
3137
+ return <>{parts.map((part, i) => ...)}</>
3138
+ }
3139
+ ```
3140
+
3141
+ **Correct (memoize or hoist):**
3142
+
3143
+ ```tsx
3144
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
3145
+
3146
+ function Highlighter({ text, query }: Props) {
3147
+ const regex = useMemo(
3148
+ () => new RegExp(`(${escapeRegex(query)})`, 'gi'),
3149
+ [query]
3150
+ )
3151
+ const parts = text.split(regex)
3152
+ return <>{parts.map((part, i) => ...)}</>
3153
+ }
3154
+ ```
3155
+
3156
+ **Warning (global regex has mutable state):**
3157
+
3158
+ Global regex (`/g`) has mutable `lastIndex` state:
3159
+
3160
+ ```typescript
3161
+ const regex = /foo/g
3162
+ regex.test('foo') // true, lastIndex = 3
3163
+ regex.test('foo') // false, lastIndex = 0
3164
+ ```
3165
+
3166
+ ---
3167
+
3168
+ ## Rule 7.10: Use Loop for Min/Max Instead of Sort
3169
+
3170
+ **Impact:** LOW
3171
+ **Tags:** javascript, arrays, performance, sorting, algorithms
3172
+
3173
+ ## Use Loop for Min/Max Instead of Sort
3174
+
3175
+ Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
3176
+
3177
+ **Incorrect (O(n log n) - sort to find latest):**
3178
+
3179
+ ```typescript
3180
+ interface Project {
3181
+ id: string
3182
+ name: string
3183
+ updatedAt: number
3184
+ }
3185
+
3186
+ function getLatestProject(projects: Project[]) {
3187
+ const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
3188
+ return sorted[0]
3189
+ }
3190
+ ```
3191
+
3192
+ Sorts the entire array just to find the maximum value.
3193
+
3194
+ **Incorrect (O(n log n) - sort for oldest and newest):**
3195
+
3196
+ ```typescript
3197
+ function getOldestAndNewest(projects: Project[]) {
3198
+ const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
3199
+ return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
3200
+ }
3201
+ ```
3202
+
3203
+ Still sorts unnecessarily when only min/max are needed.
3204
+
3205
+ **Correct (O(n) - single loop):**
3206
+
3207
+ ```typescript
3208
+ function getLatestProject(projects: Project[]) {
3209
+ if (projects.length === 0) return null
3210
+
3211
+ let latest = projects[0]
3212
+
3213
+ for (let i = 1; i < projects.length; i++) {
3214
+ if (projects[i].updatedAt > latest.updatedAt) {
3215
+ latest = projects[i]
3216
+ }
3217
+ }
3218
+
3219
+ return latest
3220
+ }
3221
+
3222
+ function getOldestAndNewest(projects: Project[]) {
3223
+ if (projects.length === 0) return { oldest: null, newest: null }
3224
+
3225
+ let oldest = projects[0]
3226
+ let newest = projects[0]
3227
+
3228
+ for (let i = 1; i < projects.length; i++) {
3229
+ if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
3230
+ if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
3231
+ }
3232
+
3233
+ return { oldest, newest }
3234
+ }
3235
+ ```
3236
+
3237
+ Single pass through the array, no copying, no sorting.
3238
+
3239
+ **Alternative (Math.min/Math.max for small arrays):**
3240
+
3241
+ ```typescript
3242
+ const numbers = [5, 2, 8, 1, 9]
3243
+ const min = Math.min(...numbers)
3244
+ const max = Math.max(...numbers)
3245
+ ```
3246
+
3247
+ 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.
3248
+
3249
+ ---
3250
+
3251
+ ## Rule 7.11: Use Set/Map for O(1) Lookups
3252
+
3253
+ **Impact:** LOW-MEDIUM
3254
+ **Tags:** javascript, set, map, data-structures, performance
3255
+
3256
+ ## Use Set/Map for O(1) Lookups
3257
+
3258
+ Convert arrays to Set/Map for repeated membership checks.
3259
+
3260
+ **Incorrect (O(n) per check):**
3261
+
3262
+ ```typescript
3263
+ const allowedIds = ['a', 'b', 'c', ...]
3264
+ items.filter(item => allowedIds.includes(item.id))
3265
+ ```
3266
+
3267
+ **Correct (O(1) per check):**
3268
+
3269
+ ```typescript
3270
+ const allowedIds = new Set(['a', 'b', 'c', ...])
3271
+ items.filter(item => allowedIds.has(item.id))
3272
+ ```
3273
+
3274
+ ---
3275
+
3276
+ ## Rule 7.12: Use toSorted() Instead of sort() for Immutability
3277
+
3278
+ **Impact:** MEDIUM-HIGH
3279
+ **Tags:** javascript, arrays, immutability, react, state, mutation
3280
+
3281
+ ## Use toSorted() Instead of sort() for Immutability
3282
+
3283
+ `.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.
3284
+
3285
+ **Incorrect (mutates original array):**
3286
+
3287
+ ```typescript
3288
+ function UserList({ users }: { users: User[] }) {
3289
+ // Mutates the users prop array!
3290
+ const sorted = useMemo(
3291
+ () => users.sort((a, b) => a.name.localeCompare(b.name)),
3292
+ [users]
3293
+ )
3294
+ return <div>{sorted.map(renderUser)}</div>
3295
+ }
3296
+ ```
3297
+
3298
+ **Correct (creates new array):**
3299
+
3300
+ ```typescript
3301
+ function UserList({ users }: { users: User[] }) {
3302
+ // Creates new sorted array, original unchanged
3303
+ const sorted = useMemo(
3304
+ () => users.toSorted((a, b) => a.name.localeCompare(b.name)),
3305
+ [users]
3306
+ )
3307
+ return <div>{sorted.map(renderUser)}</div>
3308
+ }
3309
+ ```
3310
+
3311
+ **Why this matters in React:**
3312
+
3313
+ 1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
3314
+ 2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
3315
+
3316
+ **Browser support (fallback for older browsers):**
3317
+
3318
+ `.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
3319
+
3320
+ ```typescript
3321
+ // Fallback for older browsers
3322
+ const sorted = [...items].sort((a, b) => a.value - b.value)
3323
+ ```
3324
+
3325
+ **Other immutable array methods:**
3326
+
3327
+ - `.toSorted()` - immutable sort
3328
+ - `.toReversed()` - immutable reverse
3329
+ - `.toSpliced()` - immutable splice
3330
+ - `.with()` - immutable element replacement
3331
+
3332
+
3333
+
3334
+
3335
+ ---
3336
+ # Content from 8-advanced-advanced-patterns.md
3337
+
3338
+ # 8. Advanced Patterns
3339
+
3340
+ > **Impact:** VARIABLE
3341
+ > **Focus:** Advanced patterns for specific cases that require careful implementation.
3342
+
3343
+ ---
3344
+
3345
+ ## Overview
3346
+
3347
+ This section contains **3 rules** focused on advanced patterns.
3348
+
3349
+ ---
3350
+
3351
+ ## Rule 8.1: Initialize App Once, Not Per Mount
3352
+
3353
+ **Impact:** LOW-MEDIUM
3354
+ **Tags:** initialization, useEffect, app-startup, side-effects
3355
+
3356
+ ## Initialize App Once, Not Per Mount
3357
+
3358
+ 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.
3359
+
3360
+ **Incorrect (runs twice in dev, re-runs on remount):**
3361
+
3362
+ ```tsx
3363
+ function Comp() {
3364
+ useEffect(() => {
3365
+ loadFromStorage()
3366
+ checkAuthToken()
3367
+ }, [])
3368
+
3369
+ // ...
3370
+ }
3371
+ ```
3372
+
3373
+ **Correct (once per app load):**
3374
+
3375
+ ```tsx
3376
+ let didInit = false
3377
+
3378
+ function Comp() {
3379
+ useEffect(() => {
3380
+ if (didInit) return
3381
+ didInit = true
3382
+ loadFromStorage()
3383
+ checkAuthToken()
3384
+ }, [])
3385
+
3386
+ // ...
3387
+ }
3388
+ ```
3389
+
3390
+ Reference: [Initializing the application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)
3391
+
3392
+ ---
3393
+
3394
+ ## Rule 8.2: Store Event Handlers in Refs
3395
+
3396
+ **Impact:** LOW
3397
+ **Tags:** advanced, hooks, refs, event-handlers, optimization
3398
+
3399
+ ## Store Event Handlers in Refs
3400
+
3401
+ Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
3402
+
3403
+ **Incorrect (re-subscribes on every render):**
3404
+
3405
+ ```tsx
3406
+ function useWindowEvent(event: string, handler: (e) => void) {
3407
+ useEffect(() => {
3408
+ window.addEventListener(event, handler)
3409
+ return () => window.removeEventListener(event, handler)
3410
+ }, [event, handler])
3411
+ }
3412
+ ```
3413
+
3414
+ **Correct (stable subscription):**
3415
+
3416
+ ```tsx
3417
+ function useWindowEvent(event: string, handler: (e) => void) {
3418
+ const handlerRef = useRef(handler)
3419
+ useEffect(() => {
3420
+ handlerRef.current = handler
3421
+ }, [handler])
3422
+
3423
+ useEffect(() => {
3424
+ const listener = (e) => handlerRef.current(e)
3425
+ window.addEventListener(event, listener)
3426
+ return () => window.removeEventListener(event, listener)
3427
+ }, [event])
3428
+ }
3429
+ ```
3430
+
3431
+ **Alternative: use `useEffectEvent` if you're on latest React:**
3432
+
3433
+ ```tsx
3434
+ import { useEffectEvent } from 'react'
3435
+
3436
+ function useWindowEvent(event: string, handler: (e) => void) {
3437
+ const onEvent = useEffectEvent(handler)
3438
+
3439
+ useEffect(() => {
3440
+ window.addEventListener(event, onEvent)
3441
+ return () => window.removeEventListener(event, onEvent)
3442
+ }, [event])
3443
+ }
3444
+ ```
3445
+
3446
+ `useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
3447
+
3448
+ ---
3449
+
3450
+ ## Rule 8.3: useEffectEvent for Stable Callback Refs
3451
+
3452
+ **Impact:** LOW
3453
+ **Tags:** advanced, hooks, useEffectEvent, refs, optimization
3454
+
3455
+ ## useEffectEvent for Stable Callback Refs
3456
+
3457
+ Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
3458
+
3459
+ **Incorrect (effect re-runs on every callback change):**
3460
+
3461
+ ```tsx
3462
+ function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
3463
+ const [query, setQuery] = useState('')
3464
+
3465
+ useEffect(() => {
3466
+ const timeout = setTimeout(() => onSearch(query), 300)
3467
+ return () => clearTimeout(timeout)
3468
+ }, [query, onSearch])
3469
+ }
3470
+ ```
3471
+
3472
+ **Correct (using React's useEffectEvent):**
3473
+
3474
+ ```tsx
3475
+ import { useEffectEvent } from 'react';
3476
+
3477
+ function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
3478
+ const [query, setQuery] = useState('')
3479
+ const onSearchEvent = useEffectEvent(onSearch)
3480
+
3481
+ useEffect(() => {
3482
+ const timeout = setTimeout(() => onSearchEvent(query), 300)
3483
+ return () => clearTimeout(timeout)
3484
+ }, [query])
3485
+ }
3486
+ ```
3487
+