@brookmind/ai-toolkit 1.1.7 → 1.2.1
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/LICENSE +21 -0
- package/README.md +42 -14
- package/dist/__tests__/constants.test.d.ts +2 -0
- package/dist/__tests__/constants.test.d.ts.map +1 -0
- package/dist/__tests__/constants.test.js +102 -0
- package/dist/__tests__/constants.test.js.map +1 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.js +114 -0
- package/dist/__tests__/index.test.js.map +1 -0
- package/dist/__tests__/integration/installer.test.d.ts +2 -0
- package/dist/__tests__/integration/installer.test.d.ts.map +1 -0
- package/dist/__tests__/integration/installer.test.js +425 -0
- package/dist/__tests__/integration/installer.test.js.map +1 -0
- package/dist/__tests__/services/installers.test.d.ts +2 -0
- package/dist/__tests__/services/installers.test.d.ts.map +1 -0
- package/dist/__tests__/services/installers.test.js +222 -0
- package/dist/__tests__/services/installers.test.js.map +1 -0
- package/dist/__tests__/services/opencode.test.d.ts +2 -0
- package/dist/__tests__/services/opencode.test.d.ts.map +1 -0
- package/dist/__tests__/services/opencode.test.js +120 -0
- package/dist/__tests__/services/opencode.test.js.map +1 -0
- package/dist/__tests__/ui/categorize.test.d.ts +2 -0
- package/dist/__tests__/ui/categorize.test.d.ts.map +1 -0
- package/dist/__tests__/ui/categorize.test.js +194 -0
- package/dist/__tests__/ui/categorize.test.js.map +1 -0
- package/dist/__tests__/ui/choices.test.d.ts +2 -0
- package/dist/__tests__/ui/choices.test.d.ts.map +1 -0
- package/dist/__tests__/ui/choices.test.js +180 -0
- package/dist/__tests__/ui/choices.test.js.map +1 -0
- package/dist/__tests__/ui/display.test.d.ts +2 -0
- package/dist/__tests__/ui/display.test.d.ts.map +1 -0
- package/dist/__tests__/ui/display.test.js +142 -0
- package/dist/__tests__/ui/display.test.js.map +1 -0
- package/dist/__tests__/utils/fs.test.d.ts +2 -0
- package/dist/__tests__/utils/fs.test.d.ts.map +1 -0
- package/dist/__tests__/utils/fs.test.js +142 -0
- package/dist/__tests__/utils/fs.test.js.map +1 -0
- package/dist/__tests__/utils/terminal.test.d.ts +2 -0
- package/dist/__tests__/utils/terminal.test.d.ts.map +1 -0
- package/dist/__tests__/utils/terminal.test.js +97 -0
- package/dist/__tests__/utils/terminal.test.js.map +1 -0
- package/dist/constants.d.ts +11 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +40 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +49 -332
- package/dist/index.js.map +1 -1
- package/dist/services/installers.d.ts +8 -0
- package/dist/services/installers.d.ts.map +1 -0
- package/dist/services/installers.js +79 -0
- package/dist/services/installers.js.map +1 -0
- package/dist/services/opencode.d.ts +3 -0
- package/dist/services/opencode.d.ts.map +1 -0
- package/dist/services/opencode.js +33 -0
- package/dist/services/opencode.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/categorize.d.ts +6 -0
- package/dist/ui/categorize.d.ts.map +1 -0
- package/dist/ui/categorize.js +69 -0
- package/dist/ui/categorize.js.map +1 -0
- package/dist/ui/choices.d.ts +6 -0
- package/dist/ui/choices.d.ts.map +1 -0
- package/dist/ui/choices.js +70 -0
- package/dist/ui/choices.js.map +1 -0
- package/dist/ui/display.d.ts +8 -0
- package/dist/ui/display.d.ts.map +1 -0
- package/dist/ui/display.js +86 -0
- package/dist/ui/display.js.map +1 -0
- package/dist/utils/fs.d.ts +5 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +40 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/terminal.d.ts +5 -0
- package/dist/utils/terminal.d.ts.map +1 -0
- package/dist/utils/terminal.js +18 -0
- package/dist/utils/terminal.js.map +1 -0
- package/package.json +29 -5
- package/agents/code-reviewer.md +0 -35
- package/agents/code-simplifier.md +0 -52
- package/commands/create-pr-description.md +0 -102
- package/commands/create-pr.md +0 -76
- package/commands/create-react-tests.md +0 -207
- package/mcps/context7/.mcp.json +0 -13
- package/mcps/expo-mcp/.mcp.json +0 -13
- package/mcps/figma-mcp/.mcp.json +0 -10
- package/skills/github-cli/SKILL.md +0 -125
- package/skills/pdf-processing-pro/FORMS.md +0 -610
- package/skills/pdf-processing-pro/OCR.md +0 -137
- package/skills/pdf-processing-pro/SKILL.md +0 -296
- package/skills/pdf-processing-pro/TABLES.md +0 -626
- package/skills/pdf-processing-pro/scripts/analyze_form.py +0 -307
- package/skills/react-best-practices/AGENTS.md +0 -915
- package/skills/react-best-practices/README.md +0 -127
- package/skills/react-best-practices/SKILL.md +0 -110
- package/skills/react-best-practices/metadata.json +0 -14
- package/skills/react-best-practices/rules/_sections.md +0 -41
- package/skills/react-best-practices/rules/_template.md +0 -28
- package/skills/react-best-practices/rules/advanced-event-handler-refs.md +0 -80
- package/skills/react-best-practices/rules/advanced-use-latest.md +0 -76
- package/skills/react-best-practices/rules/async-defer-await.md +0 -80
- package/skills/react-best-practices/rules/async-dependencies.md +0 -36
- package/skills/react-best-practices/rules/async-parallel.md +0 -28
- package/skills/react-best-practices/rules/async-suspense-boundaries.md +0 -100
- package/skills/react-best-practices/rules/bundle-barrel-imports.md +0 -42
- package/skills/react-best-practices/rules/bundle-conditional.md +0 -106
- package/skills/react-best-practices/rules/bundle-preload.md +0 -44
- package/skills/react-best-practices/rules/client-event-listeners.md +0 -131
- package/skills/react-best-practices/rules/client-swr-dedup.md +0 -133
- package/skills/react-best-practices/rules/js-batch-dom-css.md +0 -82
- package/skills/react-best-practices/rules/js-cache-function-results.md +0 -80
- package/skills/react-best-practices/rules/js-cache-property-access.md +0 -28
- package/skills/react-best-practices/rules/js-cache-storage.md +0 -70
- package/skills/react-best-practices/rules/js-combine-iterations.md +0 -32
- package/skills/react-best-practices/rules/js-early-exit.md +0 -50
- package/skills/react-best-practices/rules/js-hoist-regexp.md +0 -45
- package/skills/react-best-practices/rules/js-index-maps.md +0 -37
- package/skills/react-best-practices/rules/js-length-check-first.md +0 -49
- package/skills/react-best-practices/rules/js-min-max-loop.md +0 -82
- package/skills/react-best-practices/rules/js-set-map-lookups.md +0 -24
- package/skills/react-best-practices/rules/js-tosorted-immutable.md +0 -57
- package/skills/react-best-practices/rules/rendering-activity.md +0 -90
- package/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
- package/skills/react-best-practices/rules/rendering-conditional-render.md +0 -40
- package/skills/react-best-practices/rules/rendering-content-visibility.md +0 -38
- package/skills/react-best-practices/rules/rendering-hoist-jsx.md +0 -65
- package/skills/react-best-practices/rules/rendering-svg-precision.md +0 -28
- package/skills/react-best-practices/rules/rerender-defer-reads.md +0 -39
- package/skills/react-best-practices/rules/rerender-dependencies.md +0 -45
- package/skills/react-best-practices/rules/rerender-derived-state.md +0 -29
- package/skills/react-best-practices/rules/rerender-functional-setstate.md +0 -74
- package/skills/react-best-practices/rules/rerender-lazy-state-init.md +0 -58
- package/skills/react-best-practices/rules/rerender-memo.md +0 -85
- package/skills/react-best-practices/rules/rerender-transitions.md +0 -40
- package/skills/skill-creator/LICENSE.txt +0 -202
- package/skills/skill-creator/SKILL.md +0 -209
- package/skills/skill-creator/scripts/init_skill.py +0 -303
- package/skills/skill-creator/scripts/package_skill.py +0 -110
- package/skills/skill-creator/scripts/quick_validate.py +0 -65
- package/skills/spring-boot-development/EXAMPLES.md +0 -2346
- package/skills/spring-boot-development/README.md +0 -595
- package/skills/spring-boot-development/SKILL.md +0 -1519
- package/themes/README.md +0 -68
- package/themes/claude-vivid.json +0 -72
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Strategic Suspense Boundaries
|
|
3
|
-
impact: HIGH
|
|
4
|
-
impactDescription: faster initial paint
|
|
5
|
-
tags: async, suspense, layout-shift, react-19
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Strategic Suspense Boundaries
|
|
9
|
-
|
|
10
|
-
Use Suspense boundaries to show wrapper UI immediately while data loads asynchronously. This improves perceived performance by not blocking the entire UI.
|
|
11
|
-
|
|
12
|
-
**Incorrect (entire component waits for data):**
|
|
13
|
-
|
|
14
|
-
```tsx
|
|
15
|
-
function Page() {
|
|
16
|
-
const { data, isLoading } = useQuery(["data"], fetchData);
|
|
17
|
-
|
|
18
|
-
if (isLoading) return <Skeleton />;
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<div>
|
|
22
|
-
<div>Sidebar</div>
|
|
23
|
-
<div>Header</div>
|
|
24
|
-
<div>
|
|
25
|
-
<DataDisplay data={data} />
|
|
26
|
-
</div>
|
|
27
|
-
<div>Footer</div>
|
|
28
|
-
</div>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
The entire layout waits for data even though only the middle section needs it.
|
|
34
|
-
|
|
35
|
-
**Correct (wrapper shows immediately, data section has its own loading state):**
|
|
36
|
-
|
|
37
|
-
```tsx
|
|
38
|
-
function Page() {
|
|
39
|
-
return (
|
|
40
|
-
<div>
|
|
41
|
-
<div>Sidebar</div>
|
|
42
|
-
<div>Header</div>
|
|
43
|
-
<div>
|
|
44
|
-
<Suspense fallback={<Skeleton />}>
|
|
45
|
-
<DataDisplay />
|
|
46
|
-
</Suspense>
|
|
47
|
-
</div>
|
|
48
|
-
<div>Footer</div>
|
|
49
|
-
</div>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function DataDisplay() {
|
|
54
|
-
const { data } = useSuspenseQuery(["data"], fetchData);
|
|
55
|
-
return <div>{data.content}</div>;
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Sidebar, Header, and Footer render immediately. Only DataDisplay shows a skeleton while loading.
|
|
60
|
-
|
|
61
|
-
**Alternative with React 19 use() hook (share promise across components):**
|
|
62
|
-
|
|
63
|
-
```tsx
|
|
64
|
-
function Page() {
|
|
65
|
-
// Start fetch immediately, but don't await
|
|
66
|
-
const dataPromise = useMemo(() => fetchData(), []);
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<div>
|
|
70
|
-
<div>Sidebar</div>
|
|
71
|
-
<div>Header</div>
|
|
72
|
-
<Suspense fallback={<Skeleton />}>
|
|
73
|
-
<DataDisplay dataPromise={dataPromise} />
|
|
74
|
-
<DataSummary dataPromise={dataPromise} />
|
|
75
|
-
</Suspense>
|
|
76
|
-
<div>Footer</div>
|
|
77
|
-
</div>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
|
|
82
|
-
const data = use(dataPromise); // React 19: unwraps the promise
|
|
83
|
-
return <div>{data.content}</div>;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
|
|
87
|
-
const data = use(dataPromise); // Reuses the same promise
|
|
88
|
-
return <div>{data.summary}</div>;
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
|
|
93
|
-
|
|
94
|
-
**When NOT to use this pattern:**
|
|
95
|
-
|
|
96
|
-
- Critical data needed for layout decisions (affects positioning)
|
|
97
|
-
- Small, fast queries where suspense overhead isn't worth it
|
|
98
|
-
- When you want to avoid layout shift (loading → content jump)
|
|
99
|
-
|
|
100
|
-
**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Avoid Barrel File Imports
|
|
3
|
-
impact: CRITICAL
|
|
4
|
-
impactDescription: 200-800ms import cost, slow builds
|
|
5
|
-
tags: bundle, imports, tree-shaking, barrel-files, performance
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Avoid Barrel File Imports
|
|
9
|
-
|
|
10
|
-
Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
|
|
11
|
-
|
|
12
|
-
Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
|
|
13
|
-
|
|
14
|
-
**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
|
|
15
|
-
|
|
16
|
-
**Incorrect (imports entire library):**
|
|
17
|
-
|
|
18
|
-
```tsx
|
|
19
|
-
import { Check, X, Menu } from "lucide-react";
|
|
20
|
-
// Loads 1,583 modules, takes ~2.8s extra in dev
|
|
21
|
-
// Runtime cost: 200-800ms on every cold start
|
|
22
|
-
|
|
23
|
-
import { Button, TextField } from "@mui/material";
|
|
24
|
-
// Loads 2,225 modules, takes ~4.2s extra in dev
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
**Correct (imports only what you need):**
|
|
28
|
-
|
|
29
|
-
```tsx
|
|
30
|
-
import Check from "lucide-react/dist/esm/icons/check";
|
|
31
|
-
import X from "lucide-react/dist/esm/icons/x";
|
|
32
|
-
import Menu from "lucide-react/dist/esm/icons/menu";
|
|
33
|
-
// Loads only 3 modules (~2KB vs ~1MB)
|
|
34
|
-
|
|
35
|
-
import Button from "@mui/material/Button";
|
|
36
|
-
import TextField from "@mui/material/TextField";
|
|
37
|
-
// Loads only what you use
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
|
|
41
|
-
|
|
42
|
-
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`.
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Conditional Module Loading
|
|
3
|
-
impact: HIGH
|
|
4
|
-
impactDescription: loads large data only when needed
|
|
5
|
-
tags: bundle, conditional-loading, lazy-loading, react-19, suspense
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Conditional Module Loading
|
|
9
|
-
|
|
10
|
-
Load large data or modules only when a feature is activated, reducing initial bundle size.
|
|
11
|
-
|
|
12
|
-
**React 19 with use() and Suspense (Recommended):**
|
|
13
|
-
|
|
14
|
-
```tsx
|
|
15
|
-
import { use, Suspense, useMemo } from "react";
|
|
16
|
-
|
|
17
|
-
function AnimationPlayer({ enabled }: { enabled: boolean }) {
|
|
18
|
-
// Create promise only when enabled
|
|
19
|
-
const framesPromise = useMemo(
|
|
20
|
-
() =>
|
|
21
|
-
enabled ? import("./animation-frames.js").then((m) => m.frames) : null,
|
|
22
|
-
[enabled],
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
if (!framesPromise) return null;
|
|
26
|
-
|
|
27
|
-
const frames = use(framesPromise); // React 19: unwraps promise
|
|
28
|
-
return <Canvas frames={frames} />;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Wrap in Suspense
|
|
32
|
-
function AnimationSection({ enabled }: { enabled: boolean }) {
|
|
33
|
-
return (
|
|
34
|
-
<Suspense fallback={<Skeleton />}>
|
|
35
|
-
<AnimationPlayer enabled={enabled} />
|
|
36
|
-
</Suspense>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
**With React.lazy for component splitting:**
|
|
42
|
-
|
|
43
|
-
```tsx
|
|
44
|
-
import { lazy, Suspense } from "react";
|
|
45
|
-
|
|
46
|
-
// Component is only loaded when rendered
|
|
47
|
-
const HeavyEditor = lazy(() => import("./HeavyEditor"));
|
|
48
|
-
|
|
49
|
-
function EditorSection({ showEditor }: { showEditor: boolean }) {
|
|
50
|
-
if (!showEditor) return null;
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<Suspense fallback={<EditorSkeleton />}>
|
|
54
|
-
<HeavyEditor />
|
|
55
|
-
</Suspense>
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
**Traditional pattern with useEffect (pre-React 19):**
|
|
61
|
-
|
|
62
|
-
```tsx
|
|
63
|
-
function AnimationPlayer({ enabled }: { enabled: boolean }) {
|
|
64
|
-
const [frames, setFrames] = useState<Frame[] | null>(null);
|
|
65
|
-
const [error, setError] = useState<Error | null>(null);
|
|
66
|
-
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
if (enabled && !frames) {
|
|
69
|
-
import("./animation-frames.js")
|
|
70
|
-
.then((mod) => setFrames(mod.frames))
|
|
71
|
-
.catch(setError);
|
|
72
|
-
}
|
|
73
|
-
}, [enabled, frames]);
|
|
74
|
-
|
|
75
|
-
if (error) return <ErrorMessage error={error} />;
|
|
76
|
-
if (!frames) return <Skeleton />;
|
|
77
|
-
return <Canvas frames={frames} />;
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
**With React Query for data modules:**
|
|
82
|
-
|
|
83
|
-
```tsx
|
|
84
|
-
import { useQuery } from "@tanstack/react-query";
|
|
85
|
-
|
|
86
|
-
function AnimationPlayer({ enabled }: { enabled: boolean }) {
|
|
87
|
-
const { data: frames, isLoading } = useQuery({
|
|
88
|
-
queryKey: ["animation-frames"],
|
|
89
|
-
queryFn: () => import("./animation-frames.js").then((m) => m.frames),
|
|
90
|
-
enabled, // Only fetch when enabled
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
if (!enabled) return null;
|
|
94
|
-
if (isLoading) return <Skeleton />;
|
|
95
|
-
return <Canvas frames={frames} />;
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
**Key benefits:**
|
|
100
|
-
|
|
101
|
-
- Reduces initial bundle size
|
|
102
|
-
- Improves Time to Interactive (TTI)
|
|
103
|
-
- Loads resources on-demand
|
|
104
|
-
- Works with code splitting
|
|
105
|
-
|
|
106
|
-
Reference: [React lazy](https://react.dev/reference/react/lazy)
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Preload Based on User Intent
|
|
3
|
-
impact: MEDIUM
|
|
4
|
-
impactDescription: reduces perceived latency
|
|
5
|
-
tags: bundle, preload, user-intent, hover
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Preload Based on User Intent
|
|
9
|
-
|
|
10
|
-
Preload heavy bundles before they're needed to reduce perceived latency.
|
|
11
|
-
|
|
12
|
-
**Example (preload on hover/focus):**
|
|
13
|
-
|
|
14
|
-
```tsx
|
|
15
|
-
function EditorButton({ onClick }: { onClick: () => void }) {
|
|
16
|
-
const preload = () => {
|
|
17
|
-
void import("./monaco-editor");
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
|
|
22
|
-
Open Editor
|
|
23
|
-
</button>
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
**Example (preload when feature flag is enabled):**
|
|
29
|
-
|
|
30
|
-
```tsx
|
|
31
|
-
function FlagsProvider({ children, flags }: Props) {
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
if (flags.editorEnabled) {
|
|
34
|
-
void import("./monaco-editor").then((mod) => mod.init());
|
|
35
|
-
}
|
|
36
|
-
}, [flags.editorEnabled]);
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Preloading on user intent (hover, focus) or feature activation reduces perceived latency when the user actually clicks.
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Deduplicate Global Event Listeners
|
|
3
|
-
impact: MEDIUM
|
|
4
|
-
impactDescription: single listener for N components
|
|
5
|
-
tags: client, event-listeners, useSyncExternalStore, subscription
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Deduplicate Global Event Listeners
|
|
9
|
-
|
|
10
|
-
Use `useSyncExternalStore` to share global event listeners across component instances. This is the React 18+ standard pattern for subscribing to external stores.
|
|
11
|
-
|
|
12
|
-
**Incorrect (N instances = N listeners):**
|
|
13
|
-
|
|
14
|
-
```tsx
|
|
15
|
-
function useKeyboardShortcut(key: string, callback: () => void) {
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
const handler = (e: KeyboardEvent) => {
|
|
18
|
-
if (e.metaKey && e.key === key) {
|
|
19
|
-
callback();
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
window.addEventListener("keydown", handler);
|
|
23
|
-
return () => window.removeEventListener("keydown", handler);
|
|
24
|
-
}, [key, callback]);
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
When using `useKeyboardShortcut` multiple times, each instance registers a new listener.
|
|
29
|
-
|
|
30
|
-
**Correct (N instances = 1 listener with useSyncExternalStore):**
|
|
31
|
-
|
|
32
|
-
```tsx
|
|
33
|
-
import { useSyncExternalStore, useCallback, useEffect } from "react";
|
|
34
|
-
|
|
35
|
-
// Module-level store for keyboard shortcuts
|
|
36
|
-
const keyCallbacks = new Map<string, Set<() => void>>();
|
|
37
|
-
const listeners = new Set<() => void>();
|
|
38
|
-
|
|
39
|
-
function subscribeToKeyboard(onStoreChange: () => void) {
|
|
40
|
-
if (listeners.size === 0) {
|
|
41
|
-
// First subscriber - add the global listener
|
|
42
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
43
|
-
}
|
|
44
|
-
listeners.add(onStoreChange);
|
|
45
|
-
|
|
46
|
-
return () => {
|
|
47
|
-
listeners.delete(onStoreChange);
|
|
48
|
-
if (listeners.size === 0) {
|
|
49
|
-
// Last subscriber - remove the global listener
|
|
50
|
-
window.removeEventListener("keydown", handleKeyDown);
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function handleKeyDown(e: KeyboardEvent) {
|
|
56
|
-
if (e.metaKey && keyCallbacks.has(e.key)) {
|
|
57
|
-
keyCallbacks.get(e.key)!.forEach((cb) => cb());
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function useKeyboardShortcut(key: string, callback: () => void) {
|
|
62
|
-
// Register callback in the module-level Map
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
if (!keyCallbacks.has(key)) {
|
|
65
|
-
keyCallbacks.set(key, new Set());
|
|
66
|
-
}
|
|
67
|
-
keyCallbacks.get(key)!.add(callback);
|
|
68
|
-
|
|
69
|
-
return () => {
|
|
70
|
-
const set = keyCallbacks.get(key);
|
|
71
|
-
if (set) {
|
|
72
|
-
set.delete(callback);
|
|
73
|
-
if (set.size === 0) {
|
|
74
|
-
keyCallbacks.delete(key);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
}, [key, callback]);
|
|
79
|
-
|
|
80
|
-
// Subscribe to the shared keyboard listener
|
|
81
|
-
useSyncExternalStore(
|
|
82
|
-
subscribeToKeyboard,
|
|
83
|
-
() => null, // getSnapshot - we don't need state, just subscription
|
|
84
|
-
() => null, // getServerSnapshot
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Usage - multiple shortcuts share single listener
|
|
89
|
-
function Profile() {
|
|
90
|
-
useKeyboardShortcut("p", () => openProfile());
|
|
91
|
-
useKeyboardShortcut("k", () => openSearch());
|
|
92
|
-
useKeyboardShortcut("/", () => focusCommandBar());
|
|
93
|
-
// All share ONE keydown listener!
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
**Simpler pattern for single-use subscriptions:**
|
|
98
|
-
|
|
99
|
-
```tsx
|
|
100
|
-
import { useSyncExternalStore } from "react";
|
|
101
|
-
|
|
102
|
-
function useOnlineStatus() {
|
|
103
|
-
return useSyncExternalStore(
|
|
104
|
-
(callback) => {
|
|
105
|
-
window.addEventListener("online", callback);
|
|
106
|
-
window.addEventListener("offline", callback);
|
|
107
|
-
return () => {
|
|
108
|
-
window.removeEventListener("online", callback);
|
|
109
|
-
window.removeEventListener("offline", callback);
|
|
110
|
-
};
|
|
111
|
-
},
|
|
112
|
-
() => navigator.onLine,
|
|
113
|
-
() => true, // Server snapshot
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function StatusIndicator() {
|
|
118
|
-
const isOnline = useOnlineStatus();
|
|
119
|
-
return <span>{isOnline ? "🟢" : "🔴"}</span>;
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
**Why useSyncExternalStore?**
|
|
124
|
-
|
|
125
|
-
- Official React API for external subscriptions
|
|
126
|
-
- Handles concurrent rendering correctly
|
|
127
|
-
- Automatic cleanup on unmount
|
|
128
|
-
- Works with React 18+ concurrent features
|
|
129
|
-
- No external dependencies required
|
|
130
|
-
|
|
131
|
-
Reference: [React useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore)
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Use React Query for Automatic Deduplication
|
|
3
|
-
impact: MEDIUM-HIGH
|
|
4
|
-
impactDescription: automatic deduplication and caching
|
|
5
|
-
tags: client, react-query, tanstack-query, deduplication, data-fetching
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Use React Query for Automatic Deduplication
|
|
9
|
-
|
|
10
|
-
TanStack Query (React Query 5) enables request deduplication, caching, background refetching, and stale-while-revalidate patterns across component instances.
|
|
11
|
-
|
|
12
|
-
**Incorrect (no deduplication, each instance fetches):**
|
|
13
|
-
|
|
14
|
-
```tsx
|
|
15
|
-
function UserList() {
|
|
16
|
-
const [users, setUsers] = useState<User[]>([]);
|
|
17
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
18
|
-
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
fetch("/api/users")
|
|
21
|
-
.then((r) => r.json())
|
|
22
|
-
.then(setUsers)
|
|
23
|
-
.finally(() => setIsLoading(false));
|
|
24
|
-
}, []);
|
|
25
|
-
|
|
26
|
-
if (isLoading) return <Skeleton />;
|
|
27
|
-
return (
|
|
28
|
-
<ul>
|
|
29
|
-
{users.map((u) => (
|
|
30
|
-
<li key={u.id}>{u.name}</li>
|
|
31
|
-
))}
|
|
32
|
-
</ul>
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Multiple instances of `UserList` = multiple network requests.
|
|
38
|
-
|
|
39
|
-
**Correct (multiple instances share one request):**
|
|
40
|
-
|
|
41
|
-
```tsx
|
|
42
|
-
import { useQuery } from "@tanstack/react-query";
|
|
43
|
-
|
|
44
|
-
function UserList() {
|
|
45
|
-
const { data: users, isLoading } = useQuery({
|
|
46
|
-
queryKey: ["users"],
|
|
47
|
-
queryFn: () => fetch("/api/users").then((r) => r.json()),
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
if (isLoading) return <Skeleton />;
|
|
51
|
-
return (
|
|
52
|
-
<ul>
|
|
53
|
-
{users.map((u) => (
|
|
54
|
-
<li key={u.id}>{u.name}</li>
|
|
55
|
-
))}
|
|
56
|
-
</ul>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Multiple instances = single request, shared cache.
|
|
62
|
-
|
|
63
|
-
**With Suspense (React 19):**
|
|
64
|
-
|
|
65
|
-
```tsx
|
|
66
|
-
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
67
|
-
|
|
68
|
-
function UserList() {
|
|
69
|
-
const { data: users } = useSuspenseQuery({
|
|
70
|
-
queryKey: ["users"],
|
|
71
|
-
queryFn: fetchUsers,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
return (
|
|
75
|
-
<ul>
|
|
76
|
-
{users.map((u) => (
|
|
77
|
-
<li key={u.id}>{u.name}</li>
|
|
78
|
-
))}
|
|
79
|
-
</ul>
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Parent component
|
|
84
|
-
<Suspense fallback={<Skeleton />}>
|
|
85
|
-
<UserList />
|
|
86
|
-
</Suspense>;
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
**For mutations:**
|
|
90
|
-
|
|
91
|
-
```tsx
|
|
92
|
-
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
93
|
-
|
|
94
|
-
function UpdateButton({ userId }: { userId: string }) {
|
|
95
|
-
const queryClient = useQueryClient();
|
|
96
|
-
|
|
97
|
-
const { mutate, isPending } = useMutation({
|
|
98
|
-
mutationFn: (data: UserUpdate) => updateUser(userId, data),
|
|
99
|
-
onSuccess: () => {
|
|
100
|
-
// Invalidate and refetch
|
|
101
|
-
queryClient.invalidateQueries({ queryKey: ["users"] });
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
return (
|
|
106
|
-
<button onClick={() => mutate({ name: "New Name" })} disabled={isPending}>
|
|
107
|
-
{isPending ? "Updating..." : "Update"}
|
|
108
|
-
</button>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
**For immutable/static data:**
|
|
114
|
-
|
|
115
|
-
```tsx
|
|
116
|
-
const { data } = useQuery({
|
|
117
|
-
queryKey: ["config"],
|
|
118
|
-
queryFn: fetchConfig,
|
|
119
|
-
staleTime: Infinity, // Never refetch
|
|
120
|
-
gcTime: Infinity, // Never garbage collect
|
|
121
|
-
});
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
**Key benefits:**
|
|
125
|
-
|
|
126
|
-
- Automatic request deduplication
|
|
127
|
-
- Built-in caching with configurable stale times
|
|
128
|
-
- Background refetching
|
|
129
|
-
- Optimistic updates
|
|
130
|
-
- Suspense support (React 19)
|
|
131
|
-
- DevTools for debugging
|
|
132
|
-
|
|
133
|
-
Reference: [https://tanstack.com/query](https://tanstack.com/query)
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Batch DOM CSS Changes
|
|
3
|
-
impact: MEDIUM
|
|
4
|
-
impactDescription: reduces reflows/repaints
|
|
5
|
-
tags: javascript, dom, css, performance, reflow
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Batch DOM CSS Changes
|
|
9
|
-
|
|
10
|
-
Avoid changing styles one property at a time. Group multiple CSS changes together via classes or `cssText` to minimize browser reflows.
|
|
11
|
-
|
|
12
|
-
**Incorrect (multiple reflows):**
|
|
13
|
-
|
|
14
|
-
```typescript
|
|
15
|
-
function updateElementStyles(element: HTMLElement) {
|
|
16
|
-
// Each line triggers a reflow
|
|
17
|
-
element.style.width = '100px'
|
|
18
|
-
element.style.height = '200px'
|
|
19
|
-
element.style.backgroundColor = 'blue'
|
|
20
|
-
element.style.border = '1px solid black'
|
|
21
|
-
}
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
**Correct (add class - single reflow):**
|
|
25
|
-
|
|
26
|
-
```typescript
|
|
27
|
-
// CSS file
|
|
28
|
-
.highlighted-box {
|
|
29
|
-
width: 100px;
|
|
30
|
-
height: 200px;
|
|
31
|
-
background-color: blue;
|
|
32
|
-
border: 1px solid black;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// JavaScript
|
|
36
|
-
function updateElementStyles(element: HTMLElement) {
|
|
37
|
-
element.classList.add('highlighted-box')
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
**Correct (change cssText - single reflow):**
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
function updateElementStyles(element: HTMLElement) {
|
|
45
|
-
element.style.cssText = `
|
|
46
|
-
width: 100px;
|
|
47
|
-
height: 200px;
|
|
48
|
-
background-color: blue;
|
|
49
|
-
border: 1px solid black;
|
|
50
|
-
`
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
**React example:**
|
|
55
|
-
|
|
56
|
-
```tsx
|
|
57
|
-
// Incorrect: changing styles one by one
|
|
58
|
-
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
|
59
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
60
|
-
|
|
61
|
-
useEffect(() => {
|
|
62
|
-
if (ref.current && isHighlighted) {
|
|
63
|
-
ref.current.style.width = '100px'
|
|
64
|
-
ref.current.style.height = '200px'
|
|
65
|
-
ref.current.style.backgroundColor = 'blue'
|
|
66
|
-
}
|
|
67
|
-
}, [isHighlighted])
|
|
68
|
-
|
|
69
|
-
return <div ref={ref}>Content</div>
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Correct: toggle class
|
|
73
|
-
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
|
74
|
-
return (
|
|
75
|
-
<div className={isHighlighted ? 'highlighted-box' : ''}>
|
|
76
|
-
Content
|
|
77
|
-
</div>
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Prefer CSS classes over inline styles when possible. Classes are cached by the browser and provide better separation of concerns.
|