@jgamaraalv/ts-dev-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +24 -0
- package/.claude-plugin/plugin.json +24 -0
- package/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/agents/accessibility-pro.md +139 -0
- package/agents/api-builder.md +110 -0
- package/agents/code-reviewer.md +190 -0
- package/agents/database-expert.md +138 -0
- package/agents/debugger.md +241 -0
- package/agents/docker-expert.md +51 -0
- package/agents/multi-agent-coordinator.md +378 -0
- package/agents/nextjs-expert.md +136 -0
- package/agents/performance-engineer.md +138 -0
- package/agents/playwright-expert.md +126 -0
- package/agents/react-specialist.md +97 -0
- package/agents/security-scanner.md +105 -0
- package/agents/test-generator.md +221 -0
- package/agents/typescript-pro.md +253 -0
- package/agents/ux-optimizer.md +93 -0
- package/docs/rules/orchestration.md.template +126 -0
- package/package.json +28 -0
- package/skills/bullmq/SKILL.md +225 -0
- package/skills/bullmq/references/flows-and-schedulers.md +186 -0
- package/skills/bullmq/references/job-types-and-options.md +163 -0
- package/skills/bullmq/references/patterns.md +273 -0
- package/skills/bullmq/references/production.md +308 -0
- package/skills/composition-patterns/SKILL.md +58 -0
- package/skills/composition-patterns/references/architecture-avoid-boolean-props.md +87 -0
- package/skills/composition-patterns/references/architecture-compound-components.md +107 -0
- package/skills/composition-patterns/references/patterns-children-over-render-props.md +77 -0
- package/skills/composition-patterns/references/patterns-explicit-variants.md +87 -0
- package/skills/composition-patterns/references/react19-no-forwardref.md +37 -0
- package/skills/composition-patterns/references/state-context-interface.md +194 -0
- package/skills/composition-patterns/references/state-decouple-implementation.md +96 -0
- package/skills/composition-patterns/references/state-lift-state.md +126 -0
- package/skills/conventional-commits/SKILL.md +148 -0
- package/skills/docker/SKILL.md +55 -0
- package/skills/docker/references/compose-configs.md +95 -0
- package/skills/docker/references/monorepo-dockerfile.md +111 -0
- package/skills/drizzle-pg/SKILL.md +202 -0
- package/skills/drizzle-pg/references/advanced.md +299 -0
- package/skills/drizzle-pg/references/migrations.md +214 -0
- package/skills/drizzle-pg/references/queries.md +321 -0
- package/skills/drizzle-pg/references/relations.md +272 -0
- package/skills/drizzle-pg/references/schema-pg.md +256 -0
- package/skills/drizzle-pg/references/sql-operator.md +215 -0
- package/skills/fastify-best-practices/SKILL.md +143 -0
- package/skills/fastify-best-practices/references/hooks-and-lifecycle.md +122 -0
- package/skills/fastify-best-practices/references/plugins-and-encapsulation.md +137 -0
- package/skills/fastify-best-practices/references/request-reply-errors.md +189 -0
- package/skills/fastify-best-practices/references/routes-and-handlers.md +134 -0
- package/skills/fastify-best-practices/references/server-and-options.md +127 -0
- package/skills/fastify-best-practices/references/typescript-and-logging.md +223 -0
- package/skills/fastify-best-practices/references/validation-and-serialization.md +190 -0
- package/skills/ioredis/SKILL.md +51 -0
- package/skills/ioredis/references/advanced-patterns.md +312 -0
- package/skills/ioredis/references/cluster-sentinel.md +280 -0
- package/skills/ioredis/references/connection-options.md +187 -0
- package/skills/ioredis/references/core-api.md +179 -0
- package/skills/nextjs-best-practices/SKILL.md +194 -0
- package/skills/nextjs-best-practices/references/async-patterns.md +84 -0
- package/skills/nextjs-best-practices/references/bundling.md +192 -0
- package/skills/nextjs-best-practices/references/data-patterns.md +310 -0
- package/skills/nextjs-best-practices/references/debug-tricks.md +127 -0
- package/skills/nextjs-best-practices/references/directives.md +74 -0
- package/skills/nextjs-best-practices/references/error-handling.md +237 -0
- package/skills/nextjs-best-practices/references/file-conventions.md +152 -0
- package/skills/nextjs-best-practices/references/font.md +175 -0
- package/skills/nextjs-best-practices/references/functions.md +116 -0
- package/skills/nextjs-best-practices/references/hydration-error.md +86 -0
- package/skills/nextjs-best-practices/references/image.md +184 -0
- package/skills/nextjs-best-practices/references/metadata.md +305 -0
- package/skills/nextjs-best-practices/references/parallel-routes.md +299 -0
- package/skills/nextjs-best-practices/references/route-handlers.md +154 -0
- package/skills/nextjs-best-practices/references/rsc-boundaries.md +168 -0
- package/skills/nextjs-best-practices/references/runtime-selection.md +40 -0
- package/skills/nextjs-best-practices/references/scripts.md +148 -0
- package/skills/nextjs-best-practices/references/self-hosting.md +210 -0
- package/skills/nextjs-best-practices/references/suspense-boundaries.md +67 -0
- package/skills/owasp-security-review/SKILL.md +98 -0
- package/skills/owasp-security-review/references/a01-broken-access-control.md +78 -0
- package/skills/owasp-security-review/references/a02-security-misconfiguration.md +81 -0
- package/skills/owasp-security-review/references/a03-supply-chain-failures.md +65 -0
- package/skills/owasp-security-review/references/a04-cryptographic-failures.md +82 -0
- package/skills/owasp-security-review/references/a05-injection.md +106 -0
- package/skills/owasp-security-review/references/a06-insecure-design.md +76 -0
- package/skills/owasp-security-review/references/a07-authentication-failures.md +83 -0
- package/skills/owasp-security-review/references/a08-integrity-failures.md +72 -0
- package/skills/owasp-security-review/references/a09-logging-alerting-failures.md +76 -0
- package/skills/owasp-security-review/references/a10-exceptional-conditions.md +131 -0
- package/skills/postgresql/SKILL.md +50 -0
- package/skills/postgresql/references/ddl-schema.md +300 -0
- package/skills/postgresql/references/indexes.md +257 -0
- package/skills/postgresql/references/jsonb.md +261 -0
- package/skills/postgresql/references/performance.md +291 -0
- package/skills/postgresql/references/psql-cli.md +153 -0
- package/skills/postgresql/references/queries.md +287 -0
- package/skills/postgresql/references/transactions.md +280 -0
- package/skills/react-best-practices/SKILL.md +110 -0
- package/skills/react-best-practices/references/advanced-patterns.md +91 -0
- package/skills/react-best-practices/references/async-patterns.md +233 -0
- package/skills/react-best-practices/references/bundle-optimization.md +201 -0
- package/skills/react-best-practices/references/client-patterns.md +178 -0
- package/skills/react-best-practices/references/js-performance.md +210 -0
- package/skills/react-best-practices/references/rendering-performance.md +209 -0
- package/skills/react-best-practices/references/rerender-optimization.md +316 -0
- package/skills/react-best-practices/references/server-performance.md +274 -0
- package/skills/service-worker/SKILL.md +195 -0
- package/skills/service-worker/references/api-reference.md +114 -0
- package/skills/service-worker/references/caching-strategies.md +202 -0
- package/skills/service-worker/references/push-and-sync.md +261 -0
- package/skills/typescript-conventions/SKILL.md +51 -0
- package/skills/ui-ux-guidelines/SKILL.md +105 -0
- package/skills/ui-ux-guidelines/references/accessibility-and-interaction.md +74 -0
- package/skills/ui-ux-guidelines/references/forms-content-checklist.md +126 -0
- package/skills/ui-ux-guidelines/references/layout-typography-animation.md +95 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# Re-render Optimization
|
|
2
|
+
|
|
3
|
+
Reduce unnecessary React re-renders through better state management and memoization.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Defer State Reads to Usage Point](#defer-state-reads-to-usage-point)
|
|
8
|
+
- [Narrow Effect Dependencies](#narrow-effect-dependencies)
|
|
9
|
+
- [Calculate Derived State During Rendering](#calculate-derived-state-during-rendering)
|
|
10
|
+
- [Use Functional setState Updates](#use-functional-setstate-updates)
|
|
11
|
+
- [Hoist Default Non-primitive Props](#hoist-default-non-primitive-props)
|
|
12
|
+
- [Extract to Memoized Components](#extract-to-memoized-components)
|
|
13
|
+
- [Put Interaction Logic in Event Handlers](#put-interaction-logic-in-event-handlers)
|
|
14
|
+
- [Use useRef for Transient Values](#use-useref-for-transient-values)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Defer State Reads to Usage Point
|
|
19
|
+
|
|
20
|
+
Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
|
|
21
|
+
|
|
22
|
+
**Incorrect (subscribes to all searchParams changes):**
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
function ShareButton({ chatId }: { chatId: string }) {
|
|
26
|
+
const searchParams = useSearchParams();
|
|
27
|
+
|
|
28
|
+
const handleShare = () => {
|
|
29
|
+
const ref = searchParams.get("ref");
|
|
30
|
+
shareChat(chatId, { ref });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return <button onClick={handleShare}>Share</button>;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Correct (reads on demand, no subscription):**
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
function ShareButton({ chatId }: { chatId: string }) {
|
|
41
|
+
const handleShare = () => {
|
|
42
|
+
const params = new URLSearchParams(window.location.search);
|
|
43
|
+
const ref = params.get("ref");
|
|
44
|
+
shareChat(chatId, { ref });
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return <button onClick={handleShare}>Share</button>;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Narrow Effect Dependencies
|
|
54
|
+
|
|
55
|
+
Specify primitive dependencies instead of objects to minimize effect re-runs.
|
|
56
|
+
|
|
57
|
+
**Incorrect (re-runs on any user field change):**
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
console.log(user.id);
|
|
62
|
+
}, [user]);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Correct (re-runs only when id changes):**
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
console.log(user.id);
|
|
70
|
+
}, [user.id]);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**For derived state, compute outside effect:**
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
// Incorrect: runs on width=767, 766, 765...
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (width < 768) enableMobileMode();
|
|
79
|
+
}, [width]);
|
|
80
|
+
|
|
81
|
+
// Correct: runs only on boolean transition
|
|
82
|
+
const isMobile = width < 768;
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (isMobile) enableMobileMode();
|
|
85
|
+
}, [isMobile]);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Calculate Derived State During Rendering
|
|
91
|
+
|
|
92
|
+
If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render.
|
|
93
|
+
|
|
94
|
+
**Incorrect (redundant state and effect):**
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
function Form() {
|
|
98
|
+
const [firstName, setFirstName] = useState("First");
|
|
99
|
+
const [lastName, setLastName] = useState("Last");
|
|
100
|
+
const [fullName, setFullName] = useState("");
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
setFullName(firstName + " " + lastName);
|
|
104
|
+
}, [firstName, lastName]);
|
|
105
|
+
|
|
106
|
+
return <p>{fullName}</p>;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Correct (derive during render):**
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
function Form() {
|
|
114
|
+
const [firstName, setFirstName] = useState("First");
|
|
115
|
+
const [lastName, setLastName] = useState("Last");
|
|
116
|
+
const fullName = firstName + " " + lastName;
|
|
117
|
+
|
|
118
|
+
return <p>{fullName}</p>;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Reference: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Use Functional setState Updates
|
|
127
|
+
|
|
128
|
+
When updating state based on the current state value, use the functional update form. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
|
|
129
|
+
|
|
130
|
+
**Incorrect (requires state as dependency):**
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
function TodoList() {
|
|
134
|
+
const [items, setItems] = useState(initialItems);
|
|
135
|
+
|
|
136
|
+
const addItems = useCallback(
|
|
137
|
+
(newItems: Item[]) => {
|
|
138
|
+
setItems([...items, ...newItems]);
|
|
139
|
+
},
|
|
140
|
+
[items], // Recreated on every items change
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
return <ItemsEditor items={items} onAdd={addItems} />;
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Correct (stable callbacks, no stale closures):**
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
function TodoList() {
|
|
151
|
+
const [items, setItems] = useState(initialItems);
|
|
152
|
+
|
|
153
|
+
const addItems = useCallback((newItems: Item[]) => {
|
|
154
|
+
setItems((curr) => [...curr, ...newItems]);
|
|
155
|
+
}, []); // No dependencies needed
|
|
156
|
+
|
|
157
|
+
const removeItem = useCallback((id: string) => {
|
|
158
|
+
setItems((curr) => curr.filter((item) => item.id !== id));
|
|
159
|
+
}, []); // Safe and stable
|
|
160
|
+
|
|
161
|
+
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**When to use:** Any setState depending on current state, inside useCallback/useMemo, event handlers, async operations. **When direct updates are fine:** Setting state to a static value (`setCount(0)`), setting from props/arguments only.
|
|
166
|
+
|
|
167
|
+
**Note:** React Compiler can optimize some cases automatically, but functional updates are still recommended for correctness.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Hoist Default Non-primitive Props
|
|
172
|
+
|
|
173
|
+
When memoized components have default non-primitive parameter values, new instances are created on every re-render, breaking memoization.
|
|
174
|
+
|
|
175
|
+
**Incorrect (`onClick` has different values on every rerender):**
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
|
|
179
|
+
// ...
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Correct (stable default value):**
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
const NOOP = () => {};
|
|
187
|
+
|
|
188
|
+
const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
|
|
189
|
+
// ...
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Extract to Memoized Components
|
|
196
|
+
|
|
197
|
+
Extract expensive work into memoized components to enable early returns before computation.
|
|
198
|
+
|
|
199
|
+
**Incorrect (computes avatar even when loading):**
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
function Profile({ user, loading }: Props) {
|
|
203
|
+
const avatar = useMemo(() => {
|
|
204
|
+
const id = computeAvatarId(user);
|
|
205
|
+
return <Avatar id={id} />;
|
|
206
|
+
}, [user]);
|
|
207
|
+
|
|
208
|
+
if (loading) return <Skeleton />;
|
|
209
|
+
return <div>{avatar}</div>;
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Correct (skips computation when loading):**
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
|
|
217
|
+
const id = useMemo(() => computeAvatarId(user), [user]);
|
|
218
|
+
return <Avatar id={id} />;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
function Profile({ user, loading }: Props) {
|
|
222
|
+
if (loading) return <Skeleton />;
|
|
223
|
+
return (
|
|
224
|
+
<div>
|
|
225
|
+
<UserAvatar user={user} />
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Note:** React Compiler automatically optimizes re-renders, making manual `memo()`/`useMemo()` unnecessary.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Put Interaction Logic in Event Handlers
|
|
236
|
+
|
|
237
|
+
If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect.
|
|
238
|
+
|
|
239
|
+
**Incorrect (event modeled as state + effect):**
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
function Form() {
|
|
243
|
+
const [submitted, setSubmitted] = useState(false);
|
|
244
|
+
const theme = useContext(ThemeContext);
|
|
245
|
+
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
if (submitted) {
|
|
248
|
+
post("/api/register");
|
|
249
|
+
showToast("Registered", theme);
|
|
250
|
+
}
|
|
251
|
+
}, [submitted, theme]);
|
|
252
|
+
|
|
253
|
+
return <button onClick={() => setSubmitted(true)}>Submit</button>;
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Correct (do it in the handler):**
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
function Form() {
|
|
261
|
+
const theme = useContext(ThemeContext);
|
|
262
|
+
|
|
263
|
+
function handleSubmit() {
|
|
264
|
+
post("/api/register");
|
|
265
|
+
showToast("Registered", theme);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return <button onClick={handleSubmit}>Submit</button>;
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Reference: [Should this code move to an event handler?](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Use useRef for Transient Values
|
|
277
|
+
|
|
278
|
+
When a value changes frequently and you don't want a re-render on every update (mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`.
|
|
279
|
+
|
|
280
|
+
**Incorrect (renders every update):**
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
function Tracker() {
|
|
284
|
+
const [lastX, setLastX] = useState(0);
|
|
285
|
+
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
const onMove = (e: MouseEvent) => setLastX(e.clientX);
|
|
288
|
+
window.addEventListener("mousemove", onMove);
|
|
289
|
+
return () => window.removeEventListener("mousemove", onMove);
|
|
290
|
+
}, []);
|
|
291
|
+
|
|
292
|
+
return <div style={{ position: "fixed", top: 0, left: lastX, width: 8, height: 8 }} />;
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Correct (no re-render for tracking):**
|
|
297
|
+
|
|
298
|
+
```tsx
|
|
299
|
+
function Tracker() {
|
|
300
|
+
const lastXRef = useRef(0);
|
|
301
|
+
const dotRef = useRef<HTMLDivElement>(null);
|
|
302
|
+
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
const onMove = (e: MouseEvent) => {
|
|
305
|
+
lastXRef.current = e.clientX;
|
|
306
|
+
if (dotRef.current) {
|
|
307
|
+
dotRef.current.style.transform = `translateX(${e.clientX}px)`;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
window.addEventListener("mousemove", onMove);
|
|
311
|
+
return () => window.removeEventListener("mousemove", onMove);
|
|
312
|
+
}, []);
|
|
313
|
+
|
|
314
|
+
return <div ref={dotRef} style={{ position: "fixed", top: 0, width: 8, height: 8 }} />;
|
|
315
|
+
}
|
|
316
|
+
```
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Server-Side Performance
|
|
2
|
+
|
|
3
|
+
Optimize RSC data fetching, caching, and serialization.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Use after() for Non-Blocking Operations](#use-after-for-non-blocking-operations)
|
|
8
|
+
- [Authenticate Server Actions](#authenticate-server-actions)
|
|
9
|
+
- [Cross-Request LRU Caching](#cross-request-lru-caching)
|
|
10
|
+
- [Per-Request Deduplication with React.cache()](#per-request-deduplication-with-reactcache)
|
|
11
|
+
- [Avoid Duplicate Serialization in RSC Props](#avoid-duplicate-serialization-in-rsc-props)
|
|
12
|
+
- [Parallel Data Fetching with Component Composition](#parallel-data-fetching-with-component-composition)
|
|
13
|
+
- [Minimize Serialization at RSC Boundaries](#minimize-serialization-at-rsc-boundaries)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Use after() for Non-Blocking Operations
|
|
18
|
+
|
|
19
|
+
Use Next.js's `after()` to schedule work that should execute after a response is sent. Prevents logging, analytics, and other side effects from blocking the response.
|
|
20
|
+
|
|
21
|
+
**Incorrect (blocks response):**
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
export async function POST(request: Request) {
|
|
25
|
+
await updateDatabase(request);
|
|
26
|
+
const userAgent = request.headers.get("user-agent") || "unknown";
|
|
27
|
+
await logUserAction({ userAgent }); // Blocks response
|
|
28
|
+
return new Response(JSON.stringify({ status: "success" }));
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Correct (non-blocking):**
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { after } from "next/server";
|
|
36
|
+
import { headers, cookies } from "next/headers";
|
|
37
|
+
|
|
38
|
+
export async function POST(request: Request) {
|
|
39
|
+
await updateDatabase(request);
|
|
40
|
+
|
|
41
|
+
after(async () => {
|
|
42
|
+
const userAgent = (await headers()).get("user-agent") || "unknown";
|
|
43
|
+
const sessionCookie = (await cookies()).get("session-id")?.value || "anonymous";
|
|
44
|
+
logUserAction({ sessionCookie, userAgent });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return new Response(JSON.stringify({ status: "success" }));
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Common use cases: analytics tracking, audit logging, notifications, cache invalidation.
|
|
52
|
+
|
|
53
|
+
`after()` runs even if the response fails or redirects. Works in Server Actions, Route Handlers, and Server Components.
|
|
54
|
+
|
|
55
|
+
Reference: [next/server after()](https://nextjs.org/docs/app/api-reference/functions/after)
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Authenticate Server Actions
|
|
60
|
+
|
|
61
|
+
Server Actions (`"use server"`) are exposed as public endpoints. Always verify authentication and authorization **inside** each Server Action -- do not rely solely on middleware or layout guards.
|
|
62
|
+
|
|
63
|
+
**Incorrect (no authentication check):**
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
"use server";
|
|
67
|
+
|
|
68
|
+
export async function deleteUser(userId: string) {
|
|
69
|
+
await db.user.delete({ where: { id: userId } });
|
|
70
|
+
return { success: true };
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Correct (authentication + authorization + validation):**
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
"use server";
|
|
78
|
+
|
|
79
|
+
import { verifySession } from "@/lib/auth";
|
|
80
|
+
import { z } from "zod";
|
|
81
|
+
|
|
82
|
+
const updateProfileSchema = z.object({
|
|
83
|
+
userId: z.string().uuid(),
|
|
84
|
+
name: z.string().min(1).max(100),
|
|
85
|
+
email: z.string().email(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export async function updateProfile(data: unknown) {
|
|
89
|
+
const validated = updateProfileSchema.parse(data);
|
|
90
|
+
const session = await verifySession();
|
|
91
|
+
if (!session) throw new Error("Unauthorized");
|
|
92
|
+
if (session.user.id !== validated.userId) throw new Error("Can only update own profile");
|
|
93
|
+
|
|
94
|
+
await db.user.update({
|
|
95
|
+
where: { id: validated.userId },
|
|
96
|
+
data: { name: validated.name, email: validated.email },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return { success: true };
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Reference: [Next.js Authentication Guide](https://nextjs.org/docs/app/guides/authentication)
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Cross-Request LRU Caching
|
|
108
|
+
|
|
109
|
+
`React.cache()` only works within one request. For data shared across sequential requests, use an LRU cache.
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { LRUCache } from "lru-cache";
|
|
113
|
+
|
|
114
|
+
const cache = new LRUCache<string, any>({
|
|
115
|
+
max: 1000,
|
|
116
|
+
ttl: 5 * 60 * 1000, // 5 minutes
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export async function getUser(id: string) {
|
|
120
|
+
const cached = cache.get(id);
|
|
121
|
+
if (cached) return cached;
|
|
122
|
+
|
|
123
|
+
const user = await db.user.findUnique({ where: { id } });
|
|
124
|
+
cache.set(id, user);
|
|
125
|
+
return user;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute), LRU caching is especially effective because multiple concurrent requests share the same function instance. In traditional serverless, consider Redis for cross-process caching.
|
|
130
|
+
|
|
131
|
+
Reference: [node-lru-cache](https://github.com/isaacs/node-lru-cache)
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Per-Request Deduplication with React.cache()
|
|
136
|
+
|
|
137
|
+
Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { cache } from "react";
|
|
141
|
+
|
|
142
|
+
export const getCurrentUser = cache(async () => {
|
|
143
|
+
const session = await auth();
|
|
144
|
+
if (!session?.user?.id) return null;
|
|
145
|
+
return await db.user.findUnique({ where: { id: session.user.id } });
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Avoid inline objects as arguments** -- `React.cache()` uses `Object.is` equality. Inline objects always create new references, causing cache misses.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// Incorrect (always cache miss)
|
|
153
|
+
const getUser = cache(async (params: { uid: number }) => {
|
|
154
|
+
/* ... */
|
|
155
|
+
});
|
|
156
|
+
getUser({ uid: 1 });
|
|
157
|
+
getUser({ uid: 1 }); // Miss -- new object reference
|
|
158
|
+
|
|
159
|
+
// Correct (cache hit)
|
|
160
|
+
const getUser = cache(async (uid: number) => {
|
|
161
|
+
/* ... */
|
|
162
|
+
});
|
|
163
|
+
getUser(1);
|
|
164
|
+
getUser(1); // Hit -- same primitive value
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Next.js note:** `fetch` is automatically deduplicated within a single request. `React.cache()` is still essential for database queries, auth checks, heavy computations, and any non-fetch async work.
|
|
168
|
+
|
|
169
|
+
Reference: [React.cache documentation](https://react.dev/reference/react/cache)
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Avoid Duplicate Serialization in RSC Props
|
|
174
|
+
|
|
175
|
+
RSC-to-client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations in the client, not server.
|
|
176
|
+
|
|
177
|
+
**Incorrect (duplicates array):**
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
// RSC: sends 6 strings (2 arrays x 3 items)
|
|
181
|
+
<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Correct (sends 3 strings):**
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
// RSC: send once
|
|
188
|
+
<ClientList usernames={usernames} />;
|
|
189
|
+
|
|
190
|
+
// Client: transform there
|
|
191
|
+
("use client");
|
|
192
|
+
const sorted = useMemo(() => [...usernames].sort(), [usernames]);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Operations that break deduplication (create new references): `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`, `{...obj}`, `structuredClone()`.
|
|
196
|
+
|
|
197
|
+
**Exception:** Pass derived data when transformation is expensive or client doesn't need the original.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Parallel Data Fetching with Component Composition
|
|
202
|
+
|
|
203
|
+
React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
|
|
204
|
+
|
|
205
|
+
**Incorrect (Sidebar waits for Page's fetch):**
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
export default async function Page() {
|
|
209
|
+
const header = await fetchHeader();
|
|
210
|
+
return (
|
|
211
|
+
<div>
|
|
212
|
+
<div>{header}</div>
|
|
213
|
+
<Sidebar />
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Correct (both fetch simultaneously):**
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
async function Header() {
|
|
223
|
+
const data = await fetchHeader();
|
|
224
|
+
return <div>{data}</div>;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function Sidebar() {
|
|
228
|
+
const items = await fetchSidebarItems();
|
|
229
|
+
return <nav>{items.map(renderItem)}</nav>;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export default function Page() {
|
|
233
|
+
return (
|
|
234
|
+
<div>
|
|
235
|
+
<Header />
|
|
236
|
+
<Sidebar />
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Minimize Serialization at RSC Boundaries
|
|
245
|
+
|
|
246
|
+
The RSC/Client boundary serializes all object properties into the HTML response. Only pass fields that the client actually uses.
|
|
247
|
+
|
|
248
|
+
**Incorrect (serializes all 50 fields):**
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
async function Page() {
|
|
252
|
+
const user = await fetchUser(); // 50 fields
|
|
253
|
+
return <Profile user={user} />;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
("use client");
|
|
257
|
+
function Profile({ user }: { user: User }) {
|
|
258
|
+
return <div>{user.name}</div>; // uses 1 field
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Correct (serializes only 1 field):**
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
async function Page() {
|
|
266
|
+
const user = await fetchUser();
|
|
267
|
+
return <Profile name={user.name} />;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
("use client");
|
|
271
|
+
function Profile({ name }: { name: string }) {
|
|
272
|
+
return <div>{name}</div>;
|
|
273
|
+
}
|
|
274
|
+
```
|