@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.
- package/.claude-skill.json +19 -0
- package/AGENTS.md +2249 -0
- package/README.md +123 -0
- package/SKILL.md +121 -0
- package/install-skill.js +207 -0
- package/package.json +37 -0
- package/rules/_sections.md +46 -0
- package/rules/_template.md +28 -0
- package/rules/advanced-event-handler-refs.md +55 -0
- package/rules/advanced-use-latest.md +49 -0
- package/rules/async-api-routes.md +38 -0
- package/rules/async-defer-await.md +80 -0
- package/rules/async-dependencies.md +36 -0
- package/rules/async-parallel.md +28 -0
- package/rules/async-suspense-boundaries.md +99 -0
- package/rules/bundle-barrel-imports.md +59 -0
- package/rules/bundle-conditional.md +31 -0
- package/rules/bundle-defer-third-party.md +49 -0
- package/rules/bundle-dynamic-imports.md +35 -0
- package/rules/bundle-preload.md +50 -0
- package/rules/client-event-listeners.md +74 -0
- package/rules/client-swr-dedup.md +56 -0
- package/rules/js-batch-dom-css.md +82 -0
- package/rules/js-cache-function-results.md +80 -0
- package/rules/js-cache-property-access.md +28 -0
- package/rules/js-cache-storage.md +70 -0
- package/rules/js-combine-iterations.md +32 -0
- package/rules/js-early-exit.md +50 -0
- package/rules/js-hoist-regexp.md +45 -0
- package/rules/js-index-maps.md +37 -0
- package/rules/js-length-check-first.md +49 -0
- package/rules/js-min-max-loop.md +82 -0
- package/rules/js-set-map-lookups.md +24 -0
- package/rules/js-tosorted-immutable.md +57 -0
- package/rules/rendering-activity.md +26 -0
- package/rules/rendering-animate-svg-wrapper.md +47 -0
- package/rules/rendering-conditional-render.md +40 -0
- package/rules/rendering-content-visibility.md +38 -0
- package/rules/rendering-hoist-jsx.md +46 -0
- package/rules/rendering-hydration-no-flicker.md +82 -0
- package/rules/rendering-svg-precision.md +28 -0
- package/rules/rerender-defer-reads.md +39 -0
- package/rules/rerender-dependencies.md +45 -0
- package/rules/rerender-derived-state.md +29 -0
- package/rules/rerender-functional-setstate.md +74 -0
- package/rules/rerender-lazy-state-init.md +58 -0
- package/rules/rerender-memo.md +44 -0
- package/rules/rerender-transitions.md +40 -0
- package/rules/server-after-nonblocking.md +73 -0
- package/rules/server-cache-lru.md +41 -0
- package/rules/server-cache-react.md +26 -0
- package/rules/server-parallel-fetching.md +79 -0
- package/rules/server-serialization.md +38 -0
- package/uninstall-skill.js +118 -0
- 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
|
+
};
|