From c6954637157d01140b7a0d25cdc35e90406eacd4 Mon Sep 17 00:00:00 2001 From: huanghaosheng <386998068@qq.com> Date: Wed, 29 Apr 2026 23:36:32 +0800 Subject: [PATCH] vault backup: 2026-04-29 23:36:32 --- hhs/REACT/4. 进阶篇/11-组件通信模式.md | 257 ++++++++--- hhs/REACT/4. 进阶篇/12-HOC 与 Render Props.md | 246 ++++++++--- hhs/REACT/4. 进阶篇/13-并发特性.md | 121 +++++- .../4. 进阶篇/14-Serverside Rendering.md | 297 +++++++++++-- hhs/REACT/5. 工程实践篇/15-性能优化.md | 234 +++++++++- hhs/REACT/5. 工程实践篇/16-测试.md | 192 ++++++++- hhs/REACT/5. 工程实践篇/17-可访问性.md | 365 +++++++++++++--- hhs/REACT/5. 工程实践篇/18-迁移与升级.md | 406 +++++++++++++++--- hhs/REACT/README.md | 2 +- 9 files changed, 1823 insertions(+), 297 deletions(-) diff --git a/hhs/REACT/4. 进阶篇/11-组件通信模式.md b/hhs/REACT/4. 进阶篇/11-组件通信模式.md index 1a8c6f2..1e756ff 100644 --- a/hhs/REACT/4. 进阶篇/11-组件通信模式.md +++ b/hhs/REACT/4. 进阶篇/11-组件通信模式.md @@ -1,5 +1,5 @@ --- -tags: [React, Component Communication, Props, Context, Frontend] +tags: [React, Component Communication, Props, Context, State Lifting, Frontend] create time: 2026-04-29 22:10 --- @@ -7,7 +7,12 @@ create time: 2026-04-29 22:10 ## 概述 -React 中父子组件之间的数据流动有多种方式。理解每种模式的适用场景和性能影响,能够避免过度设计或通信瓶颈。本文档从简单到复杂,梳理完整的通信方案谱系。 +React 的核心理念是 **"单向数据流"** ——数据从父组件流向子组件,事件从子组件冒泡回父组件。但在真实项目中,组件层级可以很深、结构可以很散,跨组件共享状态成为日常需求。 + +本文档梳理 React 中所有主流的组件通信方式,从最基础的 Props 到状态管理库,帮助你根据实际场景选择最合适方案。 + +> [!question] 思考题 +> 假设你有一个 6 层深的组件树,最顶层需要把 `theme` 传给第 6 层的按钮组件。你会怎么做?逐层透传 Props 似乎笨拙,但引入 Context 又可能引发不必要的重渲染——你怎么权衡? ## 通信方向全景图 @@ -16,14 +21,14 @@ graph TD A["父 → 子"] --> B["Props(最基础)"] A --> C["Context Provider(跨层级)"] A --> D["状态管理库(全局共享)"] - + E["子 → 父"] --> F["回调 Prop(事件驱动)"] - E --> G["refs(imperative)"] - + E --> G["Refs(imperative)"] + H["兄弟组件"] --> I["状态提升到共同父组件"] H --> J["通过共同祖先通信"] H --> K["状态管理库 / 发布订阅"] - + style B fill:#4FC08D,color:#fff style C fill:#F5A87D,color:#000 style F fill:#61DAFB,color:#000 @@ -36,30 +41,37 @@ graph TD ```tsx function App() { const user = getUser(); - return
; // Header 不需要 user,但深层的 Avatar 需要! + return
; + // Header 不需要 user,但深层的 Avatar 需要! } function Header({ user }) { return ( ); } ``` +> [!tip] 何时该容忍 Props Drill? +> 传递 2-3 层 Props 是完全合理的,甚至值得鼓励——它让数据流向清晰可见。只有超过 4 层时才考虑替代方案。 + ### 解决方案对比 | 方法 | 适合场景 | 额外依赖 | |------|----------|---------| -| **Context** | 少量深层传递(主题、用户信息) | 无 | -| **自定义 Hook + Props 重组织** | 中等深度(3-4 层) | 无 | -| **状态管理库** | 频繁变化、多组件共享 | Zustand/Redux | -| **Render Prop / HOC** | 复用逻辑而非传值 | 无 | +| **Context** | 少量深层传递(主题、用户信息、语言设置) | 无 | +| **自定义 Hook + Props 重组织** | 中等深度(3-4 层),只需中间层不暴露 | 无 | +| **状态管理库** | 频繁变化、多组件共享 | Zustand / Redux Toolkit | +| **Render Props / HOC** | 复用逻辑而非单纯传值 | 无 | ## Callback Prop(子 → 父) +这是 React 中最基础也最重要的反向通信方式:**父组件传递一个函数作为 Prop,子组件在适当时机调用它**。 + ```tsx interface FormProps { onSubmit: (data: FormValues) => Promise; @@ -69,9 +81,9 @@ function LoginForm({ onSubmit }: FormProps) { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const data = collectFormData(); - onSubmit(data); // 通知父组件 + onSubmit(data); // ✅ 通知父组件 }; - + return
; } @@ -81,75 +93,203 @@ function App() { await api.login(data); navigate("/dashboard"); }; - + return ; } ``` -> [!note] 为什么不用 props.children 做通信? -> children 是 UI 内容插槽,不是通信机制。通信用 callback prop 保持关注点分离。 +> [!note] 为什么不用 children 做通信? +> children 是 UI 内容插槽,不是通信机制。回调 Prop 保持关注点分离——子组件只关心"什么时候触发",父组件决定"触发后做什么"。 + +> [!important] 性能提示:使用 useCallback +> 如果子组件用 `React.memo` 包裹,每次父组件重新渲染都会创建新的函数引用,导致子组件无效重渲染。用 `useCallback` 缓存回调函数: +> ```tsx +> const handleLogin = useCallback(async (data: FormValues) => { ... }, []); +> ``` ## Context 跨层级通信 +当 Props 穿透超过 3-4 层时,Context 是最轻量的替代方案。**它的本质是一个"隐式 Prop"——Provider 上方的任意后代都可以直接消费,无需经过中间组件。** + ```tsx const UserContext = createContext(null); -function UserProfile({ children }: { children: React.ReactNode }) { +function UserProfileProvider({ children }: { children: React.ReactNode }) { const user = useDatabaseUser(userId); - + return ( - {/* Avatar、Settings、OrderHistory 任意深度都可消费 */} - {children} + {children} {/* Avatar、Settings 任意深度都可消费 */} ); } function OrderHistory() { - const user = useContext(UserContext); // 直接获取,无需中间层透传 - return
{user?.orders?.map(...)}
; + const user = useContext(UserContext); // ✅ 直接获取,跳过中间层 + return
{user?.orders?.map(order => )}
; } ``` -> [!warning] Context 的性能陷阱 -> - Provider value 对象每次渲染都是新引用 → 所有消费者重渲染 -> - 解决:拆分 Context、或使用 `useReducer` 返回稳定的 `{state, dispatch}` 对象 +### Context 性能陷阱与最佳实践 + +``` +⚠️ 核心问题: + Context value 改变 → Provider 下所有消费者重渲染,无论是否真的使用了这个值 +``` + +**陷阱一:value 对象每次都是新引用** + +```tsx +// ❌ 每次渲染都创建新对象,所有消费者必重渲染 + + {children} + +``` + +**陷阱二:单个大 Context 包含多个独立状态** + +```tsx +// ❌ settings 变了,即使用户信息模块的组件也会重渲染 + + {children} + +``` + +**最佳实践:拆分成小 Context + useReducer 稳定引用** + +```tsx +// ✅ 拆分 Context,粒度更细 +const UserContext = createContext(null); +const SettingsContext = createContext({ theme: "light" }); + +// ✅ 用 useReducer 返回稳定的 { state, dispatch } 对象 +function SettingsProvider({ children }: { children: React.ReactNode }) { + const [settings, dispatch] = useReducer(settingsReducer, initialSettings); + + return ( + + {children} + + ); +} + +// 消费者只取需要的 slice +function ThemeToggle() { + const { settings, dispatch } = useContext(SettingsContext); + return ( + + ); +} +``` + +## 兄弟组件通信 + +两个没有祖先后代关系的组件如何共享数据?答案是:**将状态提升到它们的共同父组件中**。 + +```tsx +type ContactId = string; + +function ChatApp() { + const [contacts, setContacts] = useState([]); + const [selectedId, setSelectedId] = useState(null); + const [messages, setMessages] = useState([]); + + const selectedContact = useMemo( + () => contacts.find(c => c.id === selectedId), + [contacts, selectedId] + ); + + return ( +
+ {/* 左侧:联系人列表 */} + + + {/* 右侧:聊天窗口 */} + m.contactId === selectedId)} + onSend={(text) => setMessages(prev => [...prev, { text, contactId: selectedId!, timestamp: Date.now() }])} + /> +
+ ); +} +``` + +> [!diagram] 状态提升流程 +> ```mermaid +> graph LR +> A[ContactList] -- "onSelect(id)" --> P[ChatApp - State] +> P -- "contact" --> B[ChatWindow] +> P -- "onSend(text)" --> P +> B -- "sendMessage(text)" --> P +> style P fill:#4FC08D,color:#fff +> ``` + +> [!tip] 何时应该使用状态提升? +> 当兄弟组件数量少(≤ 3 个)、关系固定、状态不频繁变化时,状态提升比引入状态管理库更简单、更可维护。 ## 状态管理库方案 +当状态需要在整个应用中流转、涉及复杂操作逻辑或多条副作用时,Zustand / Redux Toolkit 等库提供了集中式状态容器。 + ```tsx -// Zustand 方案:任意两个组件共享 state,无需祖先后代关系 +// Zustand:极简 API,按 slice 订阅避免全量重渲染 import { create } from "zustand"; -const useCartStore = create((set) => ({ +interface CartState { + items: CartItem[]; + addItem: (item: CartItem) => void; + removeItem: (id: string) => void; +} + +const useCartStore = create((set) => ({ items: [], addItem: (item) => set(state => ({ items: [...state.items, item] })), + removeItem: (id) => set(state => ({ items: state.items.filter(i => i.id !== id) })), })); -function ProductCard() { - const addItem = useCartStore(s => s.addItem); +function ProductCard({ product }: { product: Product }) { + const addItem = useCartStore(s => s.addItem); // 精确订阅,仅在 addItem 引用变化时重渲染 return ; } function CartIcon() { - const items = useCartStore(s => s.items); + const items = useCartStore(s => s.items); // 仅 items 变化时重渲染 return {items.length}; } // 两者毫无关联,但共享同一份状态 ``` +> [!note] Zustand vs Redux Toolkit 选型建议 +> - **Zustand**:API 简洁、样板代码极少、支持 immer 中间件,适合中小型项目和个人项目 +> - **Redux Toolkit**:生态成熟、DevTools 体验好、middleware 体系完善(thunk/saga),适合大型团队协作项目 +> - 详见 [[hhs/REACT/3. 生态工具篇/09-状态管理.md]] + ## Ref Imperative API(命令式通信) +大多数情况下 React 推崇声明式通信(Props + Callback),但在需要**直接操控子组件实例行为**时使用 refs。典型场景:触发自定义验证、聚焦输入框、播放动画。 + ```tsx const formRef = useRef(null); function Parent() { const handleSubmit = () => { - formRef.current?.validate(); // 命令子组件执行方法 + formRef.current?.validate(); // 命令子组件执行 formRef.current?.reset(); }; - - return <>; + + return ( + <> + + + + ); } const ChildForm = forwardRef((props, ref) => { @@ -157,31 +297,44 @@ const ChildForm = forwardRef((props, ref) => { validate: () => validator.validate(), reset: () => setFields(initialValues), })); - + return
...
; }); ``` +> [!warning] 谨慎使用 Imperative Refs +> Imperative API 打破了 React 的声明式范式。能用 declarative(状态驱动 UI)解决的问题,永远不要上 imperative(直接操控 DOM)。过度使用会导致:难以测试、时序 bug、调试困难。 + ## 决策树 ```mermaid graph TD - A["需要通信?"] -->|"是"| B["通信范围?"] - - B --> C["仅父子两层"] - B --> D["多层级穿透"] - B --> E["跨层级 + 兄弟之间"] - B --> F["需命令式调用子组件方法"] - - C --> G["Props + Callback ✅"] - D --> H["Context ✅"] - E --> I["Zustand / Redux ✅"] - F --> J["forwardRef + useImperativeHandle ✅"] - - style G fill:#4FC08D,color:#fff - style H fill:#61DAFB,color:#000 - style I fill:#F5A87D,color:#000 - style J fill:#A0AEC0,color:#000 + START["需要组件通信?"] -->|"否"| DONE["无需处理 ✅"] + START -->|"是"| RANGE["通信范围?"] + + RANGE --> PARENT_CHILD["仅父子两层
(1-2 级深度)"] + RANGE --> DEEP["多层级穿透
(≥ 3 级深度)"] + RANGE --> SIBLING["兄弟组件 / 无关组件"] + RANGE --> IMPERATIVE["需命令式调用子组件方法"] + + PARENT_CHILD --> PROP["Props + Callback ✅
简单、直观、可追踪"] + DEEP --> CTX["Context ✅
配合 useReducer 保证性能"] + SIBLING --> SLIFTING["状态提升到共同父 ✅"] + SIBLING --> STORE["状态管理库(复杂场景)"] + IMPERATIVE --> REF["forwardRef +
useImperativeHandle ✅"] + + style PROP fill:#4FC08D,color:#fff + style CTX fill:#F5A87D,color:#000 + style SLIFTING fill:#61DAFB,color:#000 + style STORE fill:#C084FC,color:#fff + style REF fill:#A0AEC0,color:#000 ``` ## 关联笔记 + +- [[hhs/REACT/1. 基础篇/03-组件与 Props.md]] — Props 类型校验、组件拆分原则 +- [[hhs/REACT/2. Hooks 篇/05-核心 Hooks.md]] — useContext / useRef 原理 +- [[hhs/REACT/2. Hooks 篇/06-性能优化 Hooks.md]] — 配合 Context 的性能优化手段 +- [[hhs/REACT/4. 进阶篇/12-HOC 与 Render Props.md]] — 高阶组件与 Render Props 进阶 +- [[hhs/REACT/3. 生态工具篇/09-状态管理.md]] — Zustand / Redux Toolkit 选型对比 +- [[hhs/REACT/5. 工程实践篇/15-性能优化.md]] — React.memo、虚拟列表、Bundle 分析 diff --git a/hhs/REACT/4. 进阶篇/12-HOC 与 Render Props.md b/hhs/REACT/4. 进阶篇/12-HOC 与 Render Props.md index ed38a19..7abd2af 100644 --- a/hhs/REACT/4. 进阶篇/12-HOC 与 Render Props.md +++ b/hhs/REACT/4. 进阶篇/12-HOC 与 Render Props.md @@ -7,12 +7,22 @@ create time: 2026-04-29 22:11 ## 概述 -Hooks 出现之前,HOC(高阶组件)和 Render Props 是 React 中复用它逻辑的两种主要模式。理解它们的原理、适用场景和局限,有助于阅读遗留代码并理解为什么 Hooks 成为更优解。 +> [!question] 思考:如果两个组件需要共享同一套数据获取逻辑,该怎么设计? + +Hooks 出现之前,HOC(高阶组件)和 Render Props 是 React 中**复用状态逻辑**的两种主要模式。理解它们的原理、适用场景和局限,不仅有助于阅读遗留代码,更能帮你深刻理解为什么 Hooks 成为更优解——以及哪些场景下旧模式依然不可替代。 + +| 维度 | HOC | Render Props | Custom Hook | +|------|-----|-------------|-------------| +| **本质** | 函数包裹组件 | prop 传递渲染回调 | 纯函数抽离逻辑 | +| **侵入性** | 不改动原组件 JSX | 需修改 JSX 调用处 | 零侵入,仅在 hooks 内使用 | +| **性能影响** | 额外 wrapper 层 | 同上 | **无额外组件树开销** | ## HOC(高阶组件) ### 概念 +> [!question] 类比理解:如果把组件看作"数据 → UI"的转换器,HOC 就是在输入输出之间插入了一层"调料"。 + ```mermaid graph LR A["原始组件 Component"] -->|"注入 props"| B["HOC 函数"] @@ -48,67 +58,135 @@ const StyledTitle = withTheme(function Title({ title }: Props) { }); // ✅ TS 推断 Props 不变 ``` -### 常见 HOC 模式 +> [!note] 核心原理 +> HOC 本质上是**闭包 + 组合**。`WithAuth` 作为一个新的函数组件,可以正常使用 Hooks(因为它是组件),而包裹的 `WrappedComponent` 只负责渲染。这种方式**不修改原组件的任何代码**,仅通过 props 传递增强信息。 + +#### 关键细节:displayName + +调试嵌套 HOC 时,React DevTools 会显示一层层无意义的 wrapper 名。可以用 `displayName` 让调试更清晰: ```tsx -// 1. 日志/HUD +export function withAuth

(Wrapped: React.ComponentType

) { + const WithAuth: React.FC

= (props) => { + // ... 认证逻辑 + return ; + }; + WithAuth.displayName = `withAuth(${Wrapped.name || 'Component'})`; + return WithAuth; +} +// DevTools 中显示:withAuth(Dashboard) → 一目了然 +``` + +### 常见 HOC 模式 + +#### 1. 日志 / HUD + +```tsx +// 通用:为组件自动记录生命周期事件 function withLogger

(Comp: React.FC

): React.FC

{ return function LoggedComponent(props: P) { - useEffect(() => console.log(`${Comp.name} mounted`), []); + useEffect(() => console.log(`[Mount] ${Comp.name || 'Anonymous'}`), []); return ; }; } +``` +> [!tip] 解释 +> 不传任何额外 prop,只"旁路"添加副作用(如日志)。这是 HOC 最轻量的用法——原组件完全不知情。 -// 2. 加载态封装 +#### 2. 加载态封装 + +```tsx +// 自动管理 loading 状态 + 错误处理 function withLoading

( Comp: React.FC

, fetchData: () => Promise ): React.FC

{ return function LoadingWrapper(props: P) { const [loading, setLoading] = useState(true); - useEffect(() => { fetchData().then(() => setLoading(false)); }, []); + const [error, setError] = useState(null); + + useEffect(() => { + fetchData().then(() => setLoading(false)).catch(setError); + }, []); + + if (error) return ; return loading ? : ; }; } +``` +> [!note] 解释 +> HOC 在**不改动组件渲染逻辑**的前提下,统一接管了 "加载中 / 出错 / 成功" 三种 UI 状态。多个页面复用同一套加载策略,DRY。 -// 3. Props 转换(驼峰→kebab) +#### 3. Props 转换 + +```tsx +// 将 props 做格式变换后注入子组件 function withPropsTransformer

( Comp: React.FC

, transform: (p: P) => Record ): React.FC { return function Transformed(props: P) { const extra = transform(props); - return ; + // ⚠️ transform 返回的 key 可能与原 props 冲突 + return ; }; } ``` +> [!warning] 注意 +> 当 `transform` 返回的 key 和原 props 同名时,后者会覆盖前者。实际项目中建议约定命名空间前缀,例如 `apiData`、`cacheMeta` 等。 ### HOC 的局限性 -| 问题 | 说明 | -|------|------| -| **Static 属性丢失** | `withAuth(Page)` 返回的新组件没有 Page.getInitialProps | -| **Wrapper Hell** | `withAuth(withLogging(withData(Page)))`——嵌套过深调试困难 | -| **Props 冲突** | 多个 HOC 都注入 `data` prop,后一个覆盖前一个 | -| **ref 丢失** | 直接传 ref 给增强组件会报错(需用 forwardRef 包装) | +| 问题 | 说明 | 解决方案 | +|------|------|---------| +| **Static 属性丢失** | `withAuth(Page)` 返回的新组件没有 Page.getInitialProps | 手动拷贝:`EnhancedComponent.getInitialProps = Wrapped.getInitialProps` | +| **Wrapper Hell** | `withAuth(withLogging(withData(Page)))`——嵌套过深调试困难 | 用 `compose` 函数扁平化,或直接改用 Custom Hook | +| **Props 冲突** | 多个 HOC 都注入 `data` prop,后一个覆盖前一个 | 使用唯一命名空间前缀(如 `_data` → `useData.data`) | +| **ref 丢失** | 直接传 ref 给增强组件会报错(需用 forwardRef 包装) | 见下方兼容 ref 的方案 | +| **隐式依赖** | HOC 内部调用的 hook 不透明,阅读者不知道注入了什么 | 规范命名(`withXxx`),文档说明行为 | -> [!tip] HOC 兼容 ref -> ```tsx -> export function withAuth

(Wrapped: React.ComponentType

) { -> return React.forwardRef((props, ref) => { -> const { authenticated } = useAuth(); -> if (!authenticated) return ; -> return ; -> }); -> } -> ``` +#### 为什么需要 forwardRef? + +```tsx +export function withAuth

(Wrapped: React.ComponentType

) { + return React.forwardRef((props, ref) => { + const { authenticated } = useAuth(); + if (!authenticated) return ; + return ; + }); +} +``` +> [!tip] 解释 +> 普通函数组件不能接收 `ref`。当 HOC 需要透传 `ref` 给子组件时,必须用 `React.forwardRef` 包裹,否则 React 会抛出 "Function components cannot be given refs" 错误。这增加了额外的样板代码——Hooks 天然解决了这个问题。 + +#### compose 工具 + +当多层 HOC 叠加时,`compose` 可以让可读性更好: + +```tsx +// before: 从内到外,阅读时要先找最内层 +const Enhanced = withAuth(withCache(withPagination(UserList))); + +// after: 从左到右,符合阅读习惯 +const Enhanced = compose( + withAuth, + withCache, + withPagination, +)(UserList); +``` +> [!question] 思考 +> compose 真的改善了可读性吗?如果每个 HOC 的作用一目了然,你是否还需要它? ## Render Props ### 核心思想 +> [!question] 和 HOC 相比,Render Props 到底"额外"给了什么能力? + 通过 prop 传递一个**函数**,该函数返回 JSX——将 UI 渲染逻辑委托给调用方。 +与 HOC 的本质区别:HOC 通过**组件嵌套**注入 props;Render Props 通过**函数传参**直接暴露状态。 + ```tsx interface MouseTrackerProps { render: (position: { x: number; y: number }) => React.ReactNode; @@ -124,6 +202,7 @@ function MouseTracker({ render }: MouseTrackerProps) { }, []); // 🎯 关键:render 函数返回什么就渲染什么 + // 状态在父组件,UI 由调用方决定 return

{render(pos)}
; } @@ -133,10 +212,15 @@ function MouseTracker({ render }: MouseTrackerProps) { )} />; ``` -### 等价于 children 的情况 +> [!note] 解释 +> `MouseTracker` 是**纯粹的关注点分离**:它只负责追踪鼠标位置并触发重渲染,完全不关心屏幕上画什么。这种"关注点解耦"是 HOC 难以优雅表达的。 + +### children 作为 Render Prop + +当 render prop 只是把数据传给子内容时,可以用 `children`(本身也是函数)替代——语义更自然。 ```tsx -// 当 render prop 只是把数据传给 children 时,可以用 children 替代 +// children prop 的类型:接收 data,返回 ReactNode function DataProvider({ children }: { children: (data: Data) => React.ReactNode }) { const data = useDatabase(); return <>{children(data)}; @@ -147,42 +231,86 @@ function DataProvider({ children }: { children: (data: Data) => React.ReactNode ; ``` +> [!tip] children vs render +> - **用 children**:渲染逻辑简单、只需要传入数据 → 代码最简洁 +> - **用 render prop**:需要多个渲染入口(如 `renderEmpty` / `renderLoading`)、或需要对渲染做参数校验 → 类型约束更精确 + +### 常见模式:多回调拆分 + +当渲染逻辑复杂时,可以将一个大 `render` 拆成多个小 prop,降低心智负担: + +```tsx +interface CarouselProps { + slides: Slide[]; + renderItem: (slide: Slide, index: number) => React.ReactNode; + renderIndicator?: (index: number, active: boolean) => React.ReactNode; +} + +function Carousel({ slides, renderItem, renderIndicator }: CarouselProps) { + const [active, setActive] = useState(0); + return ( +
+ {slides.map((s, i) => renderItem(s, i))} +
+ {slides.map((_, i) => renderIndicator?.(i, i === active) ?? ( + setActive(i)} + className={i === active ? 'dot-active' : 'dot'} /> + ))} +
+
+ ); +} +``` +> [!note] 为什么拆比合好? +> 一个巨大的 `render` 回调会让调用处变得冗长。拆成独立的小 prop(`renderItem`、`renderIndicator`),调用方**按需实现**,未实现的可以省略——这是 React 库设计的经典模式。 + ## HOC vs Render Props vs Custom Hook ```mermaid graph TB - A["逻辑复用需求"] --> B["方案对比"] + A["逻辑复用需求"] --> B["三种方案"] - B --> C["HOC"] - B --> D["Render Props"] - B --> E["Custom Hook"] + B --> C["HOC
函数包裹组件"] + B --> D["Render Props
函数传递 prop"] + B --> E["Custom Hook
纯函数抽离逻辑"] - C --> F["⚠️ Wrapper 嵌套深"] - C --> G["⚠️ 静态方法丢失"] - C --> H["✅ 不修改原组件结构"] + C --> C1["⚠️ Wrapper 嵌套深"] + C --> C2["⚠️ 静态方法丢失"] + C --> C3["⚠️ ref 需 forwardRef"] + C --> C4["✅ 不修改原组件 JSX"] - D --> I["⚠️ 回调地狱"] - D --> J["⚠️ Prop 命名冲突风险"] - D --> K["✅ 灵活的 UI 控制"] + D --> D1["⚠️ 回调嵌套过深"] + D --> D2["⚠️ Prop 命名冲突风险"] + D --> D3["✅ UI 控制权完全交给调用方"] - E --> L["✅ 简洁直观"] - E --> M["✅ 可直接操作 state / effect"] - E --> N["✅ 无 wrapper 嵌套"] - E --> O["❌ 只能用于组件内部"] + E --> E1["✅ 扁平可读"] + E --> E2["✅ 直接操作 state / effect"] + E --> E3["✅ 无额外组件树开销"] + E --> E4["❌ 只能在组件内部使用"] - style L fill:#4FC08D,color:#fff - style M fill:#4FC08D,color:#fff - style N fill:#4FC08D,color:#fff + style C4 fill:#4FC08D,color:#fff + style D3 fill:#4FC08D,color:#fff + style E1 fill:#4FC08D,color:#fff + style E2 fill:#4FC08D,color:#fff + style E3 fill:#4FC08D,color:#fff ``` ## 为什么 Hooks 取代了它们? -```tsx -// ❌ HOC 方式 -const ConnectedUserList = withAuth(withCache(withPagination(UserList))); -// 三层嵌套 → 调试困难、性能不可见、type 推导混乱 +Hooks 的核心理念是**把状态逻辑从组件中抽离出来,但保持调用处扁平**。它结合了 HOC 和 Render Props 的优点: -// ✅ Hook 方式 +| 维度 | HOC / Render Props | Custom Hook | +|------|-------------------|-------------| +| **嵌套层级** | 多层 wrapper 或回调嵌套 | **零嵌套,直接平铺** | +| **状态共享** | 需要 prop 层层传递,容易混乱 | 每个 hook 管理自己的 state,互不干扰 | +| **TypeScript** | 泛型约束复杂,组合后类型可能丢失 | **完美推断**,不需要额外 type wrangling | +| **调试** | DevTools 多一层 wrapper,栈更混乱 | 与普通函数无异 | + +```tsx +// ❌ HOC 方式:三层嵌套 → 调试困难、性能不可见、type 推导混乱 +const ConnectedUserList = withAuth(withCache(withPagination(UserList))); + +// ✅ Hook 方式:扁平可读,逻辑一目了然 function UserList() { useAuth(); // 身份验证 const cache = useCache(); // 数据缓存 @@ -190,12 +318,24 @@ function UserList() { return {/* ... */}
; } -// 扁平可读、天然共享 state、TS 完美推断 ``` -> [!note] HOC 和 Render Props 真的被淘汰了吗? -> - HOC:在需要**包裹**组件但不修改其内部的场景仍有价值(如第三方库封装) -> - Render Props:当父组件需要**完全控制子组件的渲染内容**时仍然有用 -> - 但 90%+ 的场景,Custom Hook 是更好的选择 +> [!question] 思考 +> Hooks 真的完全淘汰了 HOC 和 Render Props 吗?回想一下文档开头的两个核心概念——HOC 通过**包裹**增强组件,Render Props 通过**回调**暴露渲染控制。这两种模式在哪些场景中仍然无法被 Hook 替代? + +#### 何时仍应使用旧模式? + +| 场景 | 推荐方案 | 原因 | +|------|---------|------| +| 第三方库封装(不接触用户代码) | **HOC** | 库方只操作组件类,无需知道用户内部结构 | +| UI 框架需要提供渲染占位符 | **Render Props** | 父组件需要定义多个渲染回调(如 `renderEmpty`、`renderLoading`) | +| 通用业务逻辑复用 | **Custom Hook** | 首选方案:简洁、可组合、无 DOM 开销 | ## 关联笔记 + +- [[00.Readme]] +- [[3. Hooks 篇/01-为什么需要 Hooks]] +- [[3. Hooks 篇/02-useEffect 深度解析]] +- [[3. Hooks 篇/08-自定义 Hook 最佳实践]] +- [[4. 进阶篇/07-forwardRef 与 useImperativeHandle]] +- [[4. 进阶篇/11-组件 Composition 模式]] \ No newline at end of file diff --git a/hhs/REACT/4. 进阶篇/13-并发特性.md b/hhs/REACT/4. 进阶篇/13-并发特性.md index 63f73c9..04ff18e 100644 --- a/hhs/REACT/4. 进阶篇/13-并发特性.md +++ b/hhs/REACT/4. 进阶篇/13-并发特性.md @@ -7,7 +7,11 @@ create time: 2026-04-29 22:12 ## 概述 -React 18 引入了并发渲染(Concurrent Rendering)架构,将 UI 更新划分为可中断、可恢复、可优先级调度的任务。理解并发的核心概念——Suspend、Transition、时间切片——能帮助你写出更流畅的用户体验。 +React 18 引入了并发渲染(Concurrent Rendering)架构,将 UI 更新划分为**可中断、可恢复、可优先级调度**的任务。理解并发的核心概念——Suspend、Transition、时间切片——能帮助你写出更流畅的用户体验。 + +> [!question] 思考:如果一次 setState 触发了大量 DOM 更新,为什么页面不会卡死? +> +> 答案就在 React 的 Fiber 架构里——它将渲染工作切分成小单元,每个单元完成后让出主线程给浏览器处理高优任务(如点击响应)。这就是「并发」的本质。 ## React 渲染架构演进 @@ -37,6 +41,28 @@ const SettingsPage = lazy(() => import("./pages/SettingsPage")); ``` +### Suspense + SSR(流式渲染) + +> [!tip] React 18 的 Streaming SSR +> +> Suspense 在 SSR 中的核心价值:服务端可以「分块」返回 HTML,用户先看到首屏骨架,数据就绪后逐步替换。这比传统的「等所有数据都就绪再输出完整 HTML」快得多。 + +```tsx +// 服务端组件中嵌套 Suspense boundary +export default async function Page() { + return ( +
+
{/* 同步渲染 */} + }> + {/* 等待 fetch 完成后插入 */} + +
+ ); +} +``` + +服务端输出变成**渐进式 HTML 流**:`Header → SearchSkeleton → SearchResults(loaded)` + ### Suspense + Data Fetching(实验性 API) ```tsx @@ -60,6 +86,10 @@ function Profile() { ## useTransition —— 标记低优先级更新 +> [!question] 什么时候该用 Transition? +> +> 当一次用户交互(如点击、选择)会触发多个状态更新,其中某些更新的 UI 渲染代价高昂时——你可以把「即时反馈」和「延迟渲染」分开。 + ```tsx function SearchPage() { const [query, setQuery] = useState(""); @@ -88,16 +118,40 @@ function SearchPage() { } ``` -### Transition 与直接 setState 对比 +### useTransition + Suspense 组合模式 -```mermaid -timeline - title "输入 "hello" 的渲染行为" - - 直接 setState : 每次按键 → re-render\n(h/h/e/l/o 共 5 次) - useTransition : h,e,l,l → 跳过中间\no → 最终渲染一次 +两者配合可以实现更精细的加载策略:Transition 控制哪个更新「可以等待」,Suspense 在数据就绪后展示最终内容。 + +```tsx +const [isPending, startTransition] = useTransition(); +const deferredTab = useDeferredValue(activeTab); + +return ( + <> + + {isPending && } + }> + + + +); ``` +### Transition vs DeferredValue 选择指南 + +| 场景 | 推荐 | 原因 | +|------|------|------| +| 路由切换后展示新视图 | `useTransition` | 明确标记整页过渡状态,配合 `` | +| 输入框即时搜索 + 延迟加载 | `useDeferredValue` | 一行搞定,自动处理防抖逻辑 | +| 多个状态联动更新 | `useTransition` | 更精确控制哪些 setState 属于低优先级 | +| 简单延迟一个值(如过滤条件) | `useDeferredValue` | 侵入性最小,不改变组件交互模式 | +| Tab 切换时内容区域的异步加载 | 两者组合 | `startTransition` 控制页面级 Pending,`useDeferredValue` 控制数据流 | + +> [!tip] 核心区别一句话 +> +> - **`useTransition`**:控制**「哪个 setState」**是低优先级的 → 你直接包裹需要降级的那段 setState 调用。 +> - **`useDeferredValue`**:控制**「哪个值」**是延迟更新的 → 你用 `const delayed = useDeferredValue(value)` 拿到一个滞后 ~16ms 的副本。 + ## useDeferredValue —— 延迟副本 ```tsx @@ -117,14 +171,9 @@ function TodoApp() { } ``` -> [!tip] Transition vs DeferredValue 选择指南 +> [!note] 内部机制 > -> | 场景 | 推荐 | -> |------|------| -> | 表单提交后展示新视图 | `useTransition` | -> | 输入框即时搜索 + 延迟加载 | `useDeferredValue` | -> | 多个状态联动更新 | `useTransition`(更明确控制粒度) | -> | 简单延迟一个值 | `useDeferredValue`(一行搞定) | +> `useDeferredValue` 内部等价于一次 `startTransition`。调用后 React 立即用旧值渲染,然后调度一次低优先级的 re-render 来更新为新值。你可以理解为「自动防抖 + 优先级降级」。 ## 时间切片原理 @@ -174,4 +223,46 @@ useEffect(() => { // 用于发现不纯的 render 函数和清理逻辑缺失 ``` +## 最佳实践 + +> [!tip] 实战经验总结 + +### 代码示例:安全的 Effect Cleanup + +```tsx +// ❌ 问题:异步 render → effect 执行时 dep 可能已变化 +useEffect(() => { + const controller = new AbortController(); + fetchData(url, { signal: controller.signal }).then(setData); + return () => controller.abort(); // cleanup 依赖的 url 可能已过时 +}, [dep]); + +// ✅ 推荐:用 ref 缓存最新值,保证 cleanup 拿到正确的信号 +const latestSignalRef = useRef(); + +useEffect(() => { + const controller = new AbortController(); + latestSignalRef.current = controller; + + fetchData(url, { signal: controller.signal }).then(setData); + + return () => { + if (latestSignalRef.current === controller) { + controller.abort(); // 确认是同一个请求才 abort + } + }; +}, [dep]); +``` + +### 要点清单 + +1. **优先使用 Suspense,谨慎手动 startTransition**:Suspense 配合数据获取模式是 React 官方推荐的方向,而 `startTransition` 适合「一个交互触发多个 setState」的场景。 +2. **避免在 render 中做副作用**:并发模式下 render 函数可能被多次调用、随时中断——render 应该是纯粹的「视图描述函数」。 +3. **合理使用 `` boundary 层级**:不要把所有组件包在一个大 Suspense 里,也不要每个小组件都加——根据网络请求和数据依赖划分边界。 +4. **用 `use()` + Suspense 替代 useEffect 中的数据获取**:实验性但代表未来方向,能消除竞态条件(race condition)和 loading 状态管理样板代码。 +5. **警惕 `useEffect` 的异步执行时机**:如果 effect 依赖于某个 state 的值来做 cleanup,考虑改用 `useSyncExternalStore` 或使用 ref 存储最新值。 + ## 关联笔记 +- [[11-组件通信模式]] +- [[12-HOC 与 Render Props]] +- [[14-Serverside Rendering]] diff --git a/hhs/REACT/4. 进阶篇/14-Serverside Rendering.md b/hhs/REACT/4. 进阶篇/14-Serverside Rendering.md index 5e10b9e..0a82bb1 100644 --- a/hhs/REACT/4. 进阶篇/14-Serverside Rendering.md +++ b/hhs/REACT/4. 进阶篇/14-Serverside Rendering.md @@ -1,5 +1,5 @@ --- -tags: [React, Next.js, SSR, SSG, ISR, Frontend] +tags: [React, Next.js, SSR, SSG, ISR, Frontend, Server Components] create time: 2026-04-29 22:13 --- @@ -7,30 +7,34 @@ create time: 2026-04-29 22:13 ## 概述 -服务端渲染(SSR)让 React 组件在服务器端预渲染为 HTML,显著改善首屏加载速度和 SEO。本文档以 Next.js App Router 为核心,介绍 SSR/SSG/ISR 的渲染策略与最佳实践。 +服务端渲染(SSR)让 React 组件在服务器端预渲染为 HTML,显著改善首屏加载速度和 SEO。本文档以 Next.js App Router 为核心,介绍 SSR/SSG/ISR 的渲染策略、Server Components 体系与最佳实践。 + +> [!question] 思考:用户从输入 URL 到看到页面,中间经历了哪些步骤? +> +> 传统 CSR 模式下,浏览器先拿到一个几乎空的 HTML,然后下载 JS bundle,执行 React 来生成内容——用户需要等待两件事都完成才能看到页面。SSR 把「生成 HTML」这一步搬到服务器做,用户请求回来时就已经有可读的内容了。 ## 渲染模式对比 ```mermaid graph TB - subgraph "客户端渲染 CSR" - A[HTML空白页] --> B["下载 JS Bundle"] - B --> C["执行 React hydration"] + subgraph CSR["客户端渲染 CSR"] + A["HTML空白页"] --> B["下载JS Bundle"] + B --> C["执行React hydration"] C --> D["显示内容"] end - - subgraph "服务端渲染 SSR" - E[请求页面] --> F["服务器渲染 React → HTML"] - F --> G["发送含内容的 HTML"] - G --> H["客户端 hydration"] + + subgraph SSR["服务端渲染 SSR"] + E["请求页面"] --> F["服务器渲染React -> HTML"] + F --> G["发送含内容的HTML"] + G --> H["客户端hydration"] H --> I["交互可用"] end - - subgraph "静态生成 SSG" - J["构建时渲染"] --> K["生成纯 HTML 文件"] - K --> L["CDN 分发"] + + subgraph SSG["静态生成 SSG"] + J["构建时渲染"] --> K["生成纯HTML文件"] + K --> L["CDN分发"] end - + style F fill:#4FC08D,color:#fff style H fill:#F5A87D,color:#000 style J fill:#61DAFB,color:#000 @@ -44,8 +48,18 @@ graph TB | **SSG**(Static Site Generation) | 博客、文档、营销页 | ⏱️ 构建时生成 | ✅ | | **ISR**(Incremental Static Regeneration) | 新闻列表、商品目录 | 🔄 定时后台更新 | ✅(增量) | +> [!tip] 核心区别一句话 +> +> - **SSR**:每个用户请求都触发一次服务端的完整渲染。 +> - **SSG**:构建时生成一次 HTML,所有用户共享同一个静态页面。 +> - **ISR**:先用 SSG 生成的静态页面响应,后台静默重新生成后替换——对用户无感知。 + ## Next.js App Router 架构 +> [!note] Server Component 是默认值 +> +> Next.js App Router 中,所有组件默认就是 **Server Component**——除非你在文件顶部声明 `"use client"`。这意味着你可以放心地在组件里 await 数据库查询、读取环境变量或访问文件系统,这些代码永远不会发送到浏览器。 + ```tsx // app/layout.tsx —— 根布局(所有页面共享) export default function RootLayout({ children }: { children: React.ReactNode }) { @@ -63,7 +77,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) async function HomePage() { // ✅ 直接在组件中 await API const posts = await fetchPosts(); - + return (
{posts.map(post => )} @@ -84,17 +98,17 @@ async function PostPage({ params }: { params: { slug: string } }) { sequenceDiagram participant Client as 浏览器 participant Server as 服务器 - + Client->>Server: GET /dashboard Server->>Server: 并行请求 user/profile/orders - Note over Server: 每个请求可有自己的 Suspense boundary - - Server-->>Client: HTML: Navbar + Sidebar
(立即显示,~200ms) - - Server-->>Client: Stream: Profile card
(中等优先级,~800ms) - - Server-->>Client: Stream: Order history
(低优先级,~1500ms) - + Note over Server: 每个请求可有自己的Suspense boundary + + Server-->>Client: HTML: Navbar + Sidebar
(立即显示,约200ms) + + Server-->>Client: Stream: Profile card
(中等优先级,约800ms) + + Server-->>Client: Stream: Order history
(低优先级,约1500ms) + Note over Client: 用户体验:渐进式展示,而非等全部完成 ``` @@ -107,9 +121,9 @@ function DashboardLayout({ children }: { children: React.ReactNode }) { }> - + {children} - + {/* 各区域独立 Suspense */} }> @@ -122,6 +136,10 @@ function DashboardLayout({ children }: { children: React.ReactNode }) { } ``` +> [!note] Streaming SSR 的工作原理 +> +> 服务端将 HTML 分成多个 chunk,通过网络流逐步发送给浏览器。浏览器边收边渲染——不需要等所有数据就绪。**优先级高的区域先出,低的在后**,用户体验从「全部等」变成「渐进可见」。 + ## Data Fetching 策略 ```tsx @@ -151,7 +169,16 @@ function Page() { } ``` -## SSR vs Client Component 边界 +### Cache vs Revalidate vs no-store 决策指南 + +| 策略 | 行为 | 适合场景 | +|------|------|---------| +| `fetch(url)` 不加配置 | 基于 HTTP 协议的永久缓存(直到下次部署失效) | 不常变化的配置数据 | +| `{ next: { revalidate: N } }` | CDN 级别缓存,N 秒后后台重新验证 | 商品信息、文章列表等 | +| `{ cache: "no-store" }` | 完全不走缓存,每次都回源 | 用户面板、实时仪表盘 | +| `{ next: { tags: ["posts"] } }` + `revalidateTag("posts")` | 按标签精确失效 | CMS 系统发布新文章时触发更新 | + +## Server Component vs Client Component 边界 ```tsx // server component(默认,无需声明) @@ -168,7 +195,7 @@ function InteractiveChart() { // ✅ 可以使用 useState/useEffect/DOM API const [zoom, setZoom] = useState(1); useEffect(() => { ... }, []); - + return ; } @@ -184,7 +211,219 @@ function Dashboard() { ``` > [!warning] Client → Server 通信限制 +> > - Client Component 无法直接调用 Server Component 的方法或 props +> - Server Component 也不能传给 Client Component 引用了函数、Promise 或 Generator 的值 > - 解决方案:通过 URL 参数、cookies、或后端 API 传递数据 +### 何时该用 Server Component?何时该用 Client Component? + +> [!question] 如何判断一个组件应该放在哪一边? +> +> 记住一条黄金法则:**能放服务器的就放服务器**。Server Component 是零 bundle size 的——它们不会增加客户端 JavaScript 体积。只有当你确实需要浏览器专属 API(事件监听、状态管理、DOM 操作)时才降级到 Client Component。 + +| 需要浏览器能力吗? | 推荐方案 | +|-------------------|---------| +| ❌ 不需要(只渲染数据) | Server Component ✅ | +| ✅ 需要 `useState` / `useEffect` / `onClick` | Client Component (`"use client"`) | +| ✅ 需要第三方交互库(地图、图表) | Client Component | +| ✅ 需要浏览器 API(localStorage、Geolocation) | Client Component | +| ❌ 只需要从 API 获取数据并展示 | Server Component ✅ | + +## Server Actions + +Server Action 允许你在服务端定义可直接从 Client Component 调用的异步函数——无需手动写 API 路由。 + +```tsx +// app/actions.ts —— 独立的 Server Action 模块 +"use server"; + +import { revalidatePath } from "next/cache"; + +export async function createPost(formData: FormData) { + const title = formData.get("title") as string; + const content = formData.get("content") as string; + + // 数据库写入 + await db.post.create({ data: { title, content } }); + + // 成功后刷新对应页面的缓存 + revalidatePath("/blog"); +} +``` + +```tsx +// app/blog/new/page.tsx —— 表单页面(Client Component) +"use client"; + +import { createPost } from "@/app/actions"; + +function NewPostForm() { + async function handleSubmit(formData: FormData) { + await createPost(formData); + // 提交后跳转到列表页 + router.push("/blog"); + } + + return ( +
+ +