@adonis0123/react-best-practices 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 (55) hide show
  1. package/.claude-skill.json +19 -0
  2. package/AGENTS.md +2249 -0
  3. package/README.md +123 -0
  4. package/SKILL.md +121 -0
  5. package/install-skill.js +207 -0
  6. package/package.json +37 -0
  7. package/rules/_sections.md +46 -0
  8. package/rules/_template.md +28 -0
  9. package/rules/advanced-event-handler-refs.md +55 -0
  10. package/rules/advanced-use-latest.md +49 -0
  11. package/rules/async-api-routes.md +38 -0
  12. package/rules/async-defer-await.md +80 -0
  13. package/rules/async-dependencies.md +36 -0
  14. package/rules/async-parallel.md +28 -0
  15. package/rules/async-suspense-boundaries.md +99 -0
  16. package/rules/bundle-barrel-imports.md +59 -0
  17. package/rules/bundle-conditional.md +31 -0
  18. package/rules/bundle-defer-third-party.md +49 -0
  19. package/rules/bundle-dynamic-imports.md +35 -0
  20. package/rules/bundle-preload.md +50 -0
  21. package/rules/client-event-listeners.md +74 -0
  22. package/rules/client-swr-dedup.md +56 -0
  23. package/rules/js-batch-dom-css.md +82 -0
  24. package/rules/js-cache-function-results.md +80 -0
  25. package/rules/js-cache-property-access.md +28 -0
  26. package/rules/js-cache-storage.md +70 -0
  27. package/rules/js-combine-iterations.md +32 -0
  28. package/rules/js-early-exit.md +50 -0
  29. package/rules/js-hoist-regexp.md +45 -0
  30. package/rules/js-index-maps.md +37 -0
  31. package/rules/js-length-check-first.md +49 -0
  32. package/rules/js-min-max-loop.md +82 -0
  33. package/rules/js-set-map-lookups.md +24 -0
  34. package/rules/js-tosorted-immutable.md +57 -0
  35. package/rules/rendering-activity.md +26 -0
  36. package/rules/rendering-animate-svg-wrapper.md +47 -0
  37. package/rules/rendering-conditional-render.md +40 -0
  38. package/rules/rendering-content-visibility.md +38 -0
  39. package/rules/rendering-hoist-jsx.md +46 -0
  40. package/rules/rendering-hydration-no-flicker.md +82 -0
  41. package/rules/rendering-svg-precision.md +28 -0
  42. package/rules/rerender-defer-reads.md +39 -0
  43. package/rules/rerender-dependencies.md +45 -0
  44. package/rules/rerender-derived-state.md +29 -0
  45. package/rules/rerender-functional-setstate.md +74 -0
  46. package/rules/rerender-lazy-state-init.md +58 -0
  47. package/rules/rerender-memo.md +44 -0
  48. package/rules/rerender-transitions.md +40 -0
  49. package/rules/server-after-nonblocking.md +73 -0
  50. package/rules/server-cache-lru.md +41 -0
  51. package/rules/server-cache-react.md +26 -0
  52. package/rules/server-parallel-fetching.md +79 -0
  53. package/rules/server-serialization.md +38 -0
  54. package/uninstall-skill.js +118 -0
  55. package/utils.js +94 -0
@@ -0,0 +1,41 @@
1
+ ---
2
+ title: Cross-Request LRU Caching
3
+ impact: HIGH
4
+ impactDescription: caches across requests
5
+ tags: server, cache, lru, cross-request
6
+ ---
7
+
8
+ ## Cross-Request LRU Caching
9
+
10
+ `React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
11
+
12
+ **Implementation:**
13
+
14
+ ```typescript
15
+ import { LRUCache } from 'lru-cache'
16
+
17
+ const cache = new LRUCache<string, any>({
18
+ max: 1000,
19
+ ttl: 5 * 60 * 1000 // 5 minutes
20
+ })
21
+
22
+ export async function getUser(id: string) {
23
+ const cached = cache.get(id)
24
+ if (cached) return cached
25
+
26
+ const user = await db.user.findUnique({ where: { id } })
27
+ cache.set(id, user)
28
+ return user
29
+ }
30
+
31
+ // Request 1: DB query, result cached
32
+ // Request 2: cache hit, no DB query
33
+ ```
34
+
35
+ Use when sequential user actions hit multiple endpoints needing the same data within seconds.
36
+
37
+ **With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
38
+
39
+ **In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
40
+
41
+ Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
@@ -0,0 +1,26 @@
1
+ ---
2
+ title: Per-Request Deduplication with React.cache()
3
+ impact: MEDIUM
4
+ impactDescription: deduplicates within request
5
+ tags: server, cache, react-cache, deduplication
6
+ ---
7
+
8
+ ## Per-Request Deduplication with React.cache()
9
+
10
+ Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
11
+
12
+ **Usage:**
13
+
14
+ ```typescript
15
+ import { cache } from 'react'
16
+
17
+ export const getCurrentUser = cache(async () => {
18
+ const session = await auth()
19
+ if (!session?.user?.id) return null
20
+ return await db.user.findUnique({
21
+ where: { id: session.user.id }
22
+ })
23
+ })
24
+ ```
25
+
26
+ Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
@@ -0,0 +1,79 @@
1
+ ---
2
+ title: Parallel Data Fetching with Component Composition
3
+ impact: CRITICAL
4
+ impactDescription: eliminates server-side waterfalls
5
+ tags: server, rsc, parallel-fetching, composition
6
+ ---
7
+
8
+ ## Parallel Data Fetching with Component Composition
9
+
10
+ React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
11
+
12
+ **Incorrect (Sidebar waits for Page's fetch to complete):**
13
+
14
+ ```tsx
15
+ export default async function Page() {
16
+ const header = await fetchHeader()
17
+ return (
18
+ <div>
19
+ <div>{header}</div>
20
+ <Sidebar />
21
+ </div>
22
+ )
23
+ }
24
+
25
+ async function Sidebar() {
26
+ const items = await fetchSidebarItems()
27
+ return <nav>{items.map(renderItem)}</nav>
28
+ }
29
+ ```
30
+
31
+ **Correct (both fetch simultaneously):**
32
+
33
+ ```tsx
34
+ async function Header() {
35
+ const data = await fetchHeader()
36
+ return <div>{data}</div>
37
+ }
38
+
39
+ async function Sidebar() {
40
+ const items = await fetchSidebarItems()
41
+ return <nav>{items.map(renderItem)}</nav>
42
+ }
43
+
44
+ export default function Page() {
45
+ return (
46
+ <div>
47
+ <Header />
48
+ <Sidebar />
49
+ </div>
50
+ )
51
+ }
52
+ ```
53
+
54
+ **Alternative with children prop:**
55
+
56
+ ```tsx
57
+ async function Layout({ children }: { children: ReactNode }) {
58
+ const header = await fetchHeader()
59
+ return (
60
+ <div>
61
+ <div>{header}</div>
62
+ {children}
63
+ </div>
64
+ )
65
+ }
66
+
67
+ async function Sidebar() {
68
+ const items = await fetchSidebarItems()
69
+ return <nav>{items.map(renderItem)}</nav>
70
+ }
71
+
72
+ export default function Page() {
73
+ return (
74
+ <Layout>
75
+ <Sidebar />
76
+ </Layout>
77
+ )
78
+ }
79
+ ```
@@ -0,0 +1,38 @@
1
+ ---
2
+ title: Minimize Serialization at RSC Boundaries
3
+ impact: HIGH
4
+ impactDescription: reduces data transfer size
5
+ tags: server, rsc, serialization, props
6
+ ---
7
+
8
+ ## Minimize Serialization at RSC Boundaries
9
+
10
+ The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.
11
+
12
+ **Incorrect (serializes all 50 fields):**
13
+
14
+ ```tsx
15
+ async function Page() {
16
+ const user = await fetchUser() // 50 fields
17
+ return <Profile user={user} />
18
+ }
19
+
20
+ 'use client'
21
+ function Profile({ user }: { user: User }) {
22
+ return <div>{user.name}</div> // uses 1 field
23
+ }
24
+ ```
25
+
26
+ **Correct (serializes only 1 field):**
27
+
28
+ ```tsx
29
+ async function Page() {
30
+ const user = await fetchUser()
31
+ return <Profile name={user.name} />
32
+ }
33
+
34
+ 'use client'
35
+ function Profile({ name }: { name: string }) {
36
+ return <div>{name}</div>
37
+ }
38
+ ```
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const { getEnabledTargets, extractSkillName, detectInstallLocation } = require('./utils');
8
+
9
+ function uninstallFromTarget(target, config) {
10
+ console.log(`\n🗑️ Uninstalling from ${target.name}...`);
11
+
12
+ const isGlobal = process.env.npm_config_global === 'true';
13
+ const location = detectInstallLocation(target.paths, isGlobal);
14
+
15
+ // Extract skill name from package name (remove scope prefix)
16
+ const skillName = extractSkillName(config.name);
17
+
18
+ // Path format using skill name
19
+ const skillNameTargetDir = path.join(location.base, skillName);
20
+
21
+ // Path format with full package name (including scope)
22
+ const fullPackageNameTargetDir = path.join(location.base, config.name);
23
+
24
+ let removed = false;
25
+
26
+ // Check and remove path using skill name
27
+ if (fs.existsSync(skillNameTargetDir)) {
28
+ fs.rmSync(skillNameTargetDir, { recursive: true, force: true });
29
+ console.log(` ✓ Removed skill directory: ${skillName}`);
30
+ removed = true;
31
+ }
32
+
33
+ // Check and remove path with full package name (for compatibility)
34
+ if (fs.existsSync(fullPackageNameTargetDir) && fullPackageNameTargetDir !== skillNameTargetDir) {
35
+ fs.rmSync(fullPackageNameTargetDir, { recursive: true, force: true });
36
+ console.log(` ✓ Removed skill directory: ${config.name}`);
37
+ removed = true;
38
+ }
39
+
40
+ // Update manifest
41
+ const manifestPath = path.join(location.base, '.skills-manifest.json');
42
+ if (fs.existsSync(manifestPath)) {
43
+ try {
44
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
45
+ if (manifest.skills && manifest.skills[config.name]) {
46
+ delete manifest.skills[config.name];
47
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
48
+ console.log(` ✓ Updated manifest`);
49
+ }
50
+ } catch (error) {
51
+ console.warn(' Warning: Could not update manifest:', error.message);
52
+ }
53
+ }
54
+
55
+ if (removed) {
56
+ console.log(` ✅ Uninstalled from ${target.name}`);
57
+ return true;
58
+ } else {
59
+ console.log(` ℹ️ Skill was not installed in ${target.name}`);
60
+ return false;
61
+ }
62
+ }
63
+
64
+ function uninstallSkill() {
65
+ console.log('🗑️ Uninstalling AI Coding Skill...\n');
66
+
67
+ // Read configuration
68
+ const configPath = path.join(__dirname, '.claude-skill.json');
69
+ if (!fs.existsSync(configPath)) {
70
+ console.warn('Warning: .claude-skill.json not found, skipping cleanup');
71
+ return;
72
+ }
73
+
74
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
75
+
76
+ // Get enabled targets
77
+ const enabledTargets = getEnabledTargets(config);
78
+
79
+ console.log(`Uninstalling skill "${config.name}" from ${enabledTargets.length} target(s):`);
80
+ enabledTargets.forEach(target => {
81
+ console.log(` • ${target.name}`);
82
+ });
83
+
84
+ // Uninstall from all enabled targets
85
+ const uninstalledFrom = [];
86
+ for (const target of enabledTargets) {
87
+ try {
88
+ const success = uninstallFromTarget(target, config);
89
+ if (success) {
90
+ uninstalledFrom.push(target.name);
91
+ }
92
+ } catch (error) {
93
+ console.error(`\n❌ Failed to uninstall from ${target.name}:`, error.message);
94
+ }
95
+ }
96
+
97
+ // Summary
98
+ console.log('\n' + '='.repeat(60));
99
+ if (uninstalledFrom.length > 0) {
100
+ console.log('✅ Uninstallation Complete!');
101
+ console.log('='.repeat(60));
102
+ console.log('\nUninstalled from:');
103
+ uninstalledFrom.forEach(target => {
104
+ console.log(` • ${target}`);
105
+ });
106
+ } else {
107
+ console.log('ℹ️ Skill was not installed');
108
+ console.log('='.repeat(60));
109
+ }
110
+ }
111
+
112
+ // Execute uninstall
113
+ try {
114
+ uninstallSkill();
115
+ } catch (error) {
116
+ console.error('\n⚠️ Warning during uninstall:', error.message);
117
+ // Don't exit with error code as uninstall should be best-effort
118
+ }
package/utils.js ADDED
@@ -0,0 +1,94 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const CWD = process.env.INIT_CWD || process.cwd();
6
+
7
+ /**
8
+ * Get enabled target configurations
9
+ */
10
+ function getEnabledTargets(config) {
11
+ // If no targets configuration, use default Claude Code configuration
12
+ if (!config.targets) {
13
+ return [{
14
+ name: 'claude-code',
15
+ paths: {
16
+ global: '.claude/skills',
17
+ project: '.claude/skills'
18
+ }
19
+ }];
20
+ }
21
+
22
+ // Return all enabled targets
23
+ return Object.entries(config.targets)
24
+ .filter(([_, target]) => target.enabled)
25
+ .map(([name, target]) => ({
26
+ name,
27
+ paths: target.paths
28
+ }));
29
+ }
30
+
31
+ /**
32
+ * Extract skill name from package name (remove scope prefix)
33
+ */
34
+ function extractSkillName(packageName) {
35
+ return packageName.startsWith('@') ?
36
+ packageName.split('/')[1] || packageName :
37
+ packageName;
38
+ }
39
+
40
+ /**
41
+ * Detect installation location
42
+ */
43
+ function detectInstallLocation(targetPaths, isGlobal) {
44
+ if (isGlobal) {
45
+ // Global installation: install to user home directory
46
+ return {
47
+ type: 'personal',
48
+ base: path.join(os.homedir(), targetPaths.global)
49
+ };
50
+ } else {
51
+ // Project-level installation: find the actual project root directory
52
+ let projectRoot = CWD;
53
+
54
+ // Search upward, skip node_modules directories, find the actual project root
55
+ while (projectRoot !== path.dirname(projectRoot)) {
56
+ // Check if this is a project root directory (contains package.json or .git)
57
+ const hasPackageJson = fs.existsSync(path.join(projectRoot, 'package.json'));
58
+ const hasGit = fs.existsSync(path.join(projectRoot, '.git'));
59
+
60
+ // Check if current directory is in node_modules
61
+ const isInNodeModules = projectRoot.includes('/node_modules/') ||
62
+ path.basename(projectRoot) === 'node_modules';
63
+
64
+ if ((hasPackageJson || hasGit) && !isInNodeModules) {
65
+ // Found the actual project root directory
66
+ break;
67
+ }
68
+
69
+ // Continue searching upward
70
+ projectRoot = path.dirname(projectRoot);
71
+ }
72
+
73
+ // Verify the final path is reasonable
74
+ const finalIsInNodeModules = projectRoot.includes('/node_modules/') ||
75
+ path.basename(projectRoot) === 'node_modules';
76
+
77
+ if (finalIsInNodeModules) {
78
+ // If suitable project root not found, use current working directory (with warning)
79
+ console.warn('⚠ Warning: Could not find project root directory, using current directory');
80
+ projectRoot = CWD;
81
+ }
82
+
83
+ return {
84
+ type: 'project',
85
+ base: path.join(projectRoot, targetPaths.project)
86
+ };
87
+ }
88
+ }
89
+
90
+ module.exports = {
91
+ getEnabledTargets,
92
+ extractSkillName,
93
+ detectInstallLocation
94
+ };