@pilllesss/yorn 1.0.182 → 1.0.183

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.

Potentially problematic release.


This version of @pilllesss/yorn might be problematic. Click here for more details.

Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/providers/data/.manifest.json +1 -1
  3. package/dist/skills/code-review/LICENSE +21 -0
  4. package/dist/skills/code-review/SKILL.md +233 -0
  5. package/dist/skills/code-review/assets/pr-review-template.md +137 -0
  6. package/dist/skills/code-review/assets/review-checklist.md +123 -0
  7. package/dist/skills/code-review/reference/angular.md +768 -0
  8. package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
  9. package/dist/skills/code-review/reference/c.md +890 -0
  10. package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
  11. package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
  12. package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
  13. package/dist/skills/code-review/reference/cpp.md +893 -0
  14. package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  15. package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
  16. package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
  17. package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
  18. package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
  19. package/dist/skills/code-review/reference/csharp.md +525 -0
  20. package/dist/skills/code-review/reference/css-less-sass.md +661 -0
  21. package/dist/skills/code-review/reference/dart.md +670 -0
  22. package/dist/skills/code-review/reference/django.md +985 -0
  23. package/dist/skills/code-review/reference/fastapi.md +580 -0
  24. package/dist/skills/code-review/reference/go.md +993 -0
  25. package/dist/skills/code-review/reference/java.md +409 -0
  26. package/dist/skills/code-review/reference/java8.md +586 -0
  27. package/dist/skills/code-review/reference/kotlin.md +1018 -0
  28. package/dist/skills/code-review/reference/nestjs.md +593 -0
  29. package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
  30. package/dist/skills/code-review/reference/php.md +684 -0
  31. package/dist/skills/code-review/reference/python.md +1073 -0
  32. package/dist/skills/code-review/reference/qt.md +757 -0
  33. package/dist/skills/code-review/reference/react.md +871 -0
  34. package/dist/skills/code-review/reference/ruby.md +964 -0
  35. package/dist/skills/code-review/reference/rust.md +846 -0
  36. package/dist/skills/code-review/reference/security-review-guide.md +494 -0
  37. package/dist/skills/code-review/reference/svelte.md +1064 -0
  38. package/dist/skills/code-review/reference/swift.md +936 -0
  39. package/dist/skills/code-review/reference/typescript.md +1016 -0
  40. package/dist/skills/code-review/reference/vue.md +924 -0
  41. package/dist/skills/code-review/reference/zig.md +440 -0
  42. package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
  43. package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
  44. package/dist/yorn.cjs +628 -628
  45. package/package.json +2 -2
@@ -0,0 +1,871 @@
1
+ # React Code Review Guide
2
+
3
+ React 审查重点:Hooks 规则、性能优化的适度性、组件设计、以及现代 React 19/RSC 模式。
4
+
5
+ ## 目录
6
+
7
+ - [基础 Hooks 规则](#基础-hooks-规则)
8
+ - [useEffect 模式](#useeffect-模式)
9
+ - [useMemo / useCallback](#usememo--usecallback)
10
+ - [组件设计](#组件设计)
11
+ - [Error Boundaries & Suspense](#error-boundaries--suspense)
12
+ - [Server Components (RSC)](#server-components-rsc)
13
+ - [React 19 Actions & Forms](#react-19-actions--forms)
14
+ - [Suspense & Streaming SSR](#suspense--streaming-ssr)
15
+ - [TanStack Query v5](#tanstack-query-v5)
16
+ - [Review Checklists](#review-checklists)
17
+
18
+ ---
19
+
20
+ ## 基础 Hooks 规则
21
+
22
+ ```tsx
23
+ // ❌ 条件调用 Hooks — 违反 Hooks 规则
24
+ function BadComponent({ isLoggedIn }) {
25
+ if (isLoggedIn) {
26
+ const [user, setUser] = useState(null); // Error!
27
+ }
28
+ return <div>...</div>;
29
+ }
30
+
31
+ // ✅ Hooks 必须在组件顶层调用
32
+ function GoodComponent({ isLoggedIn }) {
33
+ const [user, setUser] = useState(null);
34
+ if (!isLoggedIn) return <LoginPrompt />;
35
+ return <div>{user?.name}</div>;
36
+ }
37
+ ```
38
+
39
+ ---
40
+
41
+ ## useEffect 模式
42
+
43
+ ```tsx
44
+ // ❌ 依赖数组缺失或不完整
45
+ function BadEffect({ userId }) {
46
+ const [user, setUser] = useState(null);
47
+ useEffect(() => {
48
+ fetchUser(userId).then(setUser);
49
+ }, []); // 缺少 userId 依赖!
50
+ }
51
+
52
+ // ✅ 完整的依赖数组
53
+ function GoodEffect({ userId }) {
54
+ const [user, setUser] = useState(null);
55
+ useEffect(() => {
56
+ let cancelled = false;
57
+ fetchUser(userId).then(data => {
58
+ if (!cancelled) setUser(data);
59
+ });
60
+ return () => { cancelled = true; }; // 清理函数
61
+ }, [userId]);
62
+ }
63
+
64
+ // ❌ useEffect 用于派生状态(反模式)
65
+ function BadDerived({ items }) {
66
+ const [filteredItems, setFilteredItems] = useState([]);
67
+ useEffect(() => {
68
+ setFilteredItems(items.filter(i => i.active));
69
+ }, [items]); // 不必要的 effect + 额外渲染
70
+ return <List items={filteredItems} />;
71
+ }
72
+
73
+ // ✅ 直接在渲染时计算,或用 useMemo
74
+ function GoodDerived({ items }) {
75
+ const filteredItems = useMemo(
76
+ () => items.filter(i => i.active),
77
+ [items]
78
+ );
79
+ return <List items={filteredItems} />;
80
+ }
81
+
82
+ // ❌ useEffect 用于事件响应
83
+ function BadEventEffect() {
84
+ const [query, setQuery] = useState('');
85
+ useEffect(() => {
86
+ if (query) {
87
+ analytics.track('search', { query }); // 应该在事件处理器中
88
+ }
89
+ }, [query]);
90
+ }
91
+
92
+ // ✅ 在事件处理器中执行副作用
93
+ function GoodEvent() {
94
+ const [query, setQuery] = useState('');
95
+ const handleSearch = (q: string) => {
96
+ setQuery(q);
97
+ analytics.track('search', { query: q });
98
+ };
99
+ }
100
+ ```
101
+
102
+ ---
103
+
104
+ ## useMemo / useCallback
105
+
106
+ ```tsx
107
+ // ❌ 过度优化 — 常量不需要 useMemo
108
+ function OverOptimized() {
109
+ const config = useMemo(() => ({ timeout: 5000 }), []); // 无意义
110
+ const handleClick = useCallback(() => {
111
+ console.log('clicked');
112
+ }, []); // 如果不传给 memo 组件,无意义
113
+ }
114
+
115
+ // ✅ 只在需要时优化
116
+ function ProperlyOptimized() {
117
+ const config = { timeout: 5000 }; // 简单对象直接定义
118
+ const handleClick = () => console.log('clicked');
119
+ }
120
+
121
+ // ❌ useCallback 依赖总是变化
122
+ function BadCallback({ data }) {
123
+ // data 每次渲染都是新对象,useCallback 无效
124
+ const process = useCallback(() => {
125
+ return data.map(transform);
126
+ }, [data]);
127
+ }
128
+
129
+ // ✅ useMemo + useCallback 配合 React.memo 使用
130
+ const MemoizedChild = React.memo(function Child({ onClick, items }) {
131
+ return <div onClick={onClick}>{items.length}</div>;
132
+ });
133
+
134
+ function Parent({ rawItems }) {
135
+ const items = useMemo(() => processItems(rawItems), [rawItems]);
136
+ const handleClick = useCallback(() => {
137
+ console.log(items.length);
138
+ }, [items]);
139
+ return <MemoizedChild onClick={handleClick} items={items} />;
140
+ }
141
+ ```
142
+
143
+ ---
144
+
145
+ ## 组件设计
146
+
147
+ ```tsx
148
+ // ❌ 在组件内定义组件 — 每次渲染都创建新组件
149
+ function BadParent() {
150
+ function ChildComponent() { // 每次渲染都是新函数!
151
+ return <div>child</div>;
152
+ }
153
+ return <ChildComponent />;
154
+ }
155
+
156
+ // ✅ 组件定义在外部
157
+ function ChildComponent() {
158
+ return <div>child</div>;
159
+ }
160
+ function GoodParent() {
161
+ return <ChildComponent />;
162
+ }
163
+
164
+ // ❌ Props 总是新对象引用
165
+ function BadProps() {
166
+ return (
167
+ <MemoizedComponent
168
+ style={{ color: 'red' }} // 每次渲染新对象
169
+ onClick={() => {}} // 每次渲染新函数
170
+ />
171
+ );
172
+ }
173
+
174
+ // ✅ 稳定的引用
175
+ const style = { color: 'red' };
176
+ function GoodProps() {
177
+ const handleClick = useCallback(() => {}, []);
178
+ return <MemoizedComponent style={style} onClick={handleClick} />;
179
+ }
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Error Boundaries & Suspense
185
+
186
+ ```tsx
187
+ // ❌ 没有错误边界
188
+ function BadApp() {
189
+ return (
190
+ <Suspense fallback={<Loading />}>
191
+ <DataComponent /> {/* 错误会导致整个应用崩溃 */}
192
+ </Suspense>
193
+ );
194
+ }
195
+
196
+ // ✅ Error Boundary 包裹 Suspense
197
+ function GoodApp() {
198
+ return (
199
+ <ErrorBoundary fallback={<ErrorUI />}>
200
+ <Suspense fallback={<Loading />}>
201
+ <DataComponent />
202
+ </Suspense>
203
+ </ErrorBoundary>
204
+ );
205
+ }
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Server Components (RSC)
211
+
212
+ ```tsx
213
+ // ❌ 在 Server Component 中使用客户端特性
214
+ // app/page.tsx (Server Component by default)
215
+ function BadServerComponent() {
216
+ const [count, setCount] = useState(0); // Error! No hooks in RSC
217
+ return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
218
+ }
219
+
220
+ // ✅ 交互逻辑提取到 Client Component
221
+ // app/counter.tsx
222
+ 'use client';
223
+ function Counter() {
224
+ const [count, setCount] = useState(0);
225
+ return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
226
+ }
227
+
228
+ // app/page.tsx (Server Component)
229
+ async function GoodServerComponent() {
230
+ const data = await fetchData(); // 可以直接 await
231
+ return (
232
+ <div>
233
+ <h1>{data.title}</h1>
234
+ <Counter /> {/* 客户端组件 */}
235
+ </div>
236
+ );
237
+ }
238
+
239
+ // ❌ 'use client' 放置不当 — 整个树都变成客户端
240
+ // layout.tsx
241
+ 'use client'; // 这会让所有子组件都成为客户端组件
242
+ export default function Layout({ children }) { ... }
243
+
244
+ // ✅ 只在需要交互的组件使用 'use client'
245
+ // 将客户端逻辑隔离到叶子组件
246
+ ```
247
+
248
+ ---
249
+
250
+ ## React 19 Actions & Forms
251
+
252
+ React 19 引入了 Actions 系统和新的表单处理 Hooks,简化异步操作和乐观更新。
253
+
254
+ ### useActionState
255
+
256
+ ```tsx
257
+ // ❌ 传统方式:多个状态变量
258
+ function OldForm() {
259
+ const [isPending, setIsPending] = useState(false);
260
+ const [error, setError] = useState<string | null>(null);
261
+ const [data, setData] = useState(null);
262
+
263
+ const handleSubmit = async (formData: FormData) => {
264
+ setIsPending(true);
265
+ setError(null);
266
+ try {
267
+ const result = await submitForm(formData);
268
+ setData(result);
269
+ } catch (e) {
270
+ setError(e.message);
271
+ } finally {
272
+ setIsPending(false);
273
+ }
274
+ };
275
+ }
276
+
277
+ // ✅ React 19: useActionState 统一管理
278
+ import { useActionState } from 'react';
279
+
280
+ function NewForm() {
281
+ const [state, formAction, isPending] = useActionState(
282
+ async (prevState, formData: FormData) => {
283
+ try {
284
+ const result = await submitForm(formData);
285
+ return { success: true, data: result };
286
+ } catch (e) {
287
+ return { success: false, error: e.message };
288
+ }
289
+ },
290
+ { success: false, data: null, error: null }
291
+ );
292
+
293
+ return (
294
+ <form action={formAction}>
295
+ <input name="email" />
296
+ <button disabled={isPending}>
297
+ {isPending ? 'Submitting...' : 'Submit'}
298
+ </button>
299
+ {state.error && <p className="error">{state.error}</p>}
300
+ </form>
301
+ );
302
+ }
303
+ ```
304
+
305
+ ### useFormStatus
306
+
307
+ ```tsx
308
+ // ❌ Props 透传表单状态
309
+ function BadSubmitButton({ isSubmitting }) {
310
+ return <button disabled={isSubmitting}>Submit</button>;
311
+ }
312
+
313
+ // ✅ useFormStatus 访问父 <form> 状态(无需 props)
314
+ import { useFormStatus } from 'react-dom';
315
+
316
+ function SubmitButton() {
317
+ const { pending, data, method, action } = useFormStatus();
318
+ // 注意:必须在 <form> 内部的子组件中使用
319
+ return (
320
+ <button disabled={pending}>
321
+ {pending ? 'Submitting...' : 'Submit'}
322
+ </button>
323
+ );
324
+ }
325
+
326
+ // ❌ useFormStatus 在 form 同级组件中调用——不工作
327
+ function BadForm() {
328
+ const { pending } = useFormStatus(); // 这里无法获取状态!
329
+ return (
330
+ <form action={action}>
331
+ <button disabled={pending}>Submit</button>
332
+ </form>
333
+ );
334
+ }
335
+
336
+ // ✅ useFormStatus 必须在 form 的子组件中
337
+ function GoodForm() {
338
+ return (
339
+ <form action={action}>
340
+ <SubmitButton /> {/* useFormStatus 在这里面调用 */}
341
+ </form>
342
+ );
343
+ }
344
+ ```
345
+
346
+ ### useOptimistic
347
+
348
+ ```tsx
349
+ // ❌ 等待服务器响应再更新 UI
350
+ function SlowLike({ postId, likes }) {
351
+ const [likeCount, setLikeCount] = useState(likes);
352
+ const [isPending, setIsPending] = useState(false);
353
+
354
+ const handleLike = async () => {
355
+ setIsPending(true);
356
+ const newCount = await likePost(postId); // 等待...
357
+ setLikeCount(newCount);
358
+ setIsPending(false);
359
+ };
360
+ }
361
+
362
+ // ✅ useOptimistic 即时反馈,失败自动回滚
363
+ import { useOptimistic } from 'react';
364
+
365
+ function FastLike({ postId, likes }) {
366
+ const [optimisticLikes, addOptimisticLike] = useOptimistic(
367
+ likes,
368
+ (currentLikes, increment: number) => currentLikes + increment
369
+ );
370
+
371
+ const handleLike = async () => {
372
+ addOptimisticLike(1); // 立即更新 UI
373
+ try {
374
+ await likePost(postId); // 后台同步
375
+ } catch {
376
+ // React 自动回滚到 likes 原值
377
+ }
378
+ };
379
+
380
+ return <button onClick={handleLike}>{optimisticLikes} likes</button>;
381
+ }
382
+ ```
383
+
384
+ ### Server Actions (Next.js 15+)
385
+
386
+ ```tsx
387
+ // ❌ 客户端调用 API
388
+ 'use client';
389
+ function ClientForm() {
390
+ const handleSubmit = async (formData: FormData) => {
391
+ const res = await fetch('/api/submit', {
392
+ method: 'POST',
393
+ body: formData,
394
+ });
395
+ // ...
396
+ };
397
+ }
398
+
399
+ // ✅ Server Action + useActionState
400
+ // actions.ts
401
+ 'use server';
402
+ export async function createPost(prevState: any, formData: FormData) {
403
+ const title = formData.get('title');
404
+ await db.posts.create({ title });
405
+ revalidatePath('/posts');
406
+ return { success: true };
407
+ }
408
+
409
+ // form.tsx
410
+ 'use client';
411
+ import { createPost } from './actions';
412
+
413
+ function PostForm() {
414
+ const [state, formAction, isPending] = useActionState(createPost, null);
415
+ return (
416
+ <form action={formAction}>
417
+ <input name="title" />
418
+ <SubmitButton />
419
+ </form>
420
+ );
421
+ }
422
+ ```
423
+
424
+ ---
425
+
426
+ ## Suspense & Streaming SSR
427
+
428
+ Suspense 和 Streaming 是 React 18+ 的核心特性,在 2025 年的 Next.js 15 等框架中广泛使用。
429
+
430
+ ### 基础 Suspense
431
+
432
+ ```tsx
433
+ // ❌ 传统加载状态管理
434
+ function OldComponent() {
435
+ const [data, setData] = useState(null);
436
+ const [isLoading, setIsLoading] = useState(true);
437
+
438
+ useEffect(() => {
439
+ fetchData().then(setData).finally(() => setIsLoading(false));
440
+ }, []);
441
+
442
+ if (isLoading) return <Spinner />;
443
+ return <DataView data={data} />;
444
+ }
445
+
446
+ // ✅ Suspense 声明式加载状态
447
+ function NewComponent() {
448
+ return (
449
+ <Suspense fallback={<Spinner />}>
450
+ <DataView /> {/* 内部使用 use() 或支持 Suspense 的数据获取 */}
451
+ </Suspense>
452
+ );
453
+ }
454
+ ```
455
+
456
+ ### 多个独立 Suspense 边界
457
+
458
+ ```tsx
459
+ // ❌ 单一边界——所有内容一起加载
460
+ function BadLayout() {
461
+ return (
462
+ <Suspense fallback={<FullPageSpinner />}>
463
+ <Header />
464
+ <MainContent /> {/* 慢 */}
465
+ <Sidebar /> {/* 快 */}
466
+ </Suspense>
467
+ );
468
+ }
469
+
470
+ // ✅ 独立边界——各部分独立流式传输
471
+ function GoodLayout() {
472
+ return (
473
+ <>
474
+ <Header /> {/* 立即显示 */}
475
+ <div className="flex">
476
+ <Suspense fallback={<ContentSkeleton />}>
477
+ <MainContent /> {/* 独立加载 */}
478
+ </Suspense>
479
+ <Suspense fallback={<SidebarSkeleton />}>
480
+ <Sidebar /> {/* 独立加载 */}
481
+ </Suspense>
482
+ </div>
483
+ </>
484
+ );
485
+ }
486
+ ```
487
+
488
+ ### Next.js 15 Streaming
489
+
490
+ ```tsx
491
+ // app/page.tsx - 自动 Streaming
492
+ export default async function Page() {
493
+ // 这个 await 不会阻塞整个页面
494
+ const data = await fetchSlowData();
495
+ return <div>{data}</div>;
496
+ }
497
+
498
+ // app/loading.tsx - 自动 Suspense 边界
499
+ export default function Loading() {
500
+ return <Skeleton />;
501
+ }
502
+ ```
503
+
504
+ ### use() Hook (React 19)
505
+
506
+ ```tsx
507
+ // ✅ 在组件中读取 Promise
508
+ import { use } from 'react';
509
+
510
+ function Comments({ commentsPromise }) {
511
+ const comments = use(commentsPromise); // 自动触发 Suspense
512
+ return (
513
+ <ul>
514
+ {comments.map(c => <li key={c.id}>{c.text}</li>)}
515
+ </ul>
516
+ );
517
+ }
518
+
519
+ // 父组件创建 Promise,子组件消费
520
+ function Post({ postId }) {
521
+ const commentsPromise = fetchComments(postId); // 不 await
522
+ return (
523
+ <article>
524
+ <PostContent id={postId} />
525
+ <Suspense fallback={<CommentsSkeleton />}>
526
+ <Comments commentsPromise={commentsPromise} />
527
+ </Suspense>
528
+ </article>
529
+ );
530
+ }
531
+ ```
532
+
533
+ ---
534
+
535
+ ## TanStack Query v5
536
+
537
+ TanStack Query 是 React 生态中最流行的数据获取库,v5 是当前稳定版本。
538
+
539
+ ### 基础配置
540
+
541
+ ```tsx
542
+ // ❌ 不正确的默认配置
543
+ const queryClient = new QueryClient(); // 默认配置可能不适合
544
+
545
+ // ✅ 生产环境推荐配置
546
+ const queryClient = new QueryClient({
547
+ defaultOptions: {
548
+ queries: {
549
+ staleTime: 1000 * 60 * 5, // 5 分钟内数据视为新鲜
550
+ gcTime: 1000 * 60 * 30, // 30 分钟后垃圾回收(v5 重命名)
551
+ retry: 3,
552
+ refetchOnWindowFocus: false, // 根据需求决定
553
+ },
554
+ },
555
+ });
556
+ ```
557
+
558
+ ### queryOptions (v5 新增)
559
+
560
+ ```tsx
561
+ // ❌ 重复定义 queryKey 和 queryFn
562
+ function Component1() {
563
+ const { data } = useQuery({
564
+ queryKey: ['users', userId],
565
+ queryFn: () => fetchUser(userId),
566
+ });
567
+ }
568
+
569
+ function prefetchUser(queryClient, userId) {
570
+ queryClient.prefetchQuery({
571
+ queryKey: ['users', userId], // 重复!
572
+ queryFn: () => fetchUser(userId), // 重复!
573
+ });
574
+ }
575
+
576
+ // ✅ queryOptions 统一定义,类型安全
577
+ import { queryOptions } from '@tanstack/react-query';
578
+
579
+ const userQueryOptions = (userId: string) =>
580
+ queryOptions({
581
+ queryKey: ['users', userId],
582
+ queryFn: () => fetchUser(userId),
583
+ });
584
+
585
+ function Component1({ userId }) {
586
+ const { data } = useQuery(userQueryOptions(userId));
587
+ }
588
+
589
+ function prefetchUser(queryClient, userId) {
590
+ queryClient.prefetchQuery(userQueryOptions(userId));
591
+ }
592
+
593
+ // getQueryData 也是类型安全的
594
+ const user = queryClient.getQueryData(userQueryOptions(userId).queryKey);
595
+ ```
596
+
597
+ ### 常见陷阱
598
+
599
+ ```tsx
600
+ // ❌ staleTime 为 0 导致过度请求
601
+ useQuery({
602
+ queryKey: ['data'],
603
+ queryFn: fetchData,
604
+ // staleTime 默认为 0,每次组件挂载都会 refetch
605
+ });
606
+
607
+ // ✅ 设置合理的 staleTime
608
+ useQuery({
609
+ queryKey: ['data'],
610
+ queryFn: fetchData,
611
+ staleTime: 1000 * 60, // 1 分钟内不会重新请求
612
+ });
613
+
614
+ // ❌ 在 queryFn 中使用不稳定的引用
615
+ function BadQuery({ filters }) {
616
+ useQuery({
617
+ queryKey: ['items'], // queryKey 没有包含 filters!
618
+ queryFn: () => fetchItems(filters), // filters 变化不会触发重新请求
619
+ });
620
+ }
621
+
622
+ // ✅ queryKey 包含所有影响数据的参数
623
+ function GoodQuery({ filters }) {
624
+ useQuery({
625
+ queryKey: ['items', filters], // filters 是 queryKey 的一部分
626
+ queryFn: () => fetchItems(filters),
627
+ });
628
+ }
629
+ ```
630
+
631
+ ### useSuspenseQuery
632
+
633
+ > **重要限制**:useSuspenseQuery 与 useQuery 有显著差异,选择前需了解其限制。
634
+
635
+ #### useSuspenseQuery 的限制
636
+
637
+ | 特性 | useQuery | useSuspenseQuery |
638
+ |------|----------|------------------|
639
+ | `enabled` 选项 | ✅ 支持 | ❌ 不支持 |
640
+ | `placeholderData` | ✅ 支持 | ❌ 不支持 |
641
+ | `data` 类型 | `T \| undefined` | `T`(保证有值)|
642
+ | 错误处理 | `error` 属性 | 抛出到 Error Boundary |
643
+ | 加载状态 | `isLoading` 属性 | 挂起到 Suspense |
644
+
645
+ #### 不支持 enabled 的替代方案
646
+
647
+ ```tsx
648
+ // ❌ 使用 useQuery + enabled 实现条件查询
649
+ function BadSuspenseQuery({ userId }) {
650
+ const { data } = useSuspenseQuery({
651
+ queryKey: ['user', userId],
652
+ queryFn: () => fetchUser(userId),
653
+ enabled: !!userId, // useSuspenseQuery 不支持 enabled!
654
+ });
655
+ }
656
+
657
+ // ✅ 组件组合实现条件渲染
658
+ function GoodSuspenseQuery({ userId }) {
659
+ // useSuspenseQuery 保证 data 是 T 不是 T | undefined
660
+ const { data } = useSuspenseQuery({
661
+ queryKey: ['user', userId],
662
+ queryFn: () => fetchUser(userId),
663
+ });
664
+ return <UserProfile user={data} />;
665
+ }
666
+
667
+ function Parent({ userId }) {
668
+ if (!userId) return <NoUserSelected />;
669
+ return (
670
+ <Suspense fallback={<UserSkeleton />}>
671
+ <GoodSuspenseQuery userId={userId} />
672
+ </Suspense>
673
+ );
674
+ }
675
+ ```
676
+
677
+ #### 错误处理差异
678
+
679
+ ```tsx
680
+ // ❌ useSuspenseQuery 没有 error 属性
681
+ function BadErrorHandling() {
682
+ const { data, error } = useSuspenseQuery({...});
683
+ if (error) return <Error />; // error 总是 null!
684
+ }
685
+
686
+ // ✅ 使用 Error Boundary 处理错误
687
+ function GoodErrorHandling() {
688
+ return (
689
+ <ErrorBoundary fallback={<ErrorMessage />}>
690
+ <Suspense fallback={<Loading />}>
691
+ <DataComponent />
692
+ </Suspense>
693
+ </ErrorBoundary>
694
+ );
695
+ }
696
+
697
+ function DataComponent() {
698
+ // 错误会抛出到 Error Boundary
699
+ const { data } = useSuspenseQuery({
700
+ queryKey: ['data'],
701
+ queryFn: fetchData,
702
+ });
703
+ return <Display data={data} />;
704
+ }
705
+ ```
706
+
707
+ #### 何时选择 useSuspenseQuery
708
+
709
+ ```tsx
710
+ // ✅ 适合场景:
711
+ // 1. 数据总是需要的(无条件查询)
712
+ // 2. 组件必须有数据才能渲染
713
+ // 3. 使用 React 19 的 Suspense 模式
714
+ // 4. 服务端组件 + 客户端 hydration
715
+
716
+ // ❌ 不适合场景:
717
+ // 1. 条件查询(根据用户操作触发)
718
+ // 2. 需要 placeholderData 或初始数据
719
+ // 3. 需要在组件内处理 loading/error 状态
720
+ // 4. 多个查询有依赖关系
721
+
722
+ // ✅ 多个独立查询用 useSuspenseQueries
723
+ function MultipleQueries({ userId }) {
724
+ const [userQuery, postsQuery] = useSuspenseQueries({
725
+ queries: [
726
+ { queryKey: ['user', userId], queryFn: () => fetchUser(userId) },
727
+ { queryKey: ['posts', userId], queryFn: () => fetchPosts(userId) },
728
+ ],
729
+ });
730
+ // 两个查询并行执行,都完成后组件渲染
731
+ return <Profile user={userQuery.data} posts={postsQuery.data} />;
732
+ }
733
+ ```
734
+
735
+ ### 乐观更新 (v5 简化)
736
+
737
+ ```tsx
738
+ // ❌ 手动管理缓存的乐观更新(复杂)
739
+ const mutation = useMutation({
740
+ mutationFn: updateTodo,
741
+ onMutate: async (newTodo) => {
742
+ await queryClient.cancelQueries({ queryKey: ['todos'] });
743
+ const previousTodos = queryClient.getQueryData(['todos']);
744
+ queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
745
+ return { previousTodos };
746
+ },
747
+ onError: (err, newTodo, context) => {
748
+ queryClient.setQueryData(['todos'], context.previousTodos);
749
+ },
750
+ onSettled: () => {
751
+ queryClient.invalidateQueries({ queryKey: ['todos'] });
752
+ },
753
+ });
754
+
755
+ // ✅ v5 简化:使用 variables 进行乐观 UI
756
+ function TodoList() {
757
+ const { data: todos } = useQuery(todosQueryOptions);
758
+ const { mutate, variables, isPending } = useMutation({
759
+ mutationFn: addTodo,
760
+ onSuccess: () => {
761
+ queryClient.invalidateQueries({ queryKey: ['todos'] });
762
+ },
763
+ });
764
+
765
+ return (
766
+ <ul>
767
+ {todos?.map(todo => <TodoItem key={todo.id} todo={todo} />)}
768
+ {/* 乐观显示正在添加的 todo */}
769
+ {isPending && <TodoItem todo={variables} isOptimistic />}
770
+ </ul>
771
+ );
772
+ }
773
+ ```
774
+
775
+ ### v5 状态字段变化
776
+
777
+ ```tsx
778
+ // v4: isLoading 表示首次加载或后续获取
779
+ // v5: isPending 表示没有数据,isLoading = isPending && isFetching
780
+
781
+ const { data, isPending, isFetching, isLoading } = useQuery({...});
782
+
783
+ // isPending: 缓存中没有数据(首次加载)
784
+ // isFetching: 正在请求中(包括后台刷新)
785
+ // isLoading: isPending && isFetching(首次加载中)
786
+
787
+ // ❌ v4 代码直接迁移
788
+ if (isLoading) return <Spinner />; // v5 中行为可能不同
789
+
790
+ // ✅ 明确意图
791
+ if (isPending) return <Spinner />; // 没有数据时显示加载
792
+ // 或
793
+ if (isLoading) return <Spinner />; // 首次加载中
794
+ ```
795
+
796
+ ---
797
+
798
+ ## Review Checklists
799
+
800
+ ### Hooks 规则
801
+
802
+ - [ ] Hooks 在组件/自定义 Hook 顶层调用
803
+ - [ ] 没有条件/循环中调用 Hooks
804
+ - [ ] useEffect 依赖数组完整
805
+ - [ ] useEffect 有清理函数(订阅/定时器/请求)
806
+ - [ ] 没有用 useEffect 计算派生状态
807
+
808
+ ### 性能优化(适度原则)
809
+
810
+ - [ ] useMemo/useCallback 只用于真正需要的场景
811
+ - [ ] React.memo 配合稳定的 props 引用
812
+ - [ ] 没有在组件内定义子组件
813
+ - [ ] 没有在 JSX 中创建新对象/函数(除非传给非 memo 组件)
814
+ - [ ] 长列表使用虚拟化(react-window/react-virtual)
815
+
816
+ ### 组件设计
817
+
818
+ - [ ] 组件职责单一,不超过 200 行
819
+ - [ ] 逻辑与展示分离(Custom Hooks)
820
+ - [ ] Props 接口清晰,使用 TypeScript
821
+ - [ ] 避免 Props Drilling(考虑 Context 或组合)
822
+
823
+ ### 状态管理
824
+
825
+ - [ ] 状态就近原则(最小必要范围)
826
+ - [ ] 复杂状态用 useReducer
827
+ - [ ] 全局状态用 Context 或状态库
828
+ - [ ] 避免不必要的状态(派生 > 存储)
829
+
830
+ ### 错误处理
831
+
832
+ - [ ] 关键区域有 Error Boundary
833
+ - [ ] Suspense 配合 Error Boundary 使用
834
+ - [ ] 异步操作有错误处理
835
+
836
+ ### Server Components (RSC)
837
+
838
+ - [ ] 'use client' 只用于需要交互的组件
839
+ - [ ] Server Component 不使用 Hooks/事件处理
840
+ - [ ] 客户端组件尽量放在叶子节点
841
+ - [ ] 数据获取在 Server Component 中进行
842
+
843
+ ### React 19 Forms
844
+
845
+ - [ ] 使用 useActionState 替代多个 useState
846
+ - [ ] useFormStatus 在 form 子组件中调用
847
+ - [ ] useOptimistic 不用于关键业务(支付等)
848
+ - [ ] Server Action 正确标记 'use server'
849
+
850
+ ### Suspense & Streaming
851
+
852
+ - [ ] 按用户体验需求划分 Suspense 边界
853
+ - [ ] 每个 Suspense 有对应的 Error Boundary
854
+ - [ ] 提供有意义的 fallback(骨架屏 > Spinner)
855
+ - [ ] 避免在 layout 层级 await 慢数据
856
+
857
+ ### TanStack Query
858
+
859
+ - [ ] queryKey 包含所有影响数据的参数
860
+ - [ ] 设置合理的 staleTime(不是默认 0)
861
+ - [ ] useSuspenseQuery 不使用 enabled
862
+ - [ ] Mutation 成功后 invalidate 相关查询
863
+ - [ ] 理解 isPending vs isLoading 区别
864
+
865
+ ### 测试
866
+
867
+ - [ ] 使用 @testing-library/react
868
+ - [ ] 用 screen 查询元素
869
+ - [ ] 用 userEvent 代替 fireEvent
870
+ - [ ] 优先使用 *ByRole 查询
871
+ - [ ] 测试行为而非实现细节