@mison/ag-kit-cn 2.0.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/.agent/.shared/ui-ux-pro-max/data/charts.csv +26 -0
- package/.agent/.shared/ui-ux-pro-max/data/colors.csv +97 -0
- package/.agent/.shared/ui-ux-pro-max/data/icons.csv +101 -0
- package/.agent/.shared/ui-ux-pro-max/data/landing.csv +31 -0
- package/.agent/.shared/ui-ux-pro-max/data/products.csv +97 -0
- package/.agent/.shared/ui-ux-pro-max/data/prompts.csv +24 -0
- package/.agent/.shared/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/.agent/.shared/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.agent/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.agent/.shared/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/.agent/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.agent/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.agent/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.agent/.shared/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.agent/.shared/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.agent/.shared/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/.agent/.shared/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.agent/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.agent/.shared/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.agent/.shared/ui-ux-pro-max/data/styles.csv +59 -0
- package/.agent/.shared/ui-ux-pro-max/data/typography.csv +58 -0
- package/.agent/.shared/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/.agent/.shared/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.agent/.shared/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/.agent/.shared/ui-ux-pro-max/scripts/core.py +258 -0
- package/.agent/.shared/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/.agent/.shared/ui-ux-pro-max/scripts/search.py +106 -0
- package/.agent/ARCHITECTURE.md +285 -0
- package/.agent/agents/backend-specialist.md +268 -0
- package/.agent/agents/code-archaeologist.md +106 -0
- package/.agent/agents/database-architect.md +225 -0
- package/.agent/agents/debugger.md +225 -0
- package/.agent/agents/devops-engineer.md +242 -0
- package/.agent/agents/documentation-writer.md +104 -0
- package/.agent/agents/explorer-agent.md +73 -0
- package/.agent/agents/frontend-specialist.md +618 -0
- package/.agent/agents/game-developer.md +162 -0
- package/.agent/agents/mobile-developer.md +382 -0
- package/.agent/agents/orchestrator.md +438 -0
- package/.agent/agents/penetration-tester.md +188 -0
- package/.agent/agents/performance-optimizer.md +187 -0
- package/.agent/agents/product-manager.md +112 -0
- package/.agent/agents/product-owner.md +95 -0
- package/.agent/agents/project-planner.md +405 -0
- package/.agent/agents/qa-automation-engineer.md +103 -0
- package/.agent/agents/security-auditor.md +170 -0
- package/.agent/agents/seo-specialist.md +111 -0
- package/.agent/agents/test-engineer.md +158 -0
- package/.agent/mcp_config.json +12 -0
- package/.agent/rules/GEMINI.md +273 -0
- package/.agent/scripts/auto_preview.py +148 -0
- package/.agent/scripts/checklist.py +217 -0
- package/.agent/scripts/session_manager.py +120 -0
- package/.agent/scripts/verify_all.py +327 -0
- package/.agent/skills/api-patterns/SKILL.md +84 -0
- package/.agent/skills/api-patterns/api-style.md +42 -0
- package/.agent/skills/api-patterns/auth.md +24 -0
- package/.agent/skills/api-patterns/documentation.md +26 -0
- package/.agent/skills/api-patterns/graphql.md +41 -0
- package/.agent/skills/api-patterns/rate-limiting.md +31 -0
- package/.agent/skills/api-patterns/response.md +37 -0
- package/.agent/skills/api-patterns/rest.md +40 -0
- package/.agent/skills/api-patterns/scripts/api_validator.py +211 -0
- package/.agent/skills/api-patterns/security-testing.md +122 -0
- package/.agent/skills/api-patterns/trpc.md +41 -0
- package/.agent/skills/api-patterns/versioning.md +22 -0
- package/.agent/skills/app-builder/SKILL.md +75 -0
- package/.agent/skills/app-builder/agent-coordination.md +74 -0
- package/.agent/skills/app-builder/feature-building.md +53 -0
- package/.agent/skills/app-builder/project-detection.md +34 -0
- package/.agent/skills/app-builder/scaffolding.md +118 -0
- package/.agent/skills/app-builder/tech-stack.md +40 -0
- package/.agent/skills/app-builder/templates/SKILL.md +39 -0
- package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +76 -0
- package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +92 -0
- package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +88 -0
- package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +88 -0
- package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +83 -0
- package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +90 -0
- package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +90 -0
- package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +122 -0
- package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +122 -0
- package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +169 -0
- package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +134 -0
- package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +83 -0
- package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +119 -0
- package/.agent/skills/architecture/SKILL.md +57 -0
- package/.agent/skills/architecture/context-discovery.md +43 -0
- package/.agent/skills/architecture/examples.md +94 -0
- package/.agent/skills/architecture/pattern-selection.md +68 -0
- package/.agent/skills/architecture/patterns-reference.md +50 -0
- package/.agent/skills/architecture/trade-off-analysis.md +77 -0
- package/.agent/skills/bash-linux/SKILL.md +201 -0
- package/.agent/skills/behavioral-modes/SKILL.md +264 -0
- package/.agent/skills/brainstorming/SKILL.md +164 -0
- package/.agent/skills/brainstorming/dynamic-questioning.md +359 -0
- package/.agent/skills/clean-code/SKILL.md +200 -0
- package/.agent/skills/code-review-checklist/SKILL.md +125 -0
- package/.agent/skills/database-design/SKILL.md +54 -0
- package/.agent/skills/database-design/database-selection.md +43 -0
- package/.agent/skills/database-design/indexing.md +39 -0
- package/.agent/skills/database-design/migrations.md +50 -0
- package/.agent/skills/database-design/optimization.md +36 -0
- package/.agent/skills/database-design/orm-selection.md +30 -0
- package/.agent/skills/database-design/schema-design.md +56 -0
- package/.agent/skills/database-design/scripts/schema_validator.py +172 -0
- package/.agent/skills/deployment-procedures/SKILL.md +241 -0
- package/.agent/skills/doc.md +177 -0
- package/.agent/skills/documentation-templates/SKILL.md +194 -0
- package/.agent/skills/frontend-design/SKILL.md +418 -0
- package/.agent/skills/frontend-design/animation-guide.md +331 -0
- package/.agent/skills/frontend-design/color-system.md +307 -0
- package/.agent/skills/frontend-design/decision-trees.md +418 -0
- package/.agent/skills/frontend-design/motion-graphics.md +306 -0
- package/.agent/skills/frontend-design/scripts/accessibility_checker.py +183 -0
- package/.agent/skills/frontend-design/scripts/ux_audit.py +727 -0
- package/.agent/skills/frontend-design/typography-system.md +345 -0
- package/.agent/skills/frontend-design/ux-psychology.md +1118 -0
- package/.agent/skills/frontend-design/visual-effects.md +383 -0
- package/.agent/skills/game-development/2d-games/SKILL.md +119 -0
- package/.agent/skills/game-development/3d-games/SKILL.md +135 -0
- package/.agent/skills/game-development/SKILL.md +167 -0
- package/.agent/skills/game-development/game-art/SKILL.md +185 -0
- package/.agent/skills/game-development/game-audio/SKILL.md +190 -0
- package/.agent/skills/game-development/game-design/SKILL.md +129 -0
- package/.agent/skills/game-development/mobile-games/SKILL.md +108 -0
- package/.agent/skills/game-development/multiplayer/SKILL.md +132 -0
- package/.agent/skills/game-development/pc-games/SKILL.md +144 -0
- package/.agent/skills/game-development/vr-ar/SKILL.md +123 -0
- package/.agent/skills/game-development/web-games/SKILL.md +150 -0
- package/.agent/skills/geo-fundamentals/SKILL.md +155 -0
- package/.agent/skills/geo-fundamentals/scripts/geo_checker.py +289 -0
- package/.agent/skills/i18n-localization/SKILL.md +154 -0
- package/.agent/skills/i18n-localization/scripts/i18n_checker.py +241 -0
- package/.agent/skills/intelligent-routing/SKILL.md +335 -0
- package/.agent/skills/lint-and-validate/SKILL.md +44 -0
- package/.agent/skills/lint-and-validate/scripts/lint_runner.py +184 -0
- package/.agent/skills/lint-and-validate/scripts/type_coverage.py +173 -0
- package/.agent/skills/mcp-builder/SKILL.md +176 -0
- package/.agent/skills/mobile-design/SKILL.md +394 -0
- package/.agent/skills/mobile-design/decision-trees.md +516 -0
- package/.agent/skills/mobile-design/mobile-backend.md +491 -0
- package/.agent/skills/mobile-design/mobile-color-system.md +420 -0
- package/.agent/skills/mobile-design/mobile-debugging.md +122 -0
- package/.agent/skills/mobile-design/mobile-design-thinking.md +355 -0
- package/.agent/skills/mobile-design/mobile-navigation.md +458 -0
- package/.agent/skills/mobile-design/mobile-performance.md +767 -0
- package/.agent/skills/mobile-design/mobile-testing.md +356 -0
- package/.agent/skills/mobile-design/mobile-typography.md +432 -0
- package/.agent/skills/mobile-design/platform-android.md +666 -0
- package/.agent/skills/mobile-design/platform-ios.md +561 -0
- package/.agent/skills/mobile-design/scripts/mobile_audit.py +670 -0
- package/.agent/skills/mobile-design/touch-psychology.md +537 -0
- package/.agent/skills/nextjs-react-expert/1-async-eliminating-waterfalls.md +311 -0
- package/.agent/skills/nextjs-react-expert/2-bundle-bundle-size-optimization.md +241 -0
- package/.agent/skills/nextjs-react-expert/3-server-server-side-performance.md +489 -0
- package/.agent/skills/nextjs-react-expert/4-client-client-side-data-fetching.md +263 -0
- package/.agent/skills/nextjs-react-expert/5-rerender-re-render-optimization.md +581 -0
- package/.agent/skills/nextjs-react-expert/6-rendering-rendering-performance.md +431 -0
- package/.agent/skills/nextjs-react-expert/7-js-javascript-performance.md +683 -0
- package/.agent/skills/nextjs-react-expert/8-advanced-advanced-patterns.md +149 -0
- package/.agent/skills/nextjs-react-expert/SKILL.md +286 -0
- package/.agent/skills/nextjs-react-expert/scripts/convert_rules.py +222 -0
- package/.agent/skills/nextjs-react-expert/scripts/react_performance_checker.py +252 -0
- package/.agent/skills/nodejs-best-practices/SKILL.md +333 -0
- package/.agent/skills/parallel-agents/SKILL.md +194 -0
- package/.agent/skills/performance-profiling/SKILL.md +149 -0
- package/.agent/skills/performance-profiling/scripts/lighthouse_audit.py +76 -0
- package/.agent/skills/plan-writing/SKILL.md +152 -0
- package/.agent/skills/powershell-windows/SKILL.md +166 -0
- package/.agent/skills/python-patterns/SKILL.md +441 -0
- package/.agent/skills/red-team-tactics/SKILL.md +203 -0
- package/.agent/skills/rust-pro/SKILL.md +190 -0
- package/.agent/skills/seo-fundamentals/SKILL.md +135 -0
- package/.agent/skills/seo-fundamentals/scripts/seo_checker.py +215 -0
- package/.agent/skills/server-management/SKILL.md +161 -0
- package/.agent/skills/systematic-debugging/SKILL.md +114 -0
- package/.agent/skills/tailwind-patterns/SKILL.md +269 -0
- package/.agent/skills/tdd-workflow/SKILL.md +149 -0
- package/.agent/skills/testing-patterns/SKILL.md +178 -0
- package/.agent/skills/testing-patterns/scripts/test_runner.py +219 -0
- package/.agent/skills/vulnerability-scanner/SKILL.md +276 -0
- package/.agent/skills/vulnerability-scanner/checklists.md +131 -0
- package/.agent/skills/vulnerability-scanner/scripts/security_scan.py +459 -0
- package/.agent/skills/web-design-guidelines/SKILL.md +57 -0
- package/.agent/skills/webapp-testing/SKILL.md +187 -0
- package/.agent/skills/webapp-testing/scripts/playwright_runner.py +173 -0
- package/.agent/workflows/brainstorm.md +113 -0
- package/.agent/workflows/create.md +59 -0
- package/.agent/workflows/debug.md +103 -0
- package/.agent/workflows/deploy.md +176 -0
- package/.agent/workflows/enhance.md +63 -0
- package/.agent/workflows/orchestrate.md +242 -0
- package/.agent/workflows/plan.md +89 -0
- package/.agent/workflows/preview.md +80 -0
- package/.agent/workflows/restore-localize-compat.md +525 -0
- package/.agent/workflows/status.md +86 -0
- package/.agent/workflows/test.md +144 -0
- package/.agent/workflows/ui-ux-pro-max.md +295 -0
- package/AGENT_FLOW.md +609 -0
- package/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/README.md +260 -0
- package/bin/adapters/base.js +63 -0
- package/bin/adapters/codex.js +391 -0
- package/bin/adapters/gemini.js +137 -0
- package/bin/ag-kit.js +1336 -0
- package/bin/core/builder.js +80 -0
- package/bin/core/generator.js +59 -0
- package/bin/core/resource-loader.js +64 -0
- package/bin/core/transformer.js +208 -0
- package/bin/interactive.js +65 -0
- package/bin/utils/atomic-writer.js +97 -0
- package/bin/utils/git-helper.js +68 -0
- package/bin/utils/managed-block.js +65 -0
- package/bin/utils/manifest.js +241 -0
- package/bin/utils.js +82 -0
- package/docs/codex-rules-template.md +36 -0
- package/docs/mapping-spec.md +68 -0
- package/docs/multi-target-adapter.md +80 -0
- package/docs/official/README.md +53 -0
- package/docs/official/antigravity/agent-modes-settings.md +64 -0
- package/docs/official/antigravity/rules-workflows.md +96 -0
- package/docs/official/antigravity/skills.md +147 -0
- package/docs/official/codex/agents-md.md +119 -0
- package/docs/official/codex/config-advanced.md +358 -0
- package/docs/official/codex/config-basic.md +141 -0
- package/docs/official/codex/config-reference.md +223 -0
- package/docs/official/codex/config-sample.md +216 -0
- package/docs/official/codex/mcp.md +107 -0
- package/docs/official/codex/rules.md +79 -0
- package/docs/official/codex/skills.md +114 -0
- package/docs/official/sources-index.md +32 -0
- package/docs/operations.md +145 -0
- package/docs/terminology-style-guide.md +69 -0
- package/package.json +51 -0
- package/scripts/postinstall-check.js +112 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
# 5. 重渲染优化 (Re-render Optimization)
|
|
2
|
+
|
|
3
|
+
> **影响:** 中 (MEDIUM)
|
|
4
|
+
> **重点:** 减少不必要的重渲染,降低无效计算并提升 UI 响应性。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 概览
|
|
9
|
+
|
|
10
|
+
本节包含 **12 条规则**,聚焦重渲染优化。
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 规则 5.1:在渲染阶段计算派生状态
|
|
15
|
+
|
|
16
|
+
**影响:** 中 (MEDIUM)
|
|
17
|
+
**标签:** rerender, derived-state, useEffect, state
|
|
18
|
+
|
|
19
|
+
## 在渲染阶段计算派生状态
|
|
20
|
+
|
|
21
|
+
如果一个值可以由当前 props/state 直接计算得到,就不要再把它存进 state,或通过 effect 去更新它。应在渲染阶段直接派生,避免额外渲染和状态漂移。不要仅因为 prop 变化就在 effect 里 setState;优先使用派生值或基于 key 的重置。
|
|
22
|
+
|
|
23
|
+
**错误示例(冗余 state + effect):**
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
function Form() {
|
|
27
|
+
const [firstName, setFirstName] = useState('First')
|
|
28
|
+
const [lastName, setLastName] = useState('Last')
|
|
29
|
+
const [fullName, setFullName] = useState('')
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setFullName(firstName + ' ' + lastName)
|
|
33
|
+
}, [firstName, lastName])
|
|
34
|
+
|
|
35
|
+
return <p>{fullName}</p>
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**正确示例(渲染时直接派生):**
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
function Form() {
|
|
43
|
+
const [firstName, setFirstName] = useState('First')
|
|
44
|
+
const [lastName, setLastName] = useState('Last')
|
|
45
|
+
const fullName = firstName + ' ' + lastName
|
|
46
|
+
|
|
47
|
+
return <p>{fullName}</p>
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
参考: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 规则 5.2:将状态读取延后到实际使用点
|
|
56
|
+
|
|
57
|
+
**影响:** 中 (MEDIUM)
|
|
58
|
+
**标签:** rerender, searchParams, localStorage, optimization
|
|
59
|
+
|
|
60
|
+
## 将状态读取延后到实际使用点
|
|
61
|
+
|
|
62
|
+
如果你只在回调里读取动态状态(如 searchParams、localStorage),就不要为其建立订阅。
|
|
63
|
+
|
|
64
|
+
**错误示例(会订阅所有 searchParams 变化):**
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
function ShareButton({ chatId }: { chatId: string }) {
|
|
68
|
+
const searchParams = useSearchParams()
|
|
69
|
+
|
|
70
|
+
const handleShare = () => {
|
|
71
|
+
const ref = searchParams.get('ref')
|
|
72
|
+
shareChat(chatId, { ref })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return <button onClick={handleShare}>Share</button>
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**正确示例(按需读取,不订阅):**
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
function ShareButton({ chatId }: { chatId: string }) {
|
|
83
|
+
const handleShare = () => {
|
|
84
|
+
const params = new URLSearchParams(window.location.search)
|
|
85
|
+
const ref = params.get('ref')
|
|
86
|
+
shareChat(chatId, { ref })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return <button onClick={handleShare}>Share</button>
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 规则 5.3:简单原始类型表达式不要用 useMemo 包裹
|
|
96
|
+
|
|
97
|
+
**影响:** 低到中 (LOW-MEDIUM)
|
|
98
|
+
**标签:** rerender, useMemo, optimization
|
|
99
|
+
|
|
100
|
+
## 简单原始类型表达式不要用 useMemo 包裹
|
|
101
|
+
|
|
102
|
+
当表达式本身很简单(仅少量逻辑或算术运算),且返回原始类型(boolean、number、string)时,不要用 `useMemo` 包裹。调用 `useMemo` 及比较依赖项的开销,可能比表达式本身更高。
|
|
103
|
+
|
|
104
|
+
**错误示例:**
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
function Header({ user, notifications }: Props) {
|
|
108
|
+
const isLoading = useMemo(() => {
|
|
109
|
+
return user.isLoading || notifications.isLoading
|
|
110
|
+
}, [user.isLoading, notifications.isLoading])
|
|
111
|
+
|
|
112
|
+
if (isLoading) return <Skeleton />
|
|
113
|
+
// return some markup
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**正确示例:**
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
function Header({ user, notifications }: Props) {
|
|
121
|
+
const isLoading = user.isLoading || notifications.isLoading
|
|
122
|
+
|
|
123
|
+
if (isLoading) return <Skeleton />
|
|
124
|
+
// return some markup
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 规则 5.4:将 memo 组件中的非原始默认参数提取为常量
|
|
131
|
+
|
|
132
|
+
**影响:** 中 (MEDIUM)
|
|
133
|
+
**标签:** rerender, memo, optimization
|
|
134
|
+
|
|
135
|
+
## 将 memo 组件中的非原始默认参数提取为常量
|
|
136
|
+
|
|
137
|
+
当 memo 组件的可选参数默认值是非原始类型(数组、函数、对象)时,若调用组件时省略该参数,可能导致 memo 失效。原因是每次重渲染都会创建新实例,无法通过 `memo()` 的严格相等比较。
|
|
138
|
+
|
|
139
|
+
解决方式是把默认值提取到组件外的常量中。
|
|
140
|
+
|
|
141
|
+
**错误示例(`onClick` 每次重渲染值都不同):**
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
|
|
145
|
+
// ...
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// Used without optional onClick
|
|
149
|
+
<UserAvatar />
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**正确示例(稳定默认值):**
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
const NOOP = () => {};
|
|
156
|
+
|
|
157
|
+
const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
|
|
158
|
+
// ...
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// Used without optional onClick
|
|
162
|
+
<UserAvatar />
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 规则 5.5:将重计算逻辑提取到 memo 组件
|
|
168
|
+
|
|
169
|
+
**影响:** 中 (MEDIUM)
|
|
170
|
+
**标签:** rerender, memo, useMemo, optimization
|
|
171
|
+
|
|
172
|
+
## 将重计算逻辑提取到 memo 组件
|
|
173
|
+
|
|
174
|
+
把开销较大的计算提取到 memo 组件中,便于在计算前提前返回。
|
|
175
|
+
|
|
176
|
+
**错误示例(即使 loading 也计算 avatar):**
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
function Profile({ user, loading }: Props) {
|
|
180
|
+
const avatar = useMemo(() => {
|
|
181
|
+
const id = computeAvatarId(user)
|
|
182
|
+
return <Avatar id={id} />
|
|
183
|
+
}, [user])
|
|
184
|
+
|
|
185
|
+
if (loading) return <Skeleton />
|
|
186
|
+
return <div>{avatar}</div>
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**正确示例(loading 时跳过计算):**
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
|
|
194
|
+
const id = useMemo(() => computeAvatarId(user), [user])
|
|
195
|
+
return <Avatar id={id} />
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
function Profile({ user, loading }: Props) {
|
|
199
|
+
if (loading) return <Skeleton />
|
|
200
|
+
return (
|
|
201
|
+
<div>
|
|
202
|
+
<UserAvatar user={user} />
|
|
203
|
+
</div>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**说明:** 若项目启用 [React Compiler](https://react.dev/learn/react-compiler),通常无需手写 `memo()` 与 `useMemo()`;编译器会自动优化重渲染。
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## 规则 5.6:收窄 Effect 依赖
|
|
213
|
+
|
|
214
|
+
**影响:** 低 (LOW)
|
|
215
|
+
**标签:** rerender, useEffect, dependencies, optimization
|
|
216
|
+
|
|
217
|
+
## 收窄 Effect 依赖
|
|
218
|
+
|
|
219
|
+
优先依赖原始值,而非对象整体,以减少 effect 的重复执行。
|
|
220
|
+
|
|
221
|
+
**错误示例(user 任意字段变化都会重跑):**
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
console.log(user.id)
|
|
226
|
+
}, [user])
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**正确示例(仅 id 变化时重跑):**
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
console.log(user.id)
|
|
234
|
+
}, [user.id])
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**派生状态应在 effect 外计算:**
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
// 错误示例:width=767、766、765...都会运行
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
if (width < 768) {
|
|
243
|
+
enableMobileMode()
|
|
244
|
+
}
|
|
245
|
+
}, [width])
|
|
246
|
+
|
|
247
|
+
// 正确示例:仅在布尔值切换时运行
|
|
248
|
+
const isMobile = width < 768
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
if (isMobile) {
|
|
251
|
+
enableMobileMode()
|
|
252
|
+
}
|
|
253
|
+
}, [isMobile])
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## 规则 5.7:交互逻辑应放在事件处理器中
|
|
259
|
+
|
|
260
|
+
**影响:** 中 (MEDIUM)
|
|
261
|
+
**标签:** rerender, useEffect, events, side-effects, dependencies
|
|
262
|
+
|
|
263
|
+
## 交互逻辑应放在事件处理器中
|
|
264
|
+
|
|
265
|
+
如果副作用由明确的用户操作触发(提交、点击、拖拽),就应放在对应事件处理器里执行。不要建模成“state + effect”,这会让 effect 因无关变化重跑,甚至重复触发动作。
|
|
266
|
+
|
|
267
|
+
**错误示例(把事件建模成 state + effect):**
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
function Form() {
|
|
271
|
+
const [submitted, setSubmitted] = useState(false)
|
|
272
|
+
const theme = useContext(ThemeContext)
|
|
273
|
+
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (submitted) {
|
|
276
|
+
post('/api/register')
|
|
277
|
+
showToast('Registered', theme)
|
|
278
|
+
}
|
|
279
|
+
}, [submitted, theme])
|
|
280
|
+
|
|
281
|
+
return <button onClick={() => setSubmitted(true)}>Submit</button>
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**正确示例(在 handler 中直接执行):**
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
function Form() {
|
|
289
|
+
const theme = useContext(ThemeContext)
|
|
290
|
+
|
|
291
|
+
function handleSubmit() {
|
|
292
|
+
post('/api/register')
|
|
293
|
+
showToast('Registered', theme)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return <button onClick={handleSubmit}>Submit</button>
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
参考: [Should this code move to an event handler?](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## 规则 5.8:订阅派生状态而非连续值
|
|
305
|
+
|
|
306
|
+
**影响:** 中 (MEDIUM)
|
|
307
|
+
**标签:** rerender, derived-state, media-query, optimization
|
|
308
|
+
|
|
309
|
+
## 订阅派生状态而非连续值
|
|
310
|
+
|
|
311
|
+
用派生布尔状态替代连续值订阅,可以降低重渲染频率。
|
|
312
|
+
|
|
313
|
+
**错误示例(每个像素变化都会重渲染):**
|
|
314
|
+
|
|
315
|
+
```tsx
|
|
316
|
+
function Sidebar() {
|
|
317
|
+
const width = useWindowWidth() // updates continuously
|
|
318
|
+
const isMobile = width < 768
|
|
319
|
+
return <nav className={isMobile ? 'mobile' : 'desktop'} />
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**正确示例(仅布尔值切换时重渲染):**
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
function Sidebar() {
|
|
327
|
+
const isMobile = useMediaQuery('(max-width: 767px)')
|
|
328
|
+
return <nav className={isMobile ? 'mobile' : 'desktop'} />
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 规则 5.9:使用函数式 setState 更新
|
|
335
|
+
|
|
336
|
+
**影响:** 中 (MEDIUM)
|
|
337
|
+
**标签:** react, hooks, useState, useCallback, callbacks, closures
|
|
338
|
+
|
|
339
|
+
## 使用函数式 setState 更新
|
|
340
|
+
|
|
341
|
+
当状态更新依赖当前状态值时,应使用 setState 的函数式写法,而不是直接引用外层状态变量。这样可以避免过期闭包、减少不必要依赖,并让回调引用更稳定。
|
|
342
|
+
|
|
343
|
+
**错误示例(必须把 state 放进依赖):**
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
function TodoList() {
|
|
347
|
+
const [items, setItems] = useState(initialItems)
|
|
348
|
+
|
|
349
|
+
// 回调必须依赖 items,items 每次变化都会重建
|
|
350
|
+
const addItems = useCallback((newItems: Item[]) => {
|
|
351
|
+
setItems([...items, ...newItems])
|
|
352
|
+
}, [items]) // ❌ 依赖 items 会导致频繁重建
|
|
353
|
+
|
|
354
|
+
// 若漏掉依赖会有过期闭包风险
|
|
355
|
+
const removeItem = useCallback((id: string) => {
|
|
356
|
+
setItems(items.filter(item => item.id !== id))
|
|
357
|
+
}, []) // ❌ 缺少 items 依赖,会读取过期 items
|
|
358
|
+
|
|
359
|
+
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
第一个回调会在 `items` 每次变化时重建,可能导致子组件不必要重渲染。第二个回调存在过期闭包缺陷,会始终引用初始 `items` 值。
|
|
364
|
+
|
|
365
|
+
**正确示例(回调稳定、无过期闭包):**
|
|
366
|
+
|
|
367
|
+
```tsx
|
|
368
|
+
function TodoList() {
|
|
369
|
+
const [items, setItems] = useState(initialItems)
|
|
370
|
+
|
|
371
|
+
// 稳定回调,不会被重建
|
|
372
|
+
const addItems = useCallback((newItems: Item[]) => {
|
|
373
|
+
setItems(curr => [...curr, ...newItems])
|
|
374
|
+
}, []) // ✅ 无需依赖
|
|
375
|
+
|
|
376
|
+
// 总能读取最新状态,无过期闭包风险
|
|
377
|
+
const removeItem = useCallback((id: string) => {
|
|
378
|
+
setItems(curr => curr.filter(item => item.id !== id))
|
|
379
|
+
}, []) // ✅ 安全且稳定
|
|
380
|
+
|
|
381
|
+
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**收益:**
|
|
386
|
+
|
|
387
|
+
1. **回调引用稳定**:state 变化时不必重建回调
|
|
388
|
+
2. **避免过期闭包**:始终基于最新状态计算
|
|
389
|
+
3. **减少依赖复杂度**:依赖数组更简洁,降低隐性问题
|
|
390
|
+
4. **减少 Bug**:规避常见 React 闭包问题来源
|
|
391
|
+
|
|
392
|
+
**适用场景:**
|
|
393
|
+
|
|
394
|
+
- 任何依赖当前 state 的 setState 更新
|
|
395
|
+
- 在 useCallback/useMemo 中需要读取 state
|
|
396
|
+
- 事件处理器内引用 state
|
|
397
|
+
- 异步流程里更新 state
|
|
398
|
+
|
|
399
|
+
**可直接赋值的场景:**
|
|
400
|
+
|
|
401
|
+
- 设置静态值:`setCount(0)`
|
|
402
|
+
- 仅由 props/参数得出:`setName(newName)`
|
|
403
|
+
- 更新不依赖前值
|
|
404
|
+
|
|
405
|
+
**说明:** 即使启用 [React Compiler](https://react.dev/learn/react-compiler) 可自动优化部分场景,函数式更新仍推荐用于保证正确性并避免过期闭包。
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## 规则 5.10:使用惰性状态初始化
|
|
410
|
+
|
|
411
|
+
**影响:** 中 (MEDIUM)
|
|
412
|
+
**标签:** react, hooks, useState, performance, initialization
|
|
413
|
+
|
|
414
|
+
## 使用惰性状态初始化
|
|
415
|
+
|
|
416
|
+
对于初始化开销较大的 state,应向 `useState` 传函数。若直接传表达式,初始化逻辑会在每次渲染时执行,尽管初始化值只会被用一次。
|
|
417
|
+
|
|
418
|
+
**错误示例(每次渲染都会运行):**
|
|
419
|
+
|
|
420
|
+
```tsx
|
|
421
|
+
function FilteredList({ items }: { items: Item[] }) {
|
|
422
|
+
// buildSearchIndex() 每次渲染都会执行,即使初始化早已完成
|
|
423
|
+
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
|
|
424
|
+
const [query, setQuery] = useState('')
|
|
425
|
+
|
|
426
|
+
// query 变化时,buildSearchIndex 仍会被不必要地再次执行
|
|
427
|
+
return <SearchResults index={searchIndex} query={query} />
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function UserProfile() {
|
|
431
|
+
// JSON.parse 每次渲染都会执行
|
|
432
|
+
const [settings, setSettings] = useState(
|
|
433
|
+
JSON.parse(localStorage.getItem('settings') || '{}')
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
return <SettingsForm settings={settings} onChange={setSettings} />
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**正确示例(只执行一次):**
|
|
441
|
+
|
|
442
|
+
```tsx
|
|
443
|
+
function FilteredList({ items }: { items: Item[] }) {
|
|
444
|
+
// buildSearchIndex() 仅在首次渲染执行
|
|
445
|
+
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
|
|
446
|
+
const [query, setQuery] = useState('')
|
|
447
|
+
|
|
448
|
+
return <SearchResults index={searchIndex} query={query} />
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function UserProfile() {
|
|
452
|
+
// JSON.parse 仅在首次渲染执行
|
|
453
|
+
const [settings, setSettings] = useState(() => {
|
|
454
|
+
const stored = localStorage.getItem('settings')
|
|
455
|
+
return stored ? JSON.parse(stored) : {}
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
return <SettingsForm settings={settings} onChange={setSettings} />
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
当初始值来自 localStorage/sessionStorage、需要构建索引/映射等数据结构、读取 DOM 或做重计算转换时,优先用惰性初始化。
|
|
463
|
+
|
|
464
|
+
对于简单原始值(`useState(0)`)、直接引用(`useState(props.value)`)或廉价字面量(`useState({})`),无需函数形式。
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## 规则 5.11:对非紧急更新使用 Transition
|
|
469
|
+
|
|
470
|
+
**影响:** 中 (MEDIUM)
|
|
471
|
+
**标签:** rerender, transitions, startTransition, performance
|
|
472
|
+
|
|
473
|
+
## 对非紧急更新使用 Transition
|
|
474
|
+
|
|
475
|
+
将高频但非紧急的状态更新标记为 transition,保持界面响应流畅。
|
|
476
|
+
|
|
477
|
+
**错误示例(每次滚动都阻塞 UI):**
|
|
478
|
+
|
|
479
|
+
```tsx
|
|
480
|
+
function ScrollTracker() {
|
|
481
|
+
const [scrollY, setScrollY] = useState(0)
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
const handler = () => setScrollY(window.scrollY)
|
|
484
|
+
window.addEventListener('scroll', handler, { passive: true })
|
|
485
|
+
return () => window.removeEventListener('scroll', handler)
|
|
486
|
+
}, [])
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**正确示例(非阻塞更新):**
|
|
491
|
+
|
|
492
|
+
```tsx
|
|
493
|
+
import { startTransition } from 'react'
|
|
494
|
+
|
|
495
|
+
function ScrollTracker() {
|
|
496
|
+
const [scrollY, setScrollY] = useState(0)
|
|
497
|
+
useEffect(() => {
|
|
498
|
+
const handler = () => {
|
|
499
|
+
startTransition(() => setScrollY(window.scrollY))
|
|
500
|
+
}
|
|
501
|
+
window.addEventListener('scroll', handler, { passive: true })
|
|
502
|
+
return () => window.removeEventListener('scroll', handler)
|
|
503
|
+
}, [])
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## 规则 5.12:临时值使用 useRef
|
|
510
|
+
|
|
511
|
+
**影响:** 中 (MEDIUM)
|
|
512
|
+
**标签:** rerender, useref, state, performance
|
|
513
|
+
|
|
514
|
+
## 临时值使用 useRef
|
|
515
|
+
|
|
516
|
+
当某个值变化频繁,且你不希望每次变化都触发重渲染(例如鼠标追踪、定时器、瞬时标记)时,应使用 `useRef`,而不是 `useState`。组件 state 用于驱动 UI,ref 用于临时、贴近 DOM 的值。更新 ref 不会触发重渲染。
|
|
517
|
+
|
|
518
|
+
**错误示例(每次更新都重渲染):**
|
|
519
|
+
|
|
520
|
+
```tsx
|
|
521
|
+
function Tracker() {
|
|
522
|
+
const [lastX, setLastX] = useState(0)
|
|
523
|
+
|
|
524
|
+
useEffect(() => {
|
|
525
|
+
const onMove = (e: MouseEvent) => setLastX(e.clientX)
|
|
526
|
+
window.addEventListener('mousemove', onMove)
|
|
527
|
+
return () => window.removeEventListener('mousemove', onMove)
|
|
528
|
+
}, [])
|
|
529
|
+
|
|
530
|
+
return (
|
|
531
|
+
<div
|
|
532
|
+
style={{
|
|
533
|
+
position: 'fixed',
|
|
534
|
+
top: 0,
|
|
535
|
+
left: lastX,
|
|
536
|
+
width: 8,
|
|
537
|
+
height: 8,
|
|
538
|
+
background: 'black',
|
|
539
|
+
}}
|
|
540
|
+
/>
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
**正确示例(追踪更新不触发重渲染):**
|
|
548
|
+
|
|
549
|
+
```tsx
|
|
550
|
+
function Tracker() {
|
|
551
|
+
const lastXRef = useRef(0)
|
|
552
|
+
const dotRef = useRef<HTMLDivElement>(null)
|
|
553
|
+
|
|
554
|
+
useEffect(() => {
|
|
555
|
+
const onMove = (e: MouseEvent) => {
|
|
556
|
+
lastXRef.current = e.clientX
|
|
557
|
+
const node = dotRef.current
|
|
558
|
+
if (node) {
|
|
559
|
+
node.style.transform = `translateX(${e.clientX}px)`
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
window.addEventListener('mousemove', onMove)
|
|
563
|
+
return () => window.removeEventListener('mousemove', onMove)
|
|
564
|
+
}, [])
|
|
565
|
+
|
|
566
|
+
return (
|
|
567
|
+
<div
|
|
568
|
+
ref={dotRef}
|
|
569
|
+
style={{
|
|
570
|
+
position: 'fixed',
|
|
571
|
+
top: 0,
|
|
572
|
+
left: 0,
|
|
573
|
+
width: 8,
|
|
574
|
+
height: 8,
|
|
575
|
+
background: 'black',
|
|
576
|
+
transform: 'translateX(0px)',
|
|
577
|
+
}}
|
|
578
|
+
/>
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
```
|