@brookmind/ai-toolkit 1.0.1 → 1.1.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/README.md +54 -14
- package/agents/code-reviewer.md +6 -1
- package/agents/code-simplifier.md +52 -0
- package/bin/cli.js +1 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +321 -0
- package/dist/index.js.map +1 -0
- package/mcps/context7/.mcp.json +13 -0
- package/mcps/expo-mcp/.mcp.json +13 -0
- package/mcps/figma-mcp/.mcp.json +4 -6
- package/package.json +22 -11
- package/skills/pdf-processing-pro/FORMS.md +610 -0
- package/skills/pdf-processing-pro/OCR.md +137 -0
- package/skills/pdf-processing-pro/SKILL.md +296 -0
- package/skills/pdf-processing-pro/TABLES.md +626 -0
- package/skills/pdf-processing-pro/scripts/analyze_form.py +307 -0
- package/skills/react-best-practices/AGENTS.md +915 -0
- package/skills/react-best-practices/README.md +127 -0
- package/skills/react-best-practices/SKILL.md +110 -0
- package/skills/react-best-practices/metadata.json +14 -0
- package/skills/react-best-practices/rules/_sections.md +41 -0
- package/skills/react-best-practices/rules/_template.md +28 -0
- package/skills/react-best-practices/rules/advanced-event-handler-refs.md +80 -0
- package/skills/react-best-practices/rules/advanced-use-latest.md +76 -0
- package/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/skills/react-best-practices/rules/async-dependencies.md +36 -0
- package/skills/react-best-practices/rules/async-parallel.md +28 -0
- package/skills/react-best-practices/rules/async-suspense-boundaries.md +100 -0
- package/skills/react-best-practices/rules/bundle-barrel-imports.md +42 -0
- package/skills/react-best-practices/rules/bundle-conditional.md +106 -0
- package/skills/react-best-practices/rules/bundle-preload.md +44 -0
- package/skills/react-best-practices/rules/client-event-listeners.md +131 -0
- package/skills/react-best-practices/rules/client-swr-dedup.md +133 -0
- package/skills/react-best-practices/rules/js-batch-dom-css.md +82 -0
- package/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/skills/react-best-practices/rules/js-cache-storage.md +70 -0
- package/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/skills/react-best-practices/rules/js-length-check-first.md +49 -0
- package/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/skills/react-best-practices/rules/rendering-activity.md +90 -0
- package/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/skills/react-best-practices/rules/rendering-hoist-jsx.md +65 -0
- package/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/skills/react-best-practices/rules/rerender-memo.md +85 -0
- package/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/themes/README.md +68 -0
- package/themes/claude-vivid.json +72 -0
- package/mcps/context7/.claude-plugin +0 -1
- package/mcps/context7/README.md +0 -1
- package/mcps/context7/server.json +0 -1
- package/mcps/expo-mcp/README.md +0 -33
- package/mcps/expo-mcp/package.json +0 -30
- package/mcps/figma-mcp/README.md +0 -554
- package/mcps/figma-mcp/server.json +0 -17
- package/mcps/figma-mcp/skills/code-connect-components +0 -1
- package/mcps/figma-mcp/skills/create-design-system-rules +0 -1
- package/mcps/figma-mcp/skills/implement-design +0 -1
- package/mcps/pg-aiguide/.claude-plugin +0 -1
- package/mcps/pg-aiguide/CLAUDE.md +0 -21
- package/mcps/pg-aiguide/README.md +0 -275
- package/mcps/pg-aiguide/skills/design-postgres-tables +0 -1
- package/mcps/pg-aiguide/skills/find-hypertable-candidates +0 -1
- package/mcps/pg-aiguide/skills/migrate-postgres-tables-to-hypertables +0 -1
- package/mcps/pg-aiguide/skills/setup-timescaledb-hypertables +0 -1
- package/mcps/pg-aiguide/skills.yaml +0 -4
- package/skills/cloudflare-cli/SKILL.md +0 -151
- package/skills/docx/LICENSE.txt +0 -30
- package/skills/docx/SKILL.md +0 -197
- package/skills/docx/docx-js.md +0 -350
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +0 -1499
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +0 -146
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +0 -1085
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +0 -11
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +0 -3081
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +0 -23
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +0 -185
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +0 -287
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +0 -1676
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +0 -28
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +0 -144
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +0 -174
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +0 -25
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +0 -18
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +0 -59
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +0 -56
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +0 -195
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +0 -582
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +0 -25
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +0 -4439
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +0 -570
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +0 -509
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +0 -12
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +0 -108
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +0 -96
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +0 -3646
- package/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +0 -116
- package/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +0 -42
- package/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +0 -50
- package/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +0 -49
- package/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +0 -33
- package/skills/docx/ooxml/schemas/mce/mc.xsd +0 -75
- package/skills/docx/ooxml/schemas/microsoft/wml-2010.xsd +0 -560
- package/skills/docx/ooxml/schemas/microsoft/wml-2012.xsd +0 -67
- package/skills/docx/ooxml/schemas/microsoft/wml-2018.xsd +0 -14
- package/skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd +0 -20
- package/skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd +0 -13
- package/skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +0 -4
- package/skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd +0 -8
- package/skills/docx/ooxml/scripts/pack.py +0 -159
- package/skills/docx/ooxml/scripts/unpack.py +0 -29
- package/skills/docx/ooxml/scripts/validate.py +0 -69
- package/skills/docx/ooxml/scripts/validation/__init__.py +0 -15
- package/skills/docx/ooxml/scripts/validation/base.py +0 -951
- package/skills/docx/ooxml/scripts/validation/docx.py +0 -274
- package/skills/docx/ooxml/scripts/validation/pptx.py +0 -315
- package/skills/docx/ooxml/scripts/validation/redlining.py +0 -279
- package/skills/docx/ooxml.md +0 -610
- package/skills/docx/scripts/__init__.py +0 -1
- package/skills/docx/scripts/document.py +0 -1276
- package/skills/docx/scripts/templates/comments.xml +0 -3
- package/skills/docx/scripts/templates/commentsExtended.xml +0 -3
- package/skills/docx/scripts/templates/commentsExtensible.xml +0 -3
- package/skills/docx/scripts/templates/commentsIds.xml +0 -3
- package/skills/docx/scripts/templates/people.xml +0 -3
- package/skills/docx/scripts/utilities.py +0 -374
- package/skills/pdf/LICENSE.txt +0 -30
- package/skills/pdf/SKILL.md +0 -294
- package/skills/pdf/forms.md +0 -205
- package/skills/pdf/reference.md +0 -612
- package/skills/pdf/scripts/check_bounding_boxes.py +0 -70
- package/skills/pdf/scripts/check_bounding_boxes_test.py +0 -226
- package/skills/pdf/scripts/check_fillable_fields.py +0 -12
- package/skills/pdf/scripts/convert_pdf_to_images.py +0 -35
- package/skills/pdf/scripts/create_validation_image.py +0 -41
- package/skills/pdf/scripts/extract_form_field_info.py +0 -152
- package/skills/pdf/scripts/fill_fillable_fields.py +0 -114
- package/skills/pdf/scripts/fill_pdf_form_with_annotations.py +0 -108
- package/skills/xlsx/LICENSE.txt +0 -30
- package/skills/xlsx/SKILL.md +0 -289
- package/skills/xlsx/recalc.py +0 -178
- package/src/index.js +0 -365
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
# React Best Practices - Compiled Rules
|
|
2
|
+
|
|
3
|
+
> Performance optimization guidelines for React 19 applications. Contains 35+ rules across 7 categories, prioritized by impact.
|
|
4
|
+
|
|
5
|
+
**Last Updated:** January 2026
|
|
6
|
+
**React Version:** 19
|
|
7
|
+
**Target:** Client-side SPA (Vite, etc.)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
1. [Eliminating Waterfalls](#1-eliminating-waterfalls-critical) (CRITICAL)
|
|
14
|
+
2. [Bundle Size Optimization](#2-bundle-size-optimization-critical) (CRITICAL)
|
|
15
|
+
3. [Client-Side Data Fetching](#3-client-side-data-fetching-medium-high) (MEDIUM-HIGH)
|
|
16
|
+
4. [Re-render Optimization](#4-re-render-optimization-medium) (MEDIUM)
|
|
17
|
+
5. [Rendering Performance](#5-rendering-performance-medium) (MEDIUM)
|
|
18
|
+
6. [JavaScript Performance](#6-javascript-performance-low-medium) (LOW-MEDIUM)
|
|
19
|
+
7. [Advanced Patterns](#7-advanced-patterns-low) (LOW)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. Eliminating Waterfalls (CRITICAL)
|
|
24
|
+
|
|
25
|
+
Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
|
|
26
|
+
|
|
27
|
+
### Promise.all() for Independent Operations
|
|
28
|
+
|
|
29
|
+
**Impact:** CRITICAL (2-10× improvement)
|
|
30
|
+
|
|
31
|
+
When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
|
|
32
|
+
|
|
33
|
+
**Incorrect (sequential execution, 3 round trips):**
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
const user = await fetchUser();
|
|
37
|
+
const posts = await fetchPosts();
|
|
38
|
+
const comments = await fetchComments();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Correct (parallel execution, 1 round trip):**
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const [user, posts, comments] = await Promise.all([
|
|
45
|
+
fetchUser(),
|
|
46
|
+
fetchPosts(),
|
|
47
|
+
fetchComments(),
|
|
48
|
+
]);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Dependency-Based Parallelization
|
|
52
|
+
|
|
53
|
+
**Impact:** CRITICAL (2-10× improvement)
|
|
54
|
+
|
|
55
|
+
For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
|
|
56
|
+
|
|
57
|
+
**Incorrect (profile waits for config unnecessarily):**
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
const [user, config] = await Promise.all([fetchUser(), fetchConfig()]);
|
|
61
|
+
const profile = await fetchProfile(user.id);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Correct (config and profile run in parallel):**
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { all } from 'better-all';
|
|
68
|
+
|
|
69
|
+
const { user, config, profile } = await all({
|
|
70
|
+
async user() {
|
|
71
|
+
return fetchUser();
|
|
72
|
+
},
|
|
73
|
+
async config() {
|
|
74
|
+
return fetchConfig();
|
|
75
|
+
},
|
|
76
|
+
async profile() {
|
|
77
|
+
return fetchProfile((await this.$.user).id);
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
|
|
83
|
+
|
|
84
|
+
### Defer Await Until Needed
|
|
85
|
+
|
|
86
|
+
**Impact:** HIGH
|
|
87
|
+
|
|
88
|
+
Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
|
|
89
|
+
|
|
90
|
+
**Incorrect (blocks both branches):**
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
async function handleRequest(userId: string, skipProcessing: boolean) {
|
|
94
|
+
const userData = await fetchUserData(userId);
|
|
95
|
+
|
|
96
|
+
if (skipProcessing) {
|
|
97
|
+
return { skipped: true };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return processUserData(userData);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Correct (only blocks when needed):**
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
async function handleRequest(userId: string, skipProcessing: boolean) {
|
|
108
|
+
if (skipProcessing) {
|
|
109
|
+
return { skipped: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const userData = await fetchUserData(userId);
|
|
113
|
+
return processUserData(userData);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Strategic Suspense Boundaries
|
|
118
|
+
|
|
119
|
+
**Impact:** HIGH (faster initial paint)
|
|
120
|
+
|
|
121
|
+
Use Suspense boundaries to show wrapper UI immediately while data loads asynchronously.
|
|
122
|
+
|
|
123
|
+
**Incorrect (entire component waits for data):**
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
function Page() {
|
|
127
|
+
const { data, isLoading } = useQuery(['data'], fetchData);
|
|
128
|
+
|
|
129
|
+
if (isLoading) return <Skeleton />;
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div>
|
|
133
|
+
<div>Sidebar</div>
|
|
134
|
+
<div>Header</div>
|
|
135
|
+
<div>
|
|
136
|
+
<DataDisplay data={data} />
|
|
137
|
+
</div>
|
|
138
|
+
<div>Footer</div>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Correct (wrapper shows immediately):**
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
function Page() {
|
|
148
|
+
return (
|
|
149
|
+
<div>
|
|
150
|
+
<div>Sidebar</div>
|
|
151
|
+
<div>Header</div>
|
|
152
|
+
<Suspense fallback={<Skeleton />}>
|
|
153
|
+
<DataDisplay />
|
|
154
|
+
</Suspense>
|
|
155
|
+
<div>Footer</div>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function DataDisplay() {
|
|
161
|
+
const { data } = useSuspenseQuery(['data'], fetchData);
|
|
162
|
+
return <div>{data.content}</div>;
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**With React 19 use() hook:**
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
function Page() {
|
|
170
|
+
const dataPromise = useMemo(() => fetchData(), []);
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<Suspense fallback={<Skeleton />}>
|
|
174
|
+
<DataDisplay dataPromise={dataPromise} />
|
|
175
|
+
</Suspense>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
|
|
180
|
+
const data = use(dataPromise);
|
|
181
|
+
return <div>{data.content}</div>;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 2. Bundle Size Optimization (CRITICAL)
|
|
188
|
+
|
|
189
|
+
Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
|
|
190
|
+
|
|
191
|
+
### Avoid Barrel File Imports
|
|
192
|
+
|
|
193
|
+
**Impact:** CRITICAL (200-800ms import cost)
|
|
194
|
+
|
|
195
|
+
Import directly from source files instead of barrel files to avoid loading thousands of unused modules.
|
|
196
|
+
|
|
197
|
+
**Incorrect (imports entire library):**
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
import { Check, X, Menu } from 'lucide-react';
|
|
201
|
+
// Loads 1,583 modules, takes ~2.8s extra in dev
|
|
202
|
+
|
|
203
|
+
import { Button, TextField } from '@mui/material';
|
|
204
|
+
// Loads 2,225 modules, takes ~4.2s extra in dev
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Correct (imports only what you need):**
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
import Check from 'lucide-react/dist/esm/icons/check';
|
|
211
|
+
import X from 'lucide-react/dist/esm/icons/x';
|
|
212
|
+
import Menu from 'lucide-react/dist/esm/icons/menu';
|
|
213
|
+
|
|
214
|
+
import Button from '@mui/material/Button';
|
|
215
|
+
import TextField from '@mui/material/TextField';
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Commonly affected libraries: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `lodash`, `date-fns`.
|
|
219
|
+
|
|
220
|
+
### Conditional Module Loading
|
|
221
|
+
|
|
222
|
+
**Impact:** HIGH
|
|
223
|
+
|
|
224
|
+
Load large data or modules only when a feature is activated.
|
|
225
|
+
|
|
226
|
+
**React 19 with use() and Suspense (Recommended):**
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
import { use, Suspense, useMemo } from 'react';
|
|
230
|
+
|
|
231
|
+
function AnimationPlayer({ enabled }: { enabled: boolean }) {
|
|
232
|
+
const framesPromise = useMemo(
|
|
233
|
+
() =>
|
|
234
|
+
enabled ? import('./animation-frames.js').then((m) => m.frames) : null,
|
|
235
|
+
[enabled]
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
if (!framesPromise) return null;
|
|
239
|
+
|
|
240
|
+
const frames = use(framesPromise);
|
|
241
|
+
return <Canvas frames={frames} />;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function AnimationSection({ enabled }: { enabled: boolean }) {
|
|
245
|
+
return (
|
|
246
|
+
<Suspense fallback={<Skeleton />}>
|
|
247
|
+
<AnimationPlayer enabled={enabled} />
|
|
248
|
+
</Suspense>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**With React.lazy:**
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
const HeavyEditor = lazy(() => import('./HeavyEditor'));
|
|
257
|
+
|
|
258
|
+
function EditorSection({ showEditor }: { showEditor: boolean }) {
|
|
259
|
+
if (!showEditor) return null;
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<Suspense fallback={<EditorSkeleton />}>
|
|
263
|
+
<HeavyEditor />
|
|
264
|
+
</Suspense>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Preload Based on User Intent
|
|
270
|
+
|
|
271
|
+
**Impact:** MEDIUM
|
|
272
|
+
|
|
273
|
+
Preload heavy bundles on hover/focus to reduce perceived latency.
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
function EditorButton({ onClick }: { onClick: () => void }) {
|
|
277
|
+
const preload = () => {
|
|
278
|
+
void import('./monaco-editor');
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
|
|
283
|
+
Open Editor
|
|
284
|
+
</button>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## 3. Client-Side Data Fetching (MEDIUM-HIGH)
|
|
292
|
+
|
|
293
|
+
Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
|
|
294
|
+
|
|
295
|
+
### Use React Query for Automatic Deduplication
|
|
296
|
+
|
|
297
|
+
**Impact:** MEDIUM-HIGH
|
|
298
|
+
|
|
299
|
+
TanStack Query (React Query 5) enables request deduplication, caching, and stale-while-revalidate patterns.
|
|
300
|
+
|
|
301
|
+
**Incorrect (each instance fetches):**
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
function UserList() {
|
|
305
|
+
const [users, setUsers] = useState<User[]>([]);
|
|
306
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
307
|
+
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
fetch('/api/users')
|
|
310
|
+
.then((r) => r.json())
|
|
311
|
+
.then(setUsers)
|
|
312
|
+
.finally(() => setIsLoading(false));
|
|
313
|
+
}, []);
|
|
314
|
+
|
|
315
|
+
// ...
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Correct (multiple instances share one request):**
|
|
320
|
+
|
|
321
|
+
```tsx
|
|
322
|
+
import { useQuery } from '@tanstack/react-query';
|
|
323
|
+
|
|
324
|
+
function UserList() {
|
|
325
|
+
const { data: users, isLoading } = useQuery({
|
|
326
|
+
queryKey: ['users'],
|
|
327
|
+
queryFn: () => fetch('/api/users').then((r) => r.json()),
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
if (isLoading) return <Skeleton />;
|
|
331
|
+
return (
|
|
332
|
+
<ul>
|
|
333
|
+
{users.map((u) => (
|
|
334
|
+
<li key={u.id}>{u.name}</li>
|
|
335
|
+
))}
|
|
336
|
+
</ul>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**With Suspense (React 19):**
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
import { useSuspenseQuery } from '@tanstack/react-query';
|
|
345
|
+
|
|
346
|
+
function UserList() {
|
|
347
|
+
const { data: users } = useSuspenseQuery({
|
|
348
|
+
queryKey: ['users'],
|
|
349
|
+
queryFn: fetchUsers,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<ul>
|
|
354
|
+
{users.map((u) => (
|
|
355
|
+
<li key={u.id}>{u.name}</li>
|
|
356
|
+
))}
|
|
357
|
+
</ul>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Parent
|
|
362
|
+
<Suspense fallback={<Skeleton />}>
|
|
363
|
+
<UserList />
|
|
364
|
+
</Suspense>;
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Deduplicate Global Event Listeners
|
|
368
|
+
|
|
369
|
+
**Impact:** MEDIUM (single listener for N components)
|
|
370
|
+
|
|
371
|
+
Use `useSyncExternalStore` to share global event listeners across component instances.
|
|
372
|
+
|
|
373
|
+
**Incorrect (N instances = N listeners):**
|
|
374
|
+
|
|
375
|
+
```tsx
|
|
376
|
+
function useKeyboardShortcut(key: string, callback: () => void) {
|
|
377
|
+
useEffect(() => {
|
|
378
|
+
const handler = (e: KeyboardEvent) => {
|
|
379
|
+
if (e.metaKey && e.key === key) callback();
|
|
380
|
+
};
|
|
381
|
+
window.addEventListener('keydown', handler);
|
|
382
|
+
return () => window.removeEventListener('keydown', handler);
|
|
383
|
+
}, [key, callback]);
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
**Correct (N instances = 1 listener):**
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
import { useSyncExternalStore } from 'react';
|
|
391
|
+
|
|
392
|
+
function useOnlineStatus() {
|
|
393
|
+
return useSyncExternalStore(
|
|
394
|
+
(callback) => {
|
|
395
|
+
window.addEventListener('online', callback);
|
|
396
|
+
window.addEventListener('offline', callback);
|
|
397
|
+
return () => {
|
|
398
|
+
window.removeEventListener('online', callback);
|
|
399
|
+
window.removeEventListener('offline', callback);
|
|
400
|
+
};
|
|
401
|
+
},
|
|
402
|
+
() => navigator.onLine,
|
|
403
|
+
() => true
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Reference: [React useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore)
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## 4. Re-render Optimization (MEDIUM)
|
|
413
|
+
|
|
414
|
+
Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
|
|
415
|
+
|
|
416
|
+
### Extract to Memoized Components
|
|
417
|
+
|
|
418
|
+
**Impact:** MEDIUM
|
|
419
|
+
|
|
420
|
+
**With React 19 Compiler (Recommended):**
|
|
421
|
+
|
|
422
|
+
The compiler automatically memoizes components. Just write clean code:
|
|
423
|
+
|
|
424
|
+
```tsx
|
|
425
|
+
function UserAvatar({ user }: { user: User }) {
|
|
426
|
+
const id = computeAvatarId(user);
|
|
427
|
+
return <Avatar id={id} />;
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
**Without React Compiler (Manual):**
|
|
432
|
+
|
|
433
|
+
```tsx
|
|
434
|
+
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
|
|
435
|
+
const id = useMemo(() => computeAvatarId(user), [user]);
|
|
436
|
+
return <Avatar id={id} />;
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Use Functional setState Updates
|
|
441
|
+
|
|
442
|
+
**Impact:** MEDIUM
|
|
443
|
+
|
|
444
|
+
Use functional update form to prevent stale closures and create stable callbacks.
|
|
445
|
+
|
|
446
|
+
**Incorrect:**
|
|
447
|
+
|
|
448
|
+
```tsx
|
|
449
|
+
const addItems = useCallback(
|
|
450
|
+
(newItems: Item[]) => {
|
|
451
|
+
setItems([...items, ...newItems]);
|
|
452
|
+
},
|
|
453
|
+
[items]
|
|
454
|
+
); // Recreated on every items change
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
**Correct:**
|
|
458
|
+
|
|
459
|
+
```tsx
|
|
460
|
+
const addItems = useCallback((newItems: Item[]) => {
|
|
461
|
+
setItems((curr) => [...curr, ...newItems]);
|
|
462
|
+
}, []); // Stable, never recreated
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Use Lazy State Initialization
|
|
466
|
+
|
|
467
|
+
**Impact:** MEDIUM
|
|
468
|
+
|
|
469
|
+
Pass a function to `useState` for expensive initial values.
|
|
470
|
+
|
|
471
|
+
**Incorrect (runs on every render):**
|
|
472
|
+
|
|
473
|
+
```tsx
|
|
474
|
+
const [searchIndex] = useState(buildSearchIndex(items));
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**Correct (runs only once):**
|
|
478
|
+
|
|
479
|
+
```tsx
|
|
480
|
+
const [searchIndex] = useState(() => buildSearchIndex(items));
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Use Transitions for Non-Urgent Updates
|
|
484
|
+
|
|
485
|
+
**Impact:** MEDIUM
|
|
486
|
+
|
|
487
|
+
Mark frequent, non-urgent state updates as transitions.
|
|
488
|
+
|
|
489
|
+
```tsx
|
|
490
|
+
import { startTransition } from 'react';
|
|
491
|
+
|
|
492
|
+
const handler = () => {
|
|
493
|
+
startTransition(() => setScrollY(window.scrollY));
|
|
494
|
+
};
|
|
495
|
+
window.addEventListener('scroll', handler, { passive: true });
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Subscribe to Derived State
|
|
499
|
+
|
|
500
|
+
**Impact:** MEDIUM
|
|
501
|
+
|
|
502
|
+
Subscribe to derived boolean state instead of continuous values.
|
|
503
|
+
|
|
504
|
+
**Incorrect (re-renders on every pixel):**
|
|
505
|
+
|
|
506
|
+
```tsx
|
|
507
|
+
const width = useWindowWidth();
|
|
508
|
+
const isMobile = width < 768;
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**Correct (re-renders only on boolean change):**
|
|
512
|
+
|
|
513
|
+
```tsx
|
|
514
|
+
const isMobile = useMediaQuery('(max-width: 767px)');
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### Defer State Reads to Usage Point
|
|
518
|
+
|
|
519
|
+
**Impact:** MEDIUM
|
|
520
|
+
|
|
521
|
+
Don't subscribe to dynamic state if you only read it inside callbacks.
|
|
522
|
+
|
|
523
|
+
**Incorrect:**
|
|
524
|
+
|
|
525
|
+
```tsx
|
|
526
|
+
const searchParams = useSearchParams();
|
|
527
|
+
const handleShare = () => {
|
|
528
|
+
const ref = searchParams.get('ref');
|
|
529
|
+
shareChat(chatId, { ref });
|
|
530
|
+
};
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
**Correct:**
|
|
534
|
+
|
|
535
|
+
```tsx
|
|
536
|
+
const handleShare = () => {
|
|
537
|
+
const params = new URLSearchParams(window.location.search);
|
|
538
|
+
const ref = params.get('ref');
|
|
539
|
+
shareChat(chatId, { ref });
|
|
540
|
+
};
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Narrow Effect Dependencies
|
|
544
|
+
|
|
545
|
+
**Impact:** LOW
|
|
546
|
+
|
|
547
|
+
Specify primitive dependencies instead of objects.
|
|
548
|
+
|
|
549
|
+
**Incorrect:**
|
|
550
|
+
|
|
551
|
+
```tsx
|
|
552
|
+
useEffect(() => {
|
|
553
|
+
console.log(user.id);
|
|
554
|
+
}, [user]);
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
**Correct:**
|
|
558
|
+
|
|
559
|
+
```tsx
|
|
560
|
+
useEffect(() => {
|
|
561
|
+
console.log(user.id);
|
|
562
|
+
}, [user.id]);
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## 5. Rendering Performance (MEDIUM)
|
|
568
|
+
|
|
569
|
+
Optimizing the rendering process reduces the work the browser needs to do.
|
|
570
|
+
|
|
571
|
+
### CSS content-visibility for Long Lists
|
|
572
|
+
|
|
573
|
+
**Impact:** HIGH (10× faster initial render)
|
|
574
|
+
|
|
575
|
+
```css
|
|
576
|
+
.message-item {
|
|
577
|
+
content-visibility: auto;
|
|
578
|
+
contain-intrinsic-size: 0 80px;
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Preserve State for Hidden Components
|
|
583
|
+
|
|
584
|
+
**Impact:** MEDIUM
|
|
585
|
+
|
|
586
|
+
Use CSS visibility instead of conditional rendering to preserve state.
|
|
587
|
+
|
|
588
|
+
**Incorrect (unmounts, loses state):**
|
|
589
|
+
|
|
590
|
+
```tsx
|
|
591
|
+
return isOpen ? <ExpensiveMenu /> : null;
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
**Correct (stays mounted):**
|
|
595
|
+
|
|
596
|
+
```tsx
|
|
597
|
+
<div style={{ display: isOpen ? 'block' : 'none' }}>
|
|
598
|
+
<ExpensiveMenu />
|
|
599
|
+
</div>
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Hoist Static JSX Elements
|
|
603
|
+
|
|
604
|
+
**Impact:** LOW
|
|
605
|
+
|
|
606
|
+
**With React 19 Compiler:** Automatic - just write normal components.
|
|
607
|
+
|
|
608
|
+
**Without Compiler:**
|
|
609
|
+
|
|
610
|
+
```tsx
|
|
611
|
+
// Hoisted to module scope
|
|
612
|
+
const loadingSkeleton = <div className="animate-pulse h-20 bg-gray-200" />;
|
|
613
|
+
|
|
614
|
+
function Container({ loading }: { loading: boolean }) {
|
|
615
|
+
return <div>{loading && loadingSkeleton}</div>;
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### Animate SVG Wrapper Instead of SVG
|
|
620
|
+
|
|
621
|
+
**Impact:** LOW (enables hardware acceleration)
|
|
622
|
+
|
|
623
|
+
```tsx
|
|
624
|
+
// Incorrect
|
|
625
|
+
<svg className="animate-spin">...</svg>
|
|
626
|
+
|
|
627
|
+
// Correct
|
|
628
|
+
<div className="animate-spin">
|
|
629
|
+
<svg>...</svg>
|
|
630
|
+
</div>
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Use Explicit Conditional Rendering
|
|
634
|
+
|
|
635
|
+
**Impact:** LOW (prevents rendering 0 or NaN)
|
|
636
|
+
|
|
637
|
+
```tsx
|
|
638
|
+
// Incorrect (renders "0")
|
|
639
|
+
{
|
|
640
|
+
count && <span>{count}</span>;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Correct
|
|
644
|
+
{
|
|
645
|
+
count > 0 ? <span>{count}</span> : null;
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### Optimize SVG Precision
|
|
650
|
+
|
|
651
|
+
**Impact:** LOW
|
|
652
|
+
|
|
653
|
+
```bash
|
|
654
|
+
npx svgo --precision=1 --multipass icon.svg
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
---
|
|
658
|
+
|
|
659
|
+
## 6. JavaScript Performance (LOW-MEDIUM)
|
|
660
|
+
|
|
661
|
+
Micro-optimizations for hot paths can add up to meaningful improvements.
|
|
662
|
+
|
|
663
|
+
### Use toSorted() for Immutability
|
|
664
|
+
|
|
665
|
+
**Impact:** MEDIUM-HIGH
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
// Incorrect (mutates array)
|
|
669
|
+
const sorted = users.sort((a, b) => a.name.localeCompare(b.name));
|
|
670
|
+
|
|
671
|
+
// Correct (creates new array)
|
|
672
|
+
const sorted = users.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### Cache Repeated Function Calls
|
|
676
|
+
|
|
677
|
+
**Impact:** MEDIUM
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
const slugifyCache = new Map<string, string>();
|
|
681
|
+
|
|
682
|
+
function cachedSlugify(text: string): string {
|
|
683
|
+
if (slugifyCache.has(text)) return slugifyCache.get(text)!;
|
|
684
|
+
const result = slugify(text);
|
|
685
|
+
slugifyCache.set(text, result);
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### Batch DOM CSS Changes
|
|
691
|
+
|
|
692
|
+
**Impact:** MEDIUM
|
|
693
|
+
|
|
694
|
+
```tsx
|
|
695
|
+
// Incorrect (multiple reflows)
|
|
696
|
+
element.style.width = '100px';
|
|
697
|
+
element.style.height = '200px';
|
|
698
|
+
|
|
699
|
+
// Correct (single reflow)
|
|
700
|
+
element.classList.add('highlighted-box');
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### Early Length Check for Array Comparisons
|
|
704
|
+
|
|
705
|
+
**Impact:** MEDIUM-HIGH
|
|
706
|
+
|
|
707
|
+
```typescript
|
|
708
|
+
function hasChanges(current: string[], original: string[]) {
|
|
709
|
+
if (current.length !== original.length) return true;
|
|
710
|
+
// ... expensive comparison
|
|
711
|
+
}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### Use Set/Map for O(1) Lookups
|
|
715
|
+
|
|
716
|
+
**Impact:** LOW-MEDIUM
|
|
717
|
+
|
|
718
|
+
```typescript
|
|
719
|
+
// Incorrect O(n)
|
|
720
|
+
items.filter((item) => allowedIds.includes(item.id));
|
|
721
|
+
|
|
722
|
+
// Correct O(1)
|
|
723
|
+
const allowedSet = new Set(allowedIds);
|
|
724
|
+
items.filter((item) => allowedSet.has(item.id));
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Build Index Maps for Repeated Lookups
|
|
728
|
+
|
|
729
|
+
**Impact:** LOW-MEDIUM
|
|
730
|
+
|
|
731
|
+
```typescript
|
|
732
|
+
const userById = new Map(users.map((u) => [u.id, u]));
|
|
733
|
+
orders.map((order) => ({ ...order, user: userById.get(order.userId) }));
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
### Use Loop for Min/Max Instead of Sort
|
|
737
|
+
|
|
738
|
+
**Impact:** LOW (O(n) instead of O(n log n))
|
|
739
|
+
|
|
740
|
+
```typescript
|
|
741
|
+
let latest = projects[0];
|
|
742
|
+
for (let i = 1; i < projects.length; i++) {
|
|
743
|
+
if (projects[i].updatedAt > latest.updatedAt) latest = projects[i];
|
|
744
|
+
}
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### Cache Storage API Calls
|
|
748
|
+
|
|
749
|
+
**Impact:** LOW-MEDIUM
|
|
750
|
+
|
|
751
|
+
```typescript
|
|
752
|
+
const storageCache = new Map<string, string | null>();
|
|
753
|
+
|
|
754
|
+
function getLocalStorage(key: string) {
|
|
755
|
+
if (!storageCache.has(key)) {
|
|
756
|
+
storageCache.set(key, localStorage.getItem(key));
|
|
757
|
+
}
|
|
758
|
+
return storageCache.get(key);
|
|
759
|
+
}
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### Early Return from Functions
|
|
763
|
+
|
|
764
|
+
**Impact:** LOW-MEDIUM
|
|
765
|
+
|
|
766
|
+
```typescript
|
|
767
|
+
function validateUsers(users: User[]) {
|
|
768
|
+
for (const user of users) {
|
|
769
|
+
if (!user.email) return { valid: false, error: 'Email required' };
|
|
770
|
+
}
|
|
771
|
+
return { valid: true };
|
|
772
|
+
}
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
### Combine Multiple Array Iterations
|
|
776
|
+
|
|
777
|
+
**Impact:** LOW-MEDIUM
|
|
778
|
+
|
|
779
|
+
```typescript
|
|
780
|
+
// Incorrect (3 iterations)
|
|
781
|
+
const admins = users.filter((u) => u.isAdmin);
|
|
782
|
+
const testers = users.filter((u) => u.isTester);
|
|
783
|
+
|
|
784
|
+
// Correct (1 iteration)
|
|
785
|
+
const admins: User[] = [],
|
|
786
|
+
testers: User[] = [];
|
|
787
|
+
for (const user of users) {
|
|
788
|
+
if (user.isAdmin) admins.push(user);
|
|
789
|
+
if (user.isTester) testers.push(user);
|
|
790
|
+
}
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
### Hoist RegExp Creation
|
|
794
|
+
|
|
795
|
+
**Impact:** LOW-MEDIUM
|
|
796
|
+
|
|
797
|
+
```tsx
|
|
798
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
799
|
+
|
|
800
|
+
function Highlighter({ text, query }: Props) {
|
|
801
|
+
const regex = useMemo(() => new RegExp(`(${query})`, 'gi'), [query]);
|
|
802
|
+
// ...
|
|
803
|
+
}
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
### Cache Property Access in Loops
|
|
807
|
+
|
|
808
|
+
**Impact:** LOW-MEDIUM
|
|
809
|
+
|
|
810
|
+
```typescript
|
|
811
|
+
const value = obj.config.settings.value;
|
|
812
|
+
const len = arr.length;
|
|
813
|
+
for (let i = 0; i < len; i++) {
|
|
814
|
+
process(value);
|
|
815
|
+
}
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
---
|
|
819
|
+
|
|
820
|
+
## 7. Advanced Patterns (LOW)
|
|
821
|
+
|
|
822
|
+
Advanced patterns for specific cases that require careful implementation.
|
|
823
|
+
|
|
824
|
+
### Stable Event Callbacks with useEffectEvent
|
|
825
|
+
|
|
826
|
+
**Impact:** MEDIUM
|
|
827
|
+
|
|
828
|
+
Use `useEffectEvent` (React 19) to access latest values in effect callbacks without adding them to dependency arrays.
|
|
829
|
+
|
|
830
|
+
**Incorrect:**
|
|
831
|
+
|
|
832
|
+
```tsx
|
|
833
|
+
useEffect(() => {
|
|
834
|
+
const timeout = setTimeout(() => onSearch(query), 300);
|
|
835
|
+
return () => clearTimeout(timeout);
|
|
836
|
+
}, [query, onSearch]); // onSearch causes unnecessary re-runs
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
**Correct (React 19):**
|
|
840
|
+
|
|
841
|
+
```tsx
|
|
842
|
+
import { useEffectEvent } from 'react';
|
|
843
|
+
|
|
844
|
+
const onSearchEvent = useEffectEvent((q: string) => onSearch(q));
|
|
845
|
+
|
|
846
|
+
useEffect(() => {
|
|
847
|
+
const timeout = setTimeout(() => onSearchEvent(query), 300);
|
|
848
|
+
return () => clearTimeout(timeout);
|
|
849
|
+
}, [query]); // No need to include onSearchEvent
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
**Legacy fallback (pre-React 19):**
|
|
853
|
+
|
|
854
|
+
```tsx
|
|
855
|
+
function useLatest<T>(value: T) {
|
|
856
|
+
const ref = useRef(value);
|
|
857
|
+
useEffect(() => {
|
|
858
|
+
ref.current = value;
|
|
859
|
+
}, [value]);
|
|
860
|
+
return ref;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const onSearchRef = useLatest(onSearch);
|
|
864
|
+
useEffect(() => {
|
|
865
|
+
const timeout = setTimeout(() => onSearchRef.current(query), 300);
|
|
866
|
+
return () => clearTimeout(timeout);
|
|
867
|
+
}, [query]);
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
### Stable Event Handler References
|
|
871
|
+
|
|
872
|
+
**Impact:** MEDIUM
|
|
873
|
+
|
|
874
|
+
Use `useEffectEvent` for stable event handler references in effects.
|
|
875
|
+
|
|
876
|
+
**React 19:**
|
|
877
|
+
|
|
878
|
+
```tsx
|
|
879
|
+
import { useEffectEvent } from 'react';
|
|
880
|
+
|
|
881
|
+
function useWindowEvent(event: string, handler: () => void) {
|
|
882
|
+
const onEvent = useEffectEvent(handler);
|
|
883
|
+
|
|
884
|
+
useEffect(() => {
|
|
885
|
+
window.addEventListener(event, onEvent);
|
|
886
|
+
return () => window.removeEventListener(event, onEvent);
|
|
887
|
+
}, [event]);
|
|
888
|
+
}
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
**Legacy fallback:**
|
|
892
|
+
|
|
893
|
+
```tsx
|
|
894
|
+
function useWindowEvent(event: string, handler: () => void) {
|
|
895
|
+
const handlerRef = useRef(handler);
|
|
896
|
+
useEffect(() => {
|
|
897
|
+
handlerRef.current = handler;
|
|
898
|
+
}, [handler]);
|
|
899
|
+
|
|
900
|
+
useEffect(() => {
|
|
901
|
+
const listener = () => handlerRef.current();
|
|
902
|
+
window.addEventListener(event, listener);
|
|
903
|
+
return () => window.removeEventListener(event, listener);
|
|
904
|
+
}, [event]);
|
|
905
|
+
}
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
---
|
|
909
|
+
|
|
910
|
+
## References
|
|
911
|
+
|
|
912
|
+
- [React Compiler](https://react.dev/learn/react-compiler)
|
|
913
|
+
- [TanStack Query](https://tanstack.com/query)
|
|
914
|
+
- [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore)
|
|
915
|
+
- [useEffectEvent RFC](https://react.dev/learn/separating-events-from-effects)
|