@numbered/docs-to-context 0.1.5 → 0.3.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 +29 -12
- package/docs/liquid/README.mdx +59 -0
- package/docs/liquid/a11y-alt-text.mdx +16 -0
- package/docs/liquid/a11y-semantic-html.mdx +19 -0
- package/docs/liquid/alpine-cleanup.mdx +18 -0
- package/docs/liquid/alpine-debounce.mdx +12 -0
- package/docs/liquid/alpine-defer-heavy.mdx +13 -0
- package/docs/liquid/alpine-separate-scopes.mdx +19 -0
- package/docs/liquid/alpine-static-data.mdx +16 -0
- package/docs/liquid/css-critical-inline.mdx +16 -0
- package/docs/liquid/css-prefer-tailwind.mdx +30 -0
- package/docs/liquid/liquid-assign-over-capture.mdx +17 -0
- package/docs/liquid/liquid-avoid-nested-loops.mdx +21 -0
- package/docs/liquid/liquid-break-continue.mdx +23 -0
- package/docs/liquid/liquid-cache-assigns.mdx +16 -0
- package/docs/liquid/liquid-elsif-chain.mdx +25 -0
- package/docs/liquid/liquid-filter-early.mdx +18 -0
- package/docs/liquid/liquid-limit-loops.mdx +17 -0
- package/docs/liquid/liquid-map-join.mdx +16 -0
- package/docs/liquid/liquid-render-over-include.mdx +13 -0
- package/docs/liquid/liquid-snippet-data.mdx +17 -0
- package/docs/liquid/liquid-whitespace-control.mdx +17 -0
- package/docs/liquid/loading-defer-scripts.mdx +13 -0
- package/docs/liquid/loading-lazy-images.mdx +31 -0
- package/docs/liquid/loading-preload-critical.mdx +24 -0
- package/docs/liquid/loading-responsive-images.mdx +23 -0
- package/docs/liquid/schema-blocks-over-settings.mdx +35 -0
- package/docs/liquid/schema-section-settings.mdx +36 -0
- package/docs/react/README.mdx +99 -0
- package/docs/react/advanced-handler-refs.mdx +15 -0
- package/docs/react/advanced-use-latest.mdx +21 -0
- package/docs/react/async-defer-await.mdx +19 -0
- package/docs/react/async-promise-all.mdx +17 -0
- package/docs/react/async-start-early.mdx +25 -0
- package/docs/react/async-suspense-boundaries.mdx +35 -0
- package/docs/react/bundle-barrel-imports.mdx +25 -0
- package/docs/react/bundle-conditional-loading.mdx +21 -0
- package/docs/react/bundle-defer-third-party.mdx +15 -0
- package/docs/react/bundle-dynamic-imports.mdx +15 -0
- package/docs/react/bundle-preload-intent.mdx +24 -0
- package/docs/react/client-event-listeners.mdx +45 -0
- package/docs/react/client-swr-dedup.mdx +40 -0
- package/docs/react/effect-derive-state.mdx +12 -0
- package/docs/react/effect-event-handlers.mdx +16 -0
- package/docs/react/effect-key-reset.mdx +11 -0
- package/docs/react/effect-no-chains.mdx +22 -0
- package/docs/react/effect-notify-parents.mdx +18 -0
- package/docs/react/effect-use-memo.mdx +17 -0
- package/docs/react/js-batch-css.mdx +23 -0
- package/docs/react/js-cache-property.mdx +17 -0
- package/docs/react/js-cache-storage.mdx +29 -0
- package/docs/react/js-combine-iterations.mdx +20 -0
- package/docs/react/js-early-return.mdx +24 -0
- package/docs/react/js-hoist-regexp.mdx +21 -0
- package/docs/react/js-index-maps.mdx +14 -0
- package/docs/react/js-length-check.mdx +12 -0
- package/docs/react/js-loop-min-max.mdx +14 -0
- package/docs/react/js-set-lookups.mdx +12 -0
- package/docs/react/js-tosorted.mdx +15 -0
- package/docs/react/render-activity.mdx +17 -0
- package/docs/react/render-conditional.mdx +11 -0
- package/docs/react/render-content-visibility.mdx +12 -0
- package/docs/react/render-cx-clsx.mdx +12 -0
- package/docs/react/render-hoist-jsx.mdx +16 -0
- package/docs/react/render-hydration-flicker.mdx +25 -0
- package/docs/react/render-svg-precision.mdx +17 -0
- package/docs/react/render-svg-wrapper.mdx +19 -0
- package/docs/react/rerender-defer-reads.mdx +24 -0
- package/docs/react/rerender-derived-state.mdx +18 -0
- package/docs/react/rerender-inline-objects.mdx +18 -0
- package/docs/react/rerender-isolate-state.mdx +36 -0
- package/docs/react/rerender-lazy-init.mdx +19 -0
- package/docs/react/rerender-memo-extract.mdx +26 -0
- package/docs/react/rerender-narrow-deps.mdx +26 -0
- package/docs/react/rerender-transitions.mdx +29 -0
- package/docs/react/rerender-use-client-down.mdx +34 -0
- package/docs/react/server-lru-cache.mdx +24 -0
- package/docs/react/server-parallel-fetching.mdx +24 -0
- package/docs/react/server-react-cache.mdx +16 -0
- package/docs/react/server-rsc-serialization.mdx +17 -0
- package/package.json +2 -1
- package/scripts/extract.ts +2 -2
- package/scripts/generate.ts +9 -2
- package/scripts/inject.ts +92 -25
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Use Blocks for Repeatable Content
|
|
2
|
+
|
|
3
|
+
Use section blocks instead of numbered settings for repeatable items.
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{% comment %} BAD: hardcoded number of items {% endcomment %}
|
|
7
|
+
{% schema %}
|
|
8
|
+
{
|
|
9
|
+
"settings": [
|
|
10
|
+
{ "type": "text", "id": "slide_1_title" },
|
|
11
|
+
{ "type": "image_picker", "id": "slide_1_image" },
|
|
12
|
+
{ "type": "text", "id": "slide_2_title" },
|
|
13
|
+
{ "type": "image_picker", "id": "slide_2_image" }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
{% endschema %}
|
|
17
|
+
|
|
18
|
+
{% comment %} GOOD: dynamic, reorderable blocks {% endcomment %}
|
|
19
|
+
{% schema %}
|
|
20
|
+
{
|
|
21
|
+
"blocks": [
|
|
22
|
+
{
|
|
23
|
+
"type": "slide",
|
|
24
|
+
"name": "Slide",
|
|
25
|
+
"settings": [
|
|
26
|
+
{ "type": "text", "id": "title" },
|
|
27
|
+
{ "type": "image_picker", "id": "image" }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
{% endschema %}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Blocks are reorderable, addable, and removable by editors.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Keep Section Schema Lean
|
|
2
|
+
|
|
3
|
+
Only expose settings that editors actually need. Large schemas slow the theme editor.
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{% comment %} BAD: excessive settings {% endcomment %}
|
|
7
|
+
{% schema %}
|
|
8
|
+
{
|
|
9
|
+
"settings": [
|
|
10
|
+
{ "type": "text", "id": "title", "label": "Title" },
|
|
11
|
+
{ "type": "text", "id": "title_tag", "label": "Title HTML tag" },
|
|
12
|
+
{ "type": "text", "id": "title_class", "label": "Title CSS class" },
|
|
13
|
+
{ "type": "text", "id": "title_style", "label": "Title inline style" }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
{% endschema %}
|
|
17
|
+
|
|
18
|
+
{% comment %} GOOD: only what editors control {% endcomment %}
|
|
19
|
+
{% schema %}
|
|
20
|
+
{
|
|
21
|
+
"settings": [
|
|
22
|
+
{ "type": "text", "id": "title", "label": "Title" },
|
|
23
|
+
{
|
|
24
|
+
"type": "select", "id": "title_size", "label": "Title size",
|
|
25
|
+
"options": [
|
|
26
|
+
{ "value": "sm", "label": "Small" },
|
|
27
|
+
{ "value": "lg", "label": "Large" }
|
|
28
|
+
],
|
|
29
|
+
"default": "lg"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
{% endschema %}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Map setting values to Tailwind classes in the template, not in the schema.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# React & Next.js Best Practices
|
|
2
|
+
|
|
3
|
+
Read this index first. Load only the files relevant to your current task.
|
|
4
|
+
|
|
5
|
+
## 1. Eliminating Waterfalls — CRITICAL
|
|
6
|
+
|
|
7
|
+
| File | Pattern |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `async-defer-await.mdx` | Move await into branches where it's used |
|
|
10
|
+
| `async-promise-all.mdx` | Promise.all for independent operations |
|
|
11
|
+
| `async-start-early.mdx` | Start promises early, await late |
|
|
12
|
+
| `async-suspense-boundaries.mdx` | Suspense to stream content progressively |
|
|
13
|
+
|
|
14
|
+
## 2. Bundle Size — CRITICAL
|
|
15
|
+
|
|
16
|
+
| File | Pattern |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `bundle-barrel-imports.mdx` | Import from source, not barrel files |
|
|
19
|
+
| `bundle-dynamic-imports.mdx` | next/dynamic for heavy components |
|
|
20
|
+
| `bundle-defer-third-party.mdx` | Load analytics/logging after hydration |
|
|
21
|
+
| `bundle-preload-intent.mdx` | Preload on hover/focus/feature-flag |
|
|
22
|
+
| `bundle-conditional-loading.mdx` | Load modules only when feature activates |
|
|
23
|
+
|
|
24
|
+
## 3. Server-Side Performance — HIGH
|
|
25
|
+
|
|
26
|
+
| File | Pattern |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `server-react-cache.mdx` | React.cache() for per-request dedup |
|
|
29
|
+
| `server-lru-cache.mdx` | LRU cache for cross-request data |
|
|
30
|
+
| `server-rsc-serialization.mdx` | Pass only used fields across RSC boundary |
|
|
31
|
+
| `server-parallel-fetching.mdx` | Component composition for parallel fetches |
|
|
32
|
+
|
|
33
|
+
## 4. Client-Side Data Fetching — MEDIUM-HIGH
|
|
34
|
+
|
|
35
|
+
| File | Pattern |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `client-swr-dedup.mdx` | SWR for request dedup and caching |
|
|
38
|
+
| `client-event-listeners.mdx` | Deduplicate global event listeners |
|
|
39
|
+
|
|
40
|
+
## 5. Re-render Optimization — MEDIUM
|
|
41
|
+
|
|
42
|
+
| File | Pattern |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `rerender-use-client-down.mdx` | Push "use client" to smallest leaf |
|
|
45
|
+
| `rerender-isolate-state.mdx` | Isolate state to smallest component |
|
|
46
|
+
| `rerender-inline-objects.mdx` | Avoid inline objects in JSX |
|
|
47
|
+
| `rerender-narrow-deps.mdx` | Use primitives in effect dependencies |
|
|
48
|
+
| `rerender-defer-reads.mdx` | Read dynamic state only in callbacks |
|
|
49
|
+
| `rerender-lazy-init.mdx` | Lazy state initialization for expensive values |
|
|
50
|
+
| `rerender-derived-state.mdx` | Subscribe to derived booleans, not raw values |
|
|
51
|
+
| `rerender-memo-extract.mdx` | Extract to memoized components for early return |
|
|
52
|
+
| `rerender-transitions.mdx` | startTransition for non-urgent updates |
|
|
53
|
+
|
|
54
|
+
## 6. useEffect Rules — MEDIUM
|
|
55
|
+
|
|
56
|
+
| File | Pattern |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `effect-derive-state.mdx` | Calculate during render, not in effects |
|
|
59
|
+
| `effect-use-memo.mdx` | useMemo for expensive calculations |
|
|
60
|
+
| `effect-event-handlers.mdx` | Event logic in handlers, not effects |
|
|
61
|
+
| `effect-key-reset.mdx` | Key prop to reset state, not effects |
|
|
62
|
+
| `effect-no-chains.mdx` | Batch in event handlers, avoid chained effects |
|
|
63
|
+
| `effect-notify-parents.mdx` | Notify parents via handlers, not effects |
|
|
64
|
+
|
|
65
|
+
## 7. Rendering Performance — MEDIUM
|
|
66
|
+
|
|
67
|
+
| File | Pattern |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `render-svg-wrapper.mdx` | Animate wrapper div, not SVG element |
|
|
70
|
+
| `render-content-visibility.mdx` | content-visibility for off-screen items |
|
|
71
|
+
| `render-hoist-jsx.mdx` | Hoist static JSX outside components |
|
|
72
|
+
| `render-hydration-flicker.mdx` | Inline script to prevent hydration flicker |
|
|
73
|
+
| `render-conditional.mdx` | Ternary over && for falsy values |
|
|
74
|
+
| `render-activity.mdx` | Activity component for show/hide |
|
|
75
|
+
| `render-svg-precision.mdx` | SVGO to reduce SVG file size |
|
|
76
|
+
| `render-cx-clsx.mdx` | Skip cx/clsx for single static classes |
|
|
77
|
+
|
|
78
|
+
## 8. JavaScript Patterns — LOW-MEDIUM
|
|
79
|
+
|
|
80
|
+
| File | Pattern |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `js-index-maps.mdx` | Map for O(1) repeated lookups |
|
|
83
|
+
| `js-set-lookups.mdx` | Set for O(1) membership checks |
|
|
84
|
+
| `js-combine-iterations.mdx` | Single loop instead of multiple filter/map |
|
|
85
|
+
| `js-tosorted.mdx` | toSorted() for immutable sorting |
|
|
86
|
+
| `js-loop-min-max.mdx` | Loop for min/max instead of sort |
|
|
87
|
+
| `js-length-check.mdx` | Check length before expensive comparison |
|
|
88
|
+
| `js-batch-css.mdx` | CSS classes over inline style mutations |
|
|
89
|
+
| `js-cache-property.mdx` | Cache deep property access in loops |
|
|
90
|
+
| `js-cache-storage.mdx` | Cache localStorage/sessionStorage reads |
|
|
91
|
+
| `js-early-return.mdx` | Return early to skip unnecessary work |
|
|
92
|
+
| `js-hoist-regexp.mdx` | Hoist RegExp to module scope or useMemo |
|
|
93
|
+
|
|
94
|
+
## 9. Advanced Patterns — LOW
|
|
95
|
+
|
|
96
|
+
| File | Pattern |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `advanced-handler-refs.mdx` | Store event handlers in refs |
|
|
99
|
+
| `advanced-use-latest.mdx` | useLatest for stable callback refs |
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Store Event Handlers in Refs
|
|
2
|
+
|
|
3
|
+
Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
function useWindowEvent(event: string, handler: () => void) {
|
|
7
|
+
const ref = useRef(handler)
|
|
8
|
+
useEffect(() => { ref.current = handler }, [handler])
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const listener = () => ref.current()
|
|
11
|
+
window.addEventListener(event, listener)
|
|
12
|
+
return () => window.removeEventListener(event, listener)
|
|
13
|
+
}, [event])
|
|
14
|
+
}
|
|
15
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# useLatest for Stable Callback Refs
|
|
2
|
+
|
|
3
|
+
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
function useLatest<T>(value: T) {
|
|
7
|
+
const ref = useRef(value)
|
|
8
|
+
useEffect(() => { ref.current = value }, [value])
|
|
9
|
+
return ref
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Usage: stable effect, fresh callback
|
|
13
|
+
function SearchInput({ onSearch }) {
|
|
14
|
+
const [query, setQuery] = useState('')
|
|
15
|
+
const ref = useLatest(onSearch)
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const t = setTimeout(() => ref.current(query), 300)
|
|
18
|
+
return () => clearTimeout(t)
|
|
19
|
+
}, [query])
|
|
20
|
+
}
|
|
21
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Defer Await Until Needed
|
|
2
|
+
|
|
3
|
+
Move `await` into the branch where it's actually used.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD: blocks both branches
|
|
7
|
+
async function handleRequest(userId: string, skip: boolean) {
|
|
8
|
+
const data = await fetchUserData(userId)
|
|
9
|
+
if (skip) return { skipped: true }
|
|
10
|
+
return processUserData(data)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// GOOD: only fetches when needed
|
|
14
|
+
async function handleRequest(userId: string, skip: boolean) {
|
|
15
|
+
if (skip) return { skipped: true }
|
|
16
|
+
const data = await fetchUserData(userId)
|
|
17
|
+
return processUserData(data)
|
|
18
|
+
}
|
|
19
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Promise.all for Independent Operations
|
|
2
|
+
|
|
3
|
+
When async operations have no interdependencies, execute them concurrently.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD: sequential — 3 round trips
|
|
7
|
+
const user = await fetchUser()
|
|
8
|
+
const posts = await fetchPosts()
|
|
9
|
+
const comments = await fetchComments()
|
|
10
|
+
|
|
11
|
+
// GOOD: parallel — 1 round trip
|
|
12
|
+
const [user, posts, comments] = await Promise.all([
|
|
13
|
+
fetchUser(),
|
|
14
|
+
fetchPosts(),
|
|
15
|
+
fetchComments(),
|
|
16
|
+
])
|
|
17
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Start Promises Early, Await Late
|
|
2
|
+
|
|
3
|
+
Start independent operations immediately, even if you don't await them yet.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD: config waits for auth
|
|
7
|
+
export async function GET(request: Request) {
|
|
8
|
+
const session = await auth()
|
|
9
|
+
const config = await fetchConfig()
|
|
10
|
+
const data = await fetchData(session.user.id)
|
|
11
|
+
return Response.json({ data, config })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// GOOD: auth and config start immediately
|
|
15
|
+
export async function GET(request: Request) {
|
|
16
|
+
const sessionPromise = auth()
|
|
17
|
+
const configPromise = fetchConfig()
|
|
18
|
+
const session = await sessionPromise
|
|
19
|
+
const [config, data] = await Promise.all([
|
|
20
|
+
configPromise,
|
|
21
|
+
fetchData(session.user.id),
|
|
22
|
+
])
|
|
23
|
+
return Response.json({ data, config })
|
|
24
|
+
}
|
|
25
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Suspense Boundaries to Stream Content
|
|
2
|
+
|
|
3
|
+
Use Suspense to show wrapper UI immediately while data loads.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD: entire page blocked by data
|
|
7
|
+
async function Page() {
|
|
8
|
+
const data = await fetchData()
|
|
9
|
+
return (
|
|
10
|
+
<div>
|
|
11
|
+
<Header />
|
|
12
|
+
<DataDisplay data={data} />
|
|
13
|
+
<Footer />
|
|
14
|
+
</div>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// GOOD: shell renders immediately, data streams in
|
|
19
|
+
function Page() {
|
|
20
|
+
return (
|
|
21
|
+
<div>
|
|
22
|
+
<Header />
|
|
23
|
+
<Suspense fallback={<Skeleton />}>
|
|
24
|
+
<DataDisplay />
|
|
25
|
+
</Suspense>
|
|
26
|
+
<Footer />
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function DataDisplay() {
|
|
32
|
+
const data = await fetchData()
|
|
33
|
+
return <div>{data.content}</div>
|
|
34
|
+
}
|
|
35
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Avoid Barrel File Imports
|
|
2
|
+
|
|
3
|
+
Import directly from source files instead of barrel files to avoid loading thousands of unused modules.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD: loads entire library (1,583 modules)
|
|
7
|
+
import { Check, X, Menu } from 'lucide-react'
|
|
8
|
+
|
|
9
|
+
// GOOD: loads only 3 modules
|
|
10
|
+
import Check from 'lucide-react/dist/esm/icons/check'
|
|
11
|
+
import X from 'lucide-react/dist/esm/icons/x'
|
|
12
|
+
import Menu from 'lucide-react/dist/esm/icons/menu'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or use `optimizePackageImports` in next.config to keep ergonomic imports:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
module.exports = {
|
|
19
|
+
experimental: {
|
|
20
|
+
optimizePackageImports: ['lucide-react', '@mui/material'],
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Commonly affected: `lucide-react`, `@mui/material`, `@tabler/icons-react`, `react-icons`, `@radix-ui/react-*`, `lodash`, `date-fns`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Conditional Module Loading
|
|
2
|
+
|
|
3
|
+
Load large data or modules only when a feature is activated.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
function AnimationPlayer({ enabled }: { enabled: boolean }) {
|
|
7
|
+
const [frames, setFrames] = useState<Frame[] | null>(null)
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (enabled && !frames && typeof window !== 'undefined') {
|
|
11
|
+
import('./animation-frames.js')
|
|
12
|
+
.then(mod => setFrames(mod.frames))
|
|
13
|
+
}
|
|
14
|
+
}, [enabled, frames])
|
|
15
|
+
|
|
16
|
+
if (!frames) return <Skeleton />
|
|
17
|
+
return <Canvas frames={frames} />
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The `typeof window !== 'undefined'` check prevents bundling for SSR.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Defer Non-Critical Third-Party Libraries
|
|
2
|
+
|
|
3
|
+
Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD: blocks initial bundle
|
|
7
|
+
import { Analytics } from '@vercel/analytics/react'
|
|
8
|
+
|
|
9
|
+
// GOOD: loads after hydration
|
|
10
|
+
import dynamic from 'next/dynamic'
|
|
11
|
+
const Analytics = dynamic(
|
|
12
|
+
() => import('@vercel/analytics/react').then(m => m.Analytics),
|
|
13
|
+
{ ssr: false }
|
|
14
|
+
)
|
|
15
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Dynamic Imports for Heavy Components
|
|
2
|
+
|
|
3
|
+
Use `next/dynamic` to lazy-load large components not needed on initial render.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD: Monaco bundles with main chunk (~300KB)
|
|
7
|
+
import { MonacoEditor } from './monaco-editor'
|
|
8
|
+
|
|
9
|
+
// GOOD: loads on demand
|
|
10
|
+
import dynamic from 'next/dynamic'
|
|
11
|
+
const MonacoEditor = dynamic(
|
|
12
|
+
() => import('./monaco-editor').then(m => m.MonacoEditor),
|
|
13
|
+
{ ssr: false }
|
|
14
|
+
)
|
|
15
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Preload Based on User Intent
|
|
2
|
+
|
|
3
|
+
Preload heavy bundles before they're needed to reduce perceived latency.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
function EditorButton({ onClick }: { onClick: () => void }) {
|
|
7
|
+
const preload = () => { void import('./monaco-editor') }
|
|
8
|
+
return (
|
|
9
|
+
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
|
|
10
|
+
Open Editor
|
|
11
|
+
</button>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Also works with feature flags:
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (flags.editorEnabled) {
|
|
21
|
+
void import('./monaco-editor').then(mod => mod.init())
|
|
22
|
+
}
|
|
23
|
+
}, [flags.editorEnabled])
|
|
24
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Deduplicate Global Event Listeners
|
|
2
|
+
|
|
3
|
+
N component instances should share 1 listener, not register N listeners.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD: N instances = N listeners
|
|
7
|
+
function useKeyboardShortcut(key: string, callback: () => void) {
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const handler = (e: KeyboardEvent) => {
|
|
10
|
+
if (e.metaKey && e.key === key) callback()
|
|
11
|
+
}
|
|
12
|
+
window.addEventListener('keydown', handler)
|
|
13
|
+
return () => window.removeEventListener('keydown', handler)
|
|
14
|
+
}, [key, callback])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// GOOD: N instances = 1 listener via useSWRSubscription
|
|
18
|
+
import useSWRSubscription from 'swr/subscription'
|
|
19
|
+
|
|
20
|
+
const keyCallbacks = new Map<string, Set<() => void>>()
|
|
21
|
+
|
|
22
|
+
function useKeyboardShortcut(key: string, callback: () => void) {
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!keyCallbacks.has(key)) keyCallbacks.set(key, new Set())
|
|
25
|
+
keyCallbacks.get(key)!.add(callback)
|
|
26
|
+
return () => {
|
|
27
|
+
const set = keyCallbacks.get(key)
|
|
28
|
+
if (set) {
|
|
29
|
+
set.delete(callback)
|
|
30
|
+
if (set.size === 0) keyCallbacks.delete(key)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}, [key, callback])
|
|
34
|
+
|
|
35
|
+
useSWRSubscription('global-keydown', () => {
|
|
36
|
+
const handler = (e: KeyboardEvent) => {
|
|
37
|
+
if (e.metaKey && keyCallbacks.has(e.key)) {
|
|
38
|
+
keyCallbacks.get(e.key)!.forEach(cb => cb())
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
window.addEventListener('keydown', handler)
|
|
42
|
+
return () => window.removeEventListener('keydown', handler)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# SWR for Automatic Request Deduplication
|
|
2
|
+
|
|
3
|
+
SWR enables request deduplication, caching, and revalidation across component instances.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD: no deduplication, each instance fetches
|
|
7
|
+
function UserList() {
|
|
8
|
+
const [users, setUsers] = useState([])
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
fetch('/api/users').then(r => r.json()).then(setUsers)
|
|
11
|
+
}, [])
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// GOOD: multiple instances share one request
|
|
15
|
+
import useSWR from 'swr'
|
|
16
|
+
|
|
17
|
+
function UserList() {
|
|
18
|
+
const { data: users } = useSWR('/api/users', fetcher)
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For immutable data:
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
const { data } = useSWR('/api/config', fetcher, {
|
|
26
|
+
revalidateOnFocus: false,
|
|
27
|
+
revalidateOnReconnect: false,
|
|
28
|
+
})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For mutations:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import useSWRMutation from 'swr/mutation'
|
|
35
|
+
|
|
36
|
+
function UpdateButton() {
|
|
37
|
+
const { trigger } = useSWRMutation('/api/user', updateUser)
|
|
38
|
+
return <button onClick={() => trigger()}>Update</button>
|
|
39
|
+
}
|
|
40
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Derive State During Render
|
|
2
|
+
|
|
3
|
+
Don't use effects for derived state. Calculate during render instead.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD: redundant state + unnecessary effect
|
|
7
|
+
const [fullName, setFullName] = useState('')
|
|
8
|
+
useEffect(() => { setFullName(first + ' ' + last) }, [first, last])
|
|
9
|
+
|
|
10
|
+
// GOOD: calculate during render
|
|
11
|
+
const fullName = first + ' ' + last
|
|
12
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Handle Events in Handlers, Not Effects
|
|
2
|
+
|
|
3
|
+
Event logic belongs in event handlers, not effects.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD: event logic in effect
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (product.isInCart) showNotification(`Added ${product.name}!`)
|
|
9
|
+
}, [product])
|
|
10
|
+
|
|
11
|
+
// GOOD: event logic in handler
|
|
12
|
+
function handleBuy() {
|
|
13
|
+
addToCart(product)
|
|
14
|
+
showNotification(`Added ${product.name}!`)
|
|
15
|
+
}
|
|
16
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Reset State with Key Prop
|
|
2
|
+
|
|
3
|
+
Don't use effects to reset state on prop change. Use the `key` prop.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD: effect to reset state
|
|
7
|
+
useEffect(() => { setComment('') }, [userId])
|
|
8
|
+
|
|
9
|
+
// GOOD: key prop resets component state
|
|
10
|
+
<Profile userId={userId} key={userId} />
|
|
11
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Avoid Chained Effects
|
|
2
|
+
|
|
3
|
+
Cascading effects cause multiple re-renders. Batch updates in event handlers instead.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD: cascading effects
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (card?.gold) setGoldCardCount(c => c + 1)
|
|
9
|
+
}, [card])
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (goldCardCount > 3) { setRound(r => r + 1); setGoldCardCount(0) }
|
|
12
|
+
}, [goldCardCount])
|
|
13
|
+
|
|
14
|
+
// GOOD: batch in event handler
|
|
15
|
+
function handlePlaceCard(nextCard) {
|
|
16
|
+
setCard(nextCard)
|
|
17
|
+
if (nextCard.gold) {
|
|
18
|
+
if (goldCardCount < 3) setGoldCardCount(goldCardCount + 1)
|
|
19
|
+
else { setGoldCardCount(0); setRound(round + 1) }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Notify Parents via Handlers, Not Effects
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
// BAD: effect to notify parent
|
|
5
|
+
useEffect(() => { onChange(isOn) }, [isOn, onChange])
|
|
6
|
+
|
|
7
|
+
// GOOD: update together in handler
|
|
8
|
+
function handleClick() {
|
|
9
|
+
const next = !isOn
|
|
10
|
+
setIsOn(next)
|
|
11
|
+
onChange(next)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// BEST: lift state to parent (controlled component)
|
|
15
|
+
function Toggle({ isOn, onChange }) {
|
|
16
|
+
return <button onClick={() => onChange(!isOn)}>{isOn ? 'ON' : 'OFF'}</button>
|
|
17
|
+
}
|
|
18
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# useMemo for Expensive Calculations
|
|
2
|
+
|
|
3
|
+
Don't use effects for derived data. Use useMemo.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD: effect for derived data
|
|
7
|
+
const [visibleTodos, setVisibleTodos] = useState([])
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
setVisibleTodos(getFilteredTodos(todos, filter))
|
|
10
|
+
}, [todos, filter])
|
|
11
|
+
|
|
12
|
+
// GOOD: useMemo
|
|
13
|
+
const visibleTodos = useMemo(
|
|
14
|
+
() => getFilteredTodos(todos, filter),
|
|
15
|
+
[todos, filter]
|
|
16
|
+
)
|
|
17
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Batch DOM CSS Changes via Classes
|
|
2
|
+
|
|
3
|
+
Avoid changing styles one property at a time. Group via classes to minimize browser reflows.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD: multiple reflows
|
|
7
|
+
el.style.width = '100px'
|
|
8
|
+
el.style.height = '200px'
|
|
9
|
+
el.style.backgroundColor = 'blue'
|
|
10
|
+
|
|
11
|
+
// GOOD: single reflow
|
|
12
|
+
el.classList.add('highlighted-box')
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
In React: prefer `className` over inline `style` mutations.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
// BAD
|
|
19
|
+
<div ref={ref} /> + ref.current.style.width = '100px'
|
|
20
|
+
|
|
21
|
+
// GOOD
|
|
22
|
+
<div className={isHighlighted ? 'highlighted-box' : ''} />
|
|
23
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Cache Property Access in Loops
|
|
2
|
+
|
|
3
|
+
Cache object property lookups in hot paths.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// BAD: 3 lookups x N iterations
|
|
7
|
+
for (let i = 0; i < arr.length; i++) {
|
|
8
|
+
process(obj.config.settings.value)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// GOOD: 1 lookup total
|
|
12
|
+
const value = obj.config.settings.value
|
|
13
|
+
const len = arr.length
|
|
14
|
+
for (let i = 0; i < len; i++) {
|
|
15
|
+
process(value)
|
|
16
|
+
}
|
|
17
|
+
```
|