差别
这里会显示出您选择的修订版和当前版本之间的差别。
| 两侧同时换到之前的修订记录 前一修订版 后一修订版 | 前一修订版 | ||
| react:组件基础 [2026/04/07 09:52] – 张叶安 | react:组件基础 [2026/04/07 11:03] (当前版本) – [5.19 useReducer - 复杂状态管理] 张叶安 | ||
|---|---|---|---|
| 行 1: | 行 1: | ||
| - | ====== 第五章:组件基础 ====== | + | ====== 第五章:React 组件基础 ====== |
| ===== 5.1 函数组件 ===== | ===== 5.1 函数组件 ===== | ||
| 行 429: | 行 429: | ||
| 组件是 React 的核心概念,理解组件的各种用法是掌握 React 的关键。 | 组件是 React 的核心概念,理解组件的各种用法是掌握 React 的关键。 | ||
| - | ====== useEffect 副作用管理 ====== | + | ===== 5.12 React Hooks 简介 |
| - | ===== 6.1 useEffect 简介 ===== | + | Hooks 是 React 16.8 引入的新特性,它让我们在函数组件中使用 state 和其他 React 特性,而无需编写类组件。 |
| - | useEffect 是 React Hooks 中最重要、最基础的 Hook 之一,用于在函数组件中执行副作用操作。所谓副作用,是指那些不直接参与 UI 渲染、但会对组件外部产生影响的操作,例如数据获取、订阅、手动修改 DOM 等。 | + | **为什么需要 Hooks**: |
| - | 在类组件时代,我们通常使用生命周期方法(如 componentDidMount、componentDidUpdate、componentWillUnmount)来处理副作用。而 useEffect | + | * 类组件中的 this 指向难以理解 |
| + | * 生命周期方法中常常包含不相关的逻辑,而相关逻辑分散在不同生命周期中 | ||
| + | * 高阶组件和 render props 会导致组件嵌套地狱 | ||
| + | * 复用状态逻辑困难 | ||
| - | **基本语法**: | + | **Hooks 使用规则**: |
| + | |||
| + | * 只在最顶层调用 Hooks,不要在循环、条件或嵌套函数中调用 | ||
| + | * 只在 React 函数组件或自定义 Hooks 中调用 Hooks | ||
| + | |||
| + | ===== 5.13 useState - 状态管理 ===== | ||
| + | |||
| + | useState 是最常用的 Hook,用于在函数组件中添加状态。 | ||
| + | |||
| + | **基本用法**: | ||
| <code javascript> | <code javascript> | ||
| - | import { useEffect | + | import { useState |
| - | function | + | function |
| - | | + | // 声明一个叫 " |
| - | | + | const [count, setCount] = useState(0); |
| - | | + | |
| - | + | return ( | |
| - | // 可选的清理函数 | + | |
| - | | + | < |
| - | console.log(' | + | < |
| - | | + | |
| - | }, [/* 依赖数组 */]); | + | </button> |
| - | + | | |
| - | return | + | ); |
| } | } | ||
| </ | </ | ||
| - | useEffect 接收两个参数:第一个参数是一个执行副作用的函数,该函数可以返回一个清理函数(可选);第二个参数是依赖数组,用于控制 effect 的执行时机。 | + | **解构赋值说明**: |
| - | ===== 6.2 useEffect | + | `const [count, setCount] |
| + | - count: 当前状态值 | ||
| + | - setCount: 更新状态的函数 | ||
| - | 理解 useEffect 的执行时机是掌握它的关键。默认情况下,React 会在每次渲染完成后执行 useEffect 中的副作用函数。 | + | **函数式更新**: |
| - | **挂载时执行**: | + | 当新状态依赖于旧状态时,使用函数式更新可以避免闭包问题。 |
| - | 当依赖数组为空数组 `[]` 时,effect 只在组件挂载时执行一次,类似于类组件的 componentDidMount。 | + | <code javascript> |
| + | function Counter() { | ||
| + | const [count, setCount] = useState(0); | ||
| + | |||
| + | // 正确:使用函数式更新 | ||
| + | const increment = () => { | ||
| + | setCount(prevCount => prevCount + 1); | ||
| + | }; | ||
| + | |||
| + | return ( | ||
| + | < | ||
| + | < | ||
| + | <button onClick={increment}> | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **对象状态**: | ||
| + | |||
| + | 当 state 是对象时,更新时需要展开旧值。 | ||
| <code javascript> | <code javascript> | ||
| - | function | + | function |
| - | | + | |
| - | | + | |
| - | | + | |
| - | | + | email: '' |
| - | + | }); | |
| - | return <div>用户列表</ | + | |
| + | | ||
| + | setUser(prevUser => ({ | ||
| + | ...prevUser, | ||
| + | | ||
| + | })); | ||
| + | | ||
| + | |||
| + | return | ||
| + | | ||
| + | <input | ||
| + | value={user.name} | ||
| + | onChange={e => handleChange(' | ||
| + | placeholder=" | ||
| + | /> | ||
| + | | ||
| + | ); | ||
| } | } | ||
| </ | </ | ||
| - | **依赖变化时执行**: | + | **延迟初始化**: |
| - | 当依赖数组中包含特定的 state 或 props 时,只有当这些依赖项发生变化时,effect 才会重新执行。 | + | 当初始值计算开销较大时,可以传入函数进行延迟初始化。 |
| <code javascript> | <code javascript> | ||
| - | function | + | function |
| - | const [count, setCount] = useState(0); | + | // 只会在首次渲染时执行 computeExpensiveValue |
| + | const [value, setValue] = useState(() => computeExpensiveValue()); | ||
| | | ||
| - | // 当 userId 变化时执行 | + | // ... |
| + | } | ||
| + | </ | ||
| + | |||
| + | ===== 5.14 useEffect - 副作用管理 ===== | ||
| + | |||
| + | useEffect 是 React Hooks 中最重要、最基础的 Hook 之一,用于在函数组件中执行副作用操作。 | ||
| + | |||
| + | **基本语法**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { useEffect } from ' | ||
| + | |||
| + | function Example() { | ||
| useEffect(() => { | useEffect(() => { | ||
| - | console.log(`用户ID变化为: | + | |
| - | | + | |
| - | }, [userId]); | + | |
| + | // 可选的清理函数 | ||
| + | return | ||
| + | console.log(' | ||
| + | }; | ||
| + | }, [/* 依赖数组 */]); | ||
| | | ||
| - | | + | return <div>示例组件</div>; |
| - | useEffect(() => { | + | |
| - | console.log(`计数变化为: | + | |
| - | document.title = `点击了 ${count} 次`; | + | |
| - | }, [count]); | + | |
| - | + | ||
| - | | + | |
| } | } | ||
| </ | </ | ||
| - | **每次渲染都执行**: | + | **执行时机**: |
| - | + | ||
| - | 如果不传递依赖数组,useEffect 会在每次组件渲染后都执行。这种方式在实际开发中较少使用,因为可能导致性能问题和无限循环。 | + | |
| <code javascript> | <code javascript> | ||
| + | // 1. 每次渲染后都执行 | ||
| useEffect(() => { | useEffect(() => { | ||
| console.log(' | console.log(' | ||
| }); | }); | ||
| - | </ | ||
| - | ===== 6.3 清理函数(Cleanup) ===== | + | // 2. 只在挂载时执行(空依赖数组) |
| + | useEffect(() | ||
| + | console.log(' | ||
| + | fetchData(); | ||
| + | }, []); | ||
| - | 某些副作用需要清理,例如订阅外部数据源、设置定时器、添加事件监听器等。如果在组件卸载时不清理这些副作用,可能会导致内存泄漏。 | + | // 3. 依赖变化时执行 |
| - | + | useEffect(() => { | |
| - | useEffect 允许通过返回一个函数来指定清理逻辑。React 会在组件卸载时执行这个清理函数,也会在重新执行 effect 之前调用它来清理上一个 effect。 | + | |
| + | document.title = `点击了 ${count} 次`; | ||
| + | }, [count]); | ||
| + | </ | ||
| - | **订阅和取消订阅**: | + | **清理函数(Cleanup)**: |
| <code javascript> | <code javascript> | ||
| 行 539: | 行 607: | ||
| </ | </ | ||
| - | 当 roomId 从 " | + | **数据获取模式**: |
| - | 1. 使用 " | + | |
| - | 2. 使用 " | + | |
| - | + | ||
| - | **定时器的清理**: | + | |
| - | + | ||
| - | <code javascript> | + | |
| - | function Timer() { | + | |
| - | const [seconds, setSeconds] = useState(0); | + | |
| - | + | ||
| - | useEffect(() => { | + | |
| - | const intervalId = setInterval(() => { | + | |
| - | setSeconds(s => s + 1); | + | |
| - | }, 1000); | + | |
| - | + | ||
| - | return () => { | + | |
| - | clearInterval(intervalId); | + | |
| - | console.log(' | + | |
| - | }; | + | |
| - | }, []); | + | |
| - | + | ||
| - | return < | + | |
| - | } | + | |
| - | </ | + | |
| - | + | ||
| - | **事件监听器的清理**: | + | |
| - | + | ||
| - | <code javascript> | + | |
| - | function WindowSize() { | + | |
| - | const [size, setSize] = useState({ width: window.innerWidth, | + | |
| - | + | ||
| - | useEffect(() => { | + | |
| - | const handleResize = () => { | + | |
| - | setSize({ width: window.innerWidth, | + | |
| - | }; | + | |
| - | + | ||
| - | window.addEventListener(' | + | |
| - | + | ||
| - | return () => { | + | |
| - | window.removeEventListener(' | + | |
| - | }; | + | |
| - | }, []); | + | |
| - | + | ||
| - | return ( | + | |
| - | < | + | |
| - | 窗口宽度: | + | |
| - | 窗口高度: | + | |
| - | </ | + | |
| - | ); | + | |
| - | } | + | |
| - | </ | + | |
| - | + | ||
| - | ===== 6.4 数据获取模式 | + | |
| - | + | ||
| - | 数据获取是 useEffect 最常见的使用场景之一。掌握正确的数据获取模式对于避免竞态条件和内存泄漏至关重要。 | + | |
| - | + | ||
| - | **基本的数据获取**: | + | |
| <code javascript> | <code javascript> | ||
| 行 605: | 行 617: | ||
| useEffect(() => { | useEffect(() => { | ||
| let isCancelled = false; | let isCancelled = false; | ||
| + | const controller = new AbortController(); | ||
| | | ||
| async function fetchUser() { | async function fetchUser() { | ||
| try { | try { | ||
| setLoading(true); | setLoading(true); | ||
| - | const response = await fetch(`/ | + | const response = await fetch(`/ |
| + | signal: controller.signal | ||
| + | }); | ||
| const data = await response.json(); | const data = await response.json(); | ||
| | | ||
| - | // 检查组件是否已卸载或 userId 是否已改变 | ||
| if (!isCancelled) { | if (!isCancelled) { | ||
| setUser(data); | setUser(data); | ||
| 行 618: | 行 632: | ||
| } | } | ||
| } catch (err) { | } catch (err) { | ||
| - | if (!isCancelled) { | + | if (!isCancelled |
| setError(err.message); | setError(err.message); | ||
| - | setUser(null); | ||
| } | } | ||
| } finally { | } finally { | ||
| 行 633: | 行 646: | ||
| return () => { | return () => { | ||
| isCancelled = true; | isCancelled = true; | ||
| + | controller.abort(); | ||
| }; | }; | ||
| }, [userId]); | }, [userId]); | ||
| 行 638: | 行 652: | ||
| if (loading) return < | if (loading) return < | ||
| if (error) return < | if (error) return < | ||
| - | if (!user) return null; | ||
| | | ||
| return ( | return ( | ||
| 行 649: | 行 662: | ||
| </ | </ | ||
| - | **处理竞态条件**: | + | **useEffect 与 useLayoutEffect**: |
| - | + | ||
| - | 当依赖项快速变化时(如用户快速切换选项),可能会出现竞态条件——先发出的请求后返回,导致显示错误的数据。使用取消标志或 AbortController 可以解决这个问题。 | + | |
| <code javascript> | <code javascript> | ||
| - | function SearchResults({ query }) { | + | import |
| - | const [results, setResults] = useState([]); | + | |
| + | function MeasureExample() { | ||
| + | const divRef = useRef(null); | ||
| + | const [width, setWidth] = useState(0); | ||
| | | ||
| + | useLayoutEffect(() => { | ||
| + | // 在浏览器绘制前测量 DOM | ||
| + | const { width } = divRef.current.getBoundingClientRect(); | ||
| + | setWidth(width); | ||
| + | }, []); | ||
| + | | ||
| + | return ( | ||
| + | <div ref={divRef}> | ||
| + | 宽度: {width}px | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | - useEffect:在浏览器绘制完成后异步执行,不会阻塞视觉更新 | ||
| + | - useLayoutEffect:在浏览器绘制之前同步执行,会阻塞视觉更新 | ||
| + | |||
| + | 绝大多数情况下应该使用 useEffect,只有在需要同步测量/ | ||
| + | |||
| + | |||
| + | ===== 5.15 useRef - DOM 引用与持久化值 ===== | ||
| + | |||
| + | useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。这个对象在组件的整个生命周期内保持不变。 | ||
| + | |||
| + | **访问 DOM 元素**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { useRef } from ' | ||
| + | |||
| + | function TextInput() { | ||
| + | const inputRef = useRef(null); | ||
| + | |||
| + | const focusInput = () => { | ||
| + | // 直接访问 DOM 元素 | ||
| + | inputRef.current.focus(); | ||
| + | }; | ||
| + | |||
| + | return ( | ||
| + | <div> | ||
| + | <input ref={inputRef} type=" | ||
| + | <button onClick={focusInput}> | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **保存上一次的值**: | ||
| + | |||
| + | <code javascript> | ||
| + | function Counter() { | ||
| + | const [count, setCount] = useState(0); | ||
| + | const prevCountRef = useRef(); | ||
| + | |||
| useEffect(() => { | useEffect(() => { | ||
| - | | + | |
| - | + | prevCountRef.current | |
| - | async function search() { | + | }); |
| - | | + | |
| - | const response | + | const prevCount = prevCountRef.current; |
| - | | + | |
| - | }); | + | return |
| - | const data = await response.json(); | + | <div> |
| - | | + | |
| - | } catch (error) { | + | < |
| - | if (error.name === ' | + | </ |
| - | | + | ); |
| - | | + | } |
| - | | + | </ |
| - | console.error(' | + | |
| - | } | + | **保存定时器 ID**: |
| + | |||
| + | <code javascript> | ||
| + | function Timer() { | ||
| + | const [seconds, setSeconds] = useState(0); | ||
| + | const [isRunning, setIsRunning] | ||
| + | const intervalRef = useRef(null); | ||
| + | |||
| + | const start = () => { | ||
| + | if (!isRunning) { | ||
| + | | ||
| + | | ||
| + | | ||
| + | }, 1000); | ||
| } | } | ||
| - | | + | }; |
| - | if (query) { | + | |
| - | | + | const stop = () => { |
| - | } else { | + | if (isRunning) { |
| - | | + | |
| + | | ||
| } | } | ||
| - | | + | }; |
| + | |||
| + | const reset = () => { | ||
| + | stop(); | ||
| + | setSeconds(0); | ||
| + | }; | ||
| + | |||
| + | useEffect(() => { | ||
| + | // 组件卸载时清理定时器 | ||
| return () => { | return () => { | ||
| - | | + | |
| + | clearInterval(intervalRef.current); | ||
| + | } | ||
| }; | }; | ||
| - | }, [query]); | + | }, []); |
| - | + | ||
| return ( | return ( | ||
| - | <ul> | + | <div> |
| - | {results.map(item => <li key={item.id}>{item.name}</li>)} | + | |
| - | </ul> | + | |
| + | <button onClick={stop}>停止</button> | ||
| + | <button onClick={reset}> | ||
| + | </div> | ||
| ); | ); | ||
| } | } | ||
| </ | </ | ||
| - | ===== 6.5 多个 useEffect 的分离 ===== | + | **转发 ref(forwardRef)**: |
| - | 与类组件的生命周期方法不同,useEffect 鼓励我们将相关的逻辑放在一起,而不是按生命周期阶段组织代码。这样可以更好地分离关注点,使代码更易读、更易维护。 | + | <code javascript> |
| + | import { forwardRef, useRef } from ' | ||
| - | **分离不相关的副作用**: | + | // 子组件转发 ref 到内部 DOM 元素 |
| + | const FancyInput = forwardRef((props, | ||
| + | return <input ref={ref} className=" | ||
| + | }); | ||
| + | |||
| + | // 父组件使用 | ||
| + | function Parent() { | ||
| + | const inputRef = useRef(null); | ||
| + | |||
| + | const focusInput = () => { | ||
| + | inputRef.current.focus(); | ||
| + | }; | ||
| + | |||
| + | return ( | ||
| + | < | ||
| + | < | ||
| + | <button onClick={focusInput}> | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **useImperativeHandle 自定义暴露的实例值**: | ||
| <code javascript> | <code javascript> | ||
| - | function | + | import { forwardRef, useRef, useImperativeHandle } from ' |
| + | |||
| + | const FancyInput = forwardRef((props, | ||
| + | const inputRef = useRef(null); | ||
| + | |||
| + | // 自定义暴露给父组件的方法 | ||
| + | useImperativeHandle(ref, | ||
| + | focus: () => { | ||
| + | inputRef.current.focus(); | ||
| + | }, | ||
| + | clear: () => { | ||
| + | inputRef.current.value = ''; | ||
| + | }, | ||
| + | getValue: () => { | ||
| + | return inputRef.current.value; | ||
| + | } | ||
| + | })); | ||
| + | |||
| + | return <input ref={inputRef} {...props} />; | ||
| + | }); | ||
| + | |||
| + | // 父组件使用 | ||
| + | function | ||
| + | const fancyInputRef = useRef(null); | ||
| + | |||
| + | const handleClick = () => { | ||
| + | fancyInputRef.current.focus(); | ||
| + | console.log(fancyInputRef.current.getValue()); | ||
| + | fancyInputRef.current.clear(); | ||
| + | }; | ||
| + | |||
| + | return ( | ||
| + | < | ||
| + | < | ||
| + | <button onClick={handleClick}> | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ===== 5.16 useCallback - 缓存回调函数 ===== | ||
| + | |||
| + | useCallback 返回一个 memoized 回调函数。只有当依赖项发生变化时,才会返回新的函数。 | ||
| + | |||
| + | **基本用法**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { useCallback } from ' | ||
| + | |||
| + | function Parent() { | ||
| const [count, setCount] = useState(0); | const [count, setCount] = useState(0); | ||
| - | const [user, setUser] = useState(null); | + | const [text, setText] = useState('' |
| - | + | ||
| - | // 处理计数相关的副作用 | + | // 有 useCallback:只在 count 变化时创建新函数 |
| - | | + | |
| - | | + | |
| }, [count]); | }, [count]); | ||
| - | | + | |
| - | | + | |
| - | | + | < |
| - | | + | <button onClick={() => setCount(c => c + 1)}> |
| + | <input value={text} onChange={e | ||
| + | <Child onClick={handleClick} /> | ||
| + | | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **配合 React.memo 使用**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { memo, useCallback, | ||
| + | |||
| + | // 子组件使用 React.memo 进行浅比较 | ||
| + | const Child = memo(({ onClick, label }) => { | ||
| + | console.log(`${label} 渲染`); | ||
| + | return <button onClick={onClick}>{label}</ | ||
| + | }); | ||
| + | |||
| + | function Parent() { | ||
| + | const [count1, setCount1] = useState(0); | ||
| + | const [count2, setCount2] = useState(0); | ||
| + | |||
| + | // 使用 useCallback 缓存函数 | ||
| + | const handleClick1 = useCallback(() => { | ||
| + | setCount1(c => c + 1); | ||
| }, []); | }, []); | ||
| - | | + | |
| - | | + | |
| - | useEffect(() => { | + | |
| - | | + | |
| - | return () => subscription.unsubscribe(); | + | |
| }, []); | }, []); | ||
| - | | + | |
| - | return <div>示例</ | + | return |
| + | | ||
| + | < | ||
| + | <Child onClick={handleClick1} label=" | ||
| + | <Child onClick={handleClick2} label=" | ||
| + | | ||
| + | ); | ||
| } | } | ||
| </ | </ | ||
| - | 相比之下,如果使用类组件,这些逻辑都会混杂在 componentDidMount 和 componentDidUpdate 中,难以追踪和维护。 | + | **何时使用 |
| - | ===== 6.6 useEffect 的执行顺序 ===== | + | * 函数作为 props 传递给子组件,且子组件使用了 React.memo |
| + | * 函数作为 | ||
| + | * 函数被其他 Hooks(如 useMemo)依赖 | ||
| + | * 函数是一个复杂计算或创建成本较高 | ||
| - | 在同一个组件中,多个 useEffect 会按照它们在代码中出现的顺序依次执行。 | + | **避免过度使用**: |
| <code javascript> | <code javascript> | ||
| - | function | + | // 不需要 useCallback 的情况:简单函数直接内联 |
| - | | + | function |
| - | console.log(' | + | |
| - | }, []); | + | |
| - | + | | |
| - | | + | |
| - | console.log(' | + | |
| - | }, []); | + | return <button onClick={increment}>{count}</button>; |
| - | + | ||
| - | useEffect(() | + | |
| - | console.log(' | + | |
| - | }, []); | + | |
| - | | + | |
| - | return <div>检查控制台输出顺序</div>; | + | |
| } | } | ||
| - | // 输出顺序: | ||
| </ | </ | ||
| - | 清理函数的执行顺序与 effect 相反:先声明的 effect 后清理。 | + | |
| + | ===== 5.17 useMemo - 缓存计算结果 ===== | ||
| + | |||
| + | useMemo 返回一个 memoized 值。只有当依赖项发生变化时,才会重新计算。 | ||
| + | |||
| + | **基本用法**: | ||
| <code javascript> | <code javascript> | ||
| - | function | + | import { useMemo } from ' |
| + | |||
| + | function | ||
| + | // 使用 useMemo 缓存昂贵的计算结果 | ||
| + | const filteredData = useMemo(() => { | ||
| + | console.log(' | ||
| + | return data.filter(item => item.name.includes(filter)); | ||
| + | }, [data, filter]); | ||
| + | |||
| + | return ( | ||
| + | < | ||
| + | {filteredData.map(item => <li key={item.id}> | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **对象/ | ||
| + | |||
| + | <code javascript> | ||
| + | function Chart({ data, options }) { | ||
| + | // 使用 useMemo 保持对象的引用稳定 | ||
| + | const chartOptions = useMemo(() => ({ | ||
| + | responsive: true, | ||
| + | scales: { | ||
| + | y: { beginAtZero: | ||
| + | } | ||
| + | }), [options.startFromZero]); | ||
| + | |||
| + | // chartOptions 的引用只在依赖变化时改变 | ||
| useEffect(() => { | useEffect(() => { | ||
| - | console.log(' | + | console.log(' |
| - | | + | |
| - | }, []); | + | }, [chartOptions]); |
| - | + | ||
| - | | + | return |
| - | console.log(' | + | |
| - | | + | |
| - | }, []); | + | |
| - | + | ||
| - | // 组件卸载时输出: | + | |
| - | // Effect 2 清理 | + | |
| - | // Effect 1 清理 | + | |
| - | + | ||
| - | return < | + | |
| } | } | ||
| </ | </ | ||
| - | ===== 6.7 常见的依赖问题 ===== | + | **复杂数据的处理**: |
| - | 依赖数组是 useEffect 中最容易出错的部分。遗漏依赖或错误地添加依赖都会导致 bug。 | + | <code javascript> |
| + | function DataTable({ rows, sortKey, sortOrder }) { | ||
| + | const sortedRows = useMemo(() => { | ||
| + | return [...rows].sort((a, | ||
| + | const aVal = a[sortKey]; | ||
| + | const bVal = b[sortKey]; | ||
| + | |||
| + | if (sortOrder === ' | ||
| + | return aVal > bVal ? 1 : -1; | ||
| + | } else { | ||
| + | return aVal < bVal ? 1 : -1; | ||
| + | } | ||
| + | }); | ||
| + | }, [rows, sortKey, sortOrder]); | ||
| - | **遗漏依赖的问题**: | + | return ( |
| + | < | ||
| + | < | ||
| + | {sortedRows.map(row => ( | ||
| + | <tr key={row.id}> | ||
| + | < | ||
| + | < | ||
| + | </ | ||
| + | ))} | ||
| + | </ | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **useMemo vs useCallback**: | ||
| <code javascript> | <code javascript> | ||
| - | // 错误示例:遗漏了 count 依赖 | + | // useCallback(fn, |
| - | function | + | |
| - | const [count, setCount] = useState(0); | + | // useCallback - 缓存函数 |
| - | + | const memoizedCallback = useCallback(() => { | |
| - | | + | doSomething(a, |
| - | | + | }, [a, b]); |
| - | }, []); // 警告:count 应该在依赖数组中 | + | |
| - | + | // useMemo - 缓存值 | |
| - | return <button onClick={() => setCount(c => c + 1)}>增加</ | + | const memoizedValue = useMemo(() => { |
| + | return computeExpensiveValue(a, | ||
| + | }, [a, b]); | ||
| + | </ | ||
| + | |||
| + | ===== 5.18 useContext - 跨组件状态共享 ===== | ||
| + | |||
| + | useContext 让我们无需为每层组件手动添加 props,就能在组件树间进行数据传递。 | ||
| + | |||
| + | **创建和使用 Context**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { createContext, | ||
| + | |||
| + | // 1. 创建 Context | ||
| + | const ThemeContext = createContext(' | ||
| + | |||
| + | // 2. 提供 Context | ||
| + | function | ||
| + | const [theme, setTheme] = useState(' | ||
| + | |||
| + | | ||
| + | < | ||
| + | <Toolbar /> | ||
| + | | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | // 3. 消费 Context | ||
| + | function Toolbar() { | ||
| + | return ( | ||
| + | < | ||
| + | < | ||
| + | </ | ||
| + | | ||
| + | } | ||
| + | |||
| + | function ThemeButton() { | ||
| + | | ||
| + | | ||
| + | |||
| + | return | ||
| + | | ||
| + | style={{ background: theme === ' | ||
| + | | ||
| + | | ||
| + | 当前主题: | ||
| + | | ||
| + | ); | ||
| } | } | ||
| </ | </ | ||
| - | 上述代码中,effect 只在挂载时执行一次,之后 count 变化时不会重新执行,导致输出始终是初始值 0。 | + | **多 Context 使用**: |
| - | **修复遗漏依赖**: | + | <code javascript> |
| + | const ThemeContext = createContext(' | ||
| + | const UserContext = createContext(null); | ||
| + | |||
| + | function App() { | ||
| + | const [theme, setTheme] = useState(' | ||
| + | const [user, setUser] = useState({ name: ' | ||
| + | |||
| + | return ( | ||
| + | < | ||
| + | < | ||
| + | <Layout /> | ||
| + | </ | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | function Layout() { | ||
| + | const { theme } = useContext(ThemeContext); | ||
| + | const { user } = useContext(UserContext); | ||
| + | |||
| + | return ( | ||
| + | <div className={theme}> | ||
| + | < | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **Context 分割优化**: | ||
| <code javascript> | <code javascript> | ||
| - | useEffect(() => { | + | // 不好的做法:整个 context 变化导致所有消费者重新渲染 |
| - | | + | const AppContext = createContext({ |
| - | }, [count]); | + | theme: ' |
| + | user: null, | ||
| + | setTheme: | ||
| + | | ||
| + | }); | ||
| + | |||
| + | // 更好的做法:将经常变化和很少变化的数据分开 | ||
| + | const ThemeContext = createContext({ theme: ' | ||
| + | const UserContext = createContext({ user: null, setUser: () => {} }); | ||
| + | |||
| + | // 最佳做法:使用自定义 Hook 封装 context | ||
| + | function useTheme() { | ||
| + | const context = useContext(ThemeContext); | ||
| + | if (!context) { | ||
| + | throw new Error(' | ||
| + | } | ||
| + | return context; | ||
| + | } | ||
| </ | </ | ||
| - | **函数依赖的处理**: | ||
| - | 当 effect 中使用了函数时,需要将函数也加入依赖数组。但如果函数在每次渲染时都重新定义,会导致 effect 频繁执行。 | + | ===== 5.19 useReducer - 复杂状态管理 ===== |
| + | |||
| + | useReducer 是 useState 的替代方案,适用于 state 逻辑较复杂或包含多个子值的情况。 | ||
| + | |||
| + | **基本用法**: | ||
| <code javascript> | <code javascript> | ||
| - | // 问题:fetchData 每次渲染都重新定义,导致 effect 频繁执行 | + | import { useReducer } from ' |
| - | function | + | |
| - | | + | // 定义 |
| - | + | function | |
| - | const fetchData = async () => { | + | |
| - | | + | case ' |
| - | | + | |
| - | | + | |
| - | }; | + | return { count: state.count - 1 }; |
| - | + | | |
| - | | + | return { count: 0 }; |
| - | fetchData(); | + | |
| - | | + | throw new Error(' |
| - | + | } | |
| - | return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>; | + | } |
| + | |||
| + | function Counter() { | ||
| + | | ||
| + | |||
| + | return | ||
| + | | ||
| + | < | ||
| + | <button onClick={() => dispatch({ type: ' | ||
| + | | ||
| + | <button onClick={() => dispatch({ type: ' | ||
| + | </ | ||
| + | ); | ||
| } | } | ||
| </ | </ | ||
| - | 解决方案是将函数定义在 useEffect 内部,或使用 useCallback: | + | **复杂状态管理**: |
| <code javascript> | <code javascript> | ||
| - | // 方案1:函数定义在 effect 内部 | + | const initialState = { |
| - | useEffect(() => { | + | |
| + | data: null, | ||
| + | error: null | ||
| + | }; | ||
| + | |||
| + | function dataReducer(state, action) { | ||
| + | switch | ||
| + | case ' | ||
| + | return { ...state, loading: true, error: null }; | ||
| + | case ' | ||
| + | return { ...state, loading: false, data: action.payload }; | ||
| + | case ' | ||
| + | return { ...state, loading: false, error: action.payload }; | ||
| + | default: | ||
| + | return state; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function DataComponent() { | ||
| + | const [state, dispatch] = useReducer(dataReducer, | ||
| const fetchData = async () => { | const fetchData = async () => { | ||
| - | const response = await fetch(apiUrl); | + | |
| - | const data = await response.json(); | + | try { |
| - | | + | |
| + | const data = await response.json(); | ||
| + | | ||
| + | } catch (error) { | ||
| + | dispatch({ type: ' | ||
| + | } | ||
| }; | }; | ||
| - | fetchData(); | ||
| - | }, [apiUrl]); | ||
| - | |||
| - | // 方案2:使用 useCallback | ||
| - | const fetchData = useCallback(async () => { | ||
| - | const response = await fetch(apiUrl); | ||
| - | const data = await response.json(); | ||
| - | setUsers(data); | ||
| - | }, [apiUrl]); | ||
| - | useEffect(() => { | + | return |
| - | | + | < |
| - | }, [fetchData]); | + | {state.loading && < |
| + | {state.error && < | ||
| + | {state.data && < | ||
| + | <button onClick={fetchData}> | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| </ | </ | ||
| - | ===== 6.8 useEffect 与 useLayoutEffect ===== | + | **useReducer vs useState**: |
| - | React 提供了两个类似的 Hook:useEffect 和 useLayoutEffect。它们的签名完全相同,但执行时机不同。 | + | | 场景 | 推荐方案 | |
| + | | 简单状态(单个值) | useState | | ||
| + | | 复杂状态(多个子值相互依赖) | useReducer | | ||
| + | | 需要优化性能,state 更新逻辑复杂 | useReducer | | ||
| + | | 需要复用状态逻辑 | useReducer + 自定义 Hook | | ||
| - | **执行时机差异**: | + | ===== 5.20 其他常用 Hooks ===== |
| - | - useEffect:在浏览器绘制完成后异步执行,不会阻塞视觉更新 | + | **useId |
| - | - useLayoutEffect:在浏览器绘制之前同步执行,会阻塞视觉更新 | + | |
| <code javascript> | <code javascript> | ||
| - | import { useLayoutEffect | + | import { useId } from ' |
| - | function | + | function |
| - | const divRef | + | const id = useId(); |
| - | const [width, setWidth] = useState(0); | + | |
| - | + | ||
| - | useLayoutEffect(() => { | + | |
| - | // 在浏览器绘制前测量 DOM | + | |
| - | const { width } = divRef.current.getBoundingClientRect(); | + | |
| - | setWidth(width); | + | |
| - | }, []); | + | |
| | | ||
| return ( | return ( | ||
| - | < | + | <div> |
| - | | + | <label htmlFor={id + ' |
| + | | ||
| + | |||
| + | <label htmlFor={id + ' | ||
| + | <input id={id + ' | ||
| </ | </ | ||
| ); | ); | ||
| 行 880: | 行 1255: | ||
| </ | </ | ||
| - | **何时使用 useLayoutEffect**: | + | **useTransition - 非紧急更新**: |
| - | 绝大多数情况下,应该使用 useEffect。只有当出现以下情况时才考虑 useLayoutEffect: | + | <code javascript> |
| + | import { useTransition, | ||
| - | - 需要在浏览器绘制前测量 DOM 元素(如获取元素尺寸、位置) | + | function TabContainer() { |
| - | - 需要根据测量结果同步修改 DOM,避免视觉闪烁 | + | const [isPending, startTransition] = useTransition(); |
| - | - 需要同步重新渲染以修复视觉不一致 | + | const [tab, setTab] = useState(' |
| - | **服务端渲染注意事项**: | + | const selectTab = (nextTab) => { |
| + | // 将状态更新标记为低优先级 | ||
| + | startTransition(() => { | ||
| + | setTab(nextTab); | ||
| + | }); | ||
| + | }; | ||
| + | |||
| + | return ( | ||
| + | < | ||
| + | {isPending && < | ||
| + | < | ||
| + | < | ||
| + | {tab === ' | ||
| + | {tab === ' | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| - | 在服务端渲染(SSR)环境中,useLayoutEffect 会产生警告,因为服务端无法测量 DOM。解决方法是使用 useEffect 代替,或使用动态导入在客户端渲染。 | + | **useDeferredValue - 延迟更新值**: |
| <code javascript> | <code javascript> | ||
| - | import { useEffect, useLayoutEffect | + | import { useDeferredValue, useState |
| - | const useIsomorphicLayoutEffect = typeof window !== 'undefined' | + | function SearchResults() { |
| + | | ||
| + | const deferredQuery = useDeferredValue(query); | ||
| - | function Component() { | + | return |
| - | | + | < |
| - | // 在服务端使用 | + | <input |
| - | | + | value={query} |
| - | | + | |
| - | | + | placeholder=" |
| + | /> | ||
| + | {/* 使用 | ||
| + | <SlowList query={deferredQuery} /> | ||
| + | | ||
| + | ); | ||
| } | } | ||
| </ | </ | ||
| - | ===== 6.9 自定义 Hook 封装 useEffect ===== | ||
| - | 将常用的 useEffect 逻辑封装成自定义 | + | ===== 5.21 自定义 |
| - | **useFetch | + | 自定义 Hooks 是提取组件逻辑到可复用函数的方式。自定义 Hook 是一个函数,其名称以 " |
| + | |||
| + | **useFetch | ||
| <code javascript> | <code javascript> | ||
| + | import { useState, useEffect } from ' | ||
| + | |||
| function useFetch(url) { | function useFetch(url) { | ||
| const [data, setData] = useState(null); | const [data, setData] = useState(null); | ||
| const [loading, setLoading] = useState(true); | const [loading, setLoading] = useState(true); | ||
| const [error, setError] = useState(null); | const [error, setError] = useState(null); | ||
| - | | + | |
| useEffect(() => { | useEffect(() => { | ||
| const controller = new AbortController(); | const controller = new AbortController(); | ||
| 行 945: | 行 1348: | ||
| return () => controller.abort(); | return () => controller.abort(); | ||
| }, [url]); | }, [url]); | ||
| - | | + | |
| return { data, loading, error }; | return { data, loading, error }; | ||
| } | } | ||
| 行 964: | 行 1367: | ||
| </ | </ | ||
| - | **useDebounce Hook**: | + | **useLocalStorage - 本地存储同步**: |
| <code javascript> | <code javascript> | ||
| + | import { useState, useEffect } from ' | ||
| + | |||
| + | function useLocalStorage(key, | ||
| + | // 获取初始值 | ||
| + | const [storedValue, | ||
| + | try { | ||
| + | const item = window.localStorage.getItem(key); | ||
| + | return item ? JSON.parse(item) : initialValue; | ||
| + | } catch (error) { | ||
| + | console.error(error); | ||
| + | return initialValue; | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | // 当值变化时更新 localStorage | ||
| + | useEffect(() => { | ||
| + | try { | ||
| + | window.localStorage.setItem(key, | ||
| + | } catch (error) { | ||
| + | console.error(error); | ||
| + | } | ||
| + | }, [key, storedValue]); | ||
| + | |||
| + | return [storedValue, | ||
| + | } | ||
| + | |||
| + | // 使用 | ||
| + | function App() { | ||
| + | const [name, setName] = useLocalStorage(' | ||
| + | | ||
| + | return ( | ||
| + | < | ||
| + | value={name} | ||
| + | onChange={e => setName(e.target.value)} | ||
| + | placeholder=" | ||
| + | /> | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **useDebounce - 防抖**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { useState, useEffect } from ' | ||
| + | |||
| function useDebounce(value, | function useDebounce(value, | ||
| const [debouncedValue, | const [debouncedValue, | ||
| - | | + | |
| useEffect(() => { | useEffect(() => { | ||
| const timer = setTimeout(() => { | const timer = setTimeout(() => { | ||
| setDebouncedValue(value); | setDebouncedValue(value); | ||
| }, delay); | }, delay); | ||
| - | | + | |
| return () => { | return () => { | ||
| clearTimeout(timer); | clearTimeout(timer); | ||
| }; | }; | ||
| }, [value, delay]); | }, [value, delay]); | ||
| - | | + | |
| return debouncedValue; | return debouncedValue; | ||
| } | } | ||
| 行 987: | 行 1435: | ||
| const [input, setInput] = useState('' | const [input, setInput] = useState('' | ||
| const debouncedInput = useDebounce(input, | const debouncedInput = useDebounce(input, | ||
| - | | + | |
| useEffect(() => { | useEffect(() => { | ||
| if (debouncedInput) { | if (debouncedInput) { | ||
| 行 993: | 行 1441: | ||
| } | } | ||
| }, [debouncedInput]); | }, [debouncedInput]); | ||
| - | | + | |
| return ( | return ( | ||
| <input | <input | ||
| 行 1004: | 行 1452: | ||
| </ | </ | ||
| - | ===== 6.10 常见陷阱与最佳实践 | + | **usePrevious - 获取上一次的值**: |
| + | |||
| + | <code javascript> | ||
| + | import { useRef, useEffect } from ' | ||
| + | |||
| + | function usePrevious(value) { | ||
| + | const ref = useRef(); | ||
| + | |||
| + | useEffect(() | ||
| + | ref.current | ||
| + | }); | ||
| + | |||
| + | return ref.current; | ||
| + | } | ||
| + | |||
| + | // 使用 | ||
| + | function Counter() { | ||
| + | const [count, setCount] | ||
| + | const prevCount | ||
| + | |||
| + | return ( | ||
| + | < | ||
| + | < | ||
| + | <button onClick={() => setCount(c => c + 1)}> | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **useOnClickOutside - 点击外部关闭**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { useEffect, useRef } from ' | ||
| + | |||
| + | function useOnClickOutside(ref, | ||
| + | useEffect(() => { | ||
| + | const listener = (event) => { | ||
| + | // 如果点击的是 ref 元素内部,不执行 handler | ||
| + | if (!ref.current || ref.current.contains(event.target)) { | ||
| + | return; | ||
| + | } | ||
| + | handler(event); | ||
| + | }; | ||
| + | |||
| + | document.addEventListener(' | ||
| + | document.addEventListener(' | ||
| + | |||
| + | return () => { | ||
| + | document.removeEventListener(' | ||
| + | document.removeEventListener(' | ||
| + | }; | ||
| + | }, [ref, handler]); | ||
| + | } | ||
| + | |||
| + | // 使用 | ||
| + | function Dropdown() { | ||
| + | const ref = useRef(); | ||
| + | const [isOpen, setIsOpen] = useState(false); | ||
| + | |||
| + | useOnClickOutside(ref, | ||
| + | |||
| + | return ( | ||
| + | <div ref={ref}> | ||
| + | <button onClick={() => setIsOpen(!isOpen)}> | ||
| + | {isOpen && <div className=" | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ===== 5.22 Hooks 最佳实践 ===== | ||
| + | |||
| + | **1. 只在最顶层调用 Hooks**: | ||
| + | |||
| + | <code javascript> | ||
| + | // 正确 | ||
| + | function Example() { | ||
| + | const [count, setCount] = useState(0); | ||
| + | useEffect(() => { }); | ||
| + | |||
| + | if (condition) { // 条件在 Hooks 之后 | ||
| + | return; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // 错误 - 条件中使用 Hook | ||
| + | function Example() { | ||
| + | if (condition) { | ||
| + | const [count, setCount] = useState(0); | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **2. 只在 React 函数中调用 Hooks**: | ||
| + | |||
| + | <code javascript> | ||
| + | // 正确 - 在函数组件中 | ||
| + | function MyComponent() { | ||
| + | const [state, setState] = useState(0); | ||
| + | } | ||
| + | |||
| + | // 正确 - 在自定义 Hook 中 | ||
| + | function useMyHook() { | ||
| + | const [state, setState] = useState(0); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **3. 使用 ESLint 插件**: | ||
| + | |||
| + | <code json> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **4. 分离不相关的副作用**: | ||
| + | |||
| + | <code javascript> | ||
| + | // 好的做法:分离关注点 | ||
| + | function Example() { | ||
| + | // 处理订阅 | ||
| + | useEffect(() => { | ||
| + | const subscription = subscribe(); | ||
| + | return () => subscription.unsubscribe(); | ||
| + | }, []); | ||
| + | |||
| + | // 处理 DOM 操作 | ||
| + | useEffect(() => { | ||
| + | document.title = ' | ||
| + | }, []); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **5. 避免过度优化**: | ||
| + | |||
| + | <code javascript> | ||
| + | // 不需要 useMemo 的简单计算 | ||
| + | const doubled = count * 2; | ||
| + | |||
| + | // 不需要 useCallback 的简单函数 | ||
| + | const handleClick = () => setCount(c => c + 1); | ||
| + | </ | ||
| + | |||
| + | ===== 5.23 常见陷阱与解决方案 | ||
| - | **避免无限循环**: | + | **陷阱 1:useEffect |
| <code javascript> | <code javascript> | ||
| 行 1023: | 行 1618: | ||
| </ | </ | ||
| - | **对象和数组依赖的处理**: | + | **陷阱 2:遗漏依赖** |
| - | 对象和数组是引用类型,每次渲染都会创建新的引用,即使内容相同。 | + | <code javascript> |
| + | // 错误:遗漏了 count 依赖 | ||
| + | useEffect(() => { | ||
| + | console.log(`当前计数: | ||
| + | }, []); // 警告:count 应该在依赖数组中 | ||
| + | |||
| + | // 正确:添加所有依赖 | ||
| + | useEffect(() => { | ||
| + | console.log(`当前计数: | ||
| + | }, [count]); | ||
| + | </ | ||
| + | |||
| + | **陷阱 3:对象和数组依赖** | ||
| <code javascript> | <code javascript> | ||
| 行 1040: | 行 1647: | ||
| // 方案2:使用 useMemo | // 方案2:使用 useMemo | ||
| const options = useMemo(() => ({ page, size }), [page, size]); | const options = useMemo(() => ({ page, size }), [page, size]); | ||
| - | |||
| - | useEffect(() => { | ||
| - | fetchData(options); | ||
| - | }, [options]); | ||
| </ | </ | ||
| - | **依赖检查工具**: | + | ===== 5.24 总结 ===== |
| - | + | ||
| - | 使用 ESLint 的 react-hooks/ | + | |
| - | + | ||
| - | <code json> | + | |
| - | { | + | |
| - | " | + | |
| - | " | + | |
| - | " | + | |
| - | " | + | |
| - | } | + | |
| - | } | + | |
| - | </ | + | |
| - | + | ||
| - | **使用 ref 跳过首次渲染**: | + | |
| - | + | ||
| - | 有时候我们只想在依赖更新时执行 effect,而不是在挂载时。 | + | |
| - | + | ||
| - | <code javascript> | + | |
| - | function useUpdateEffect(effect, | + | |
| - | const isFirst | + | |
| - | + | ||
| - | useEffect(() | + | |
| - | if (isFirst.current) { | + | |
| - | isFirst.current | + | |
| - | return; | + | |
| - | } | + | |
| - | return effect(); | + | |
| - | }, deps); | + | |
| - | } | + | |
| - | + | ||
| - | // 使用 | + | |
| - | function Component() { | + | |
| - | const [count, setCount] | + | |
| - | + | ||
| - | useUpdateEffect(() | + | |
| - | console.log(' | + | |
| - | }, [count]); | + | |
| - | + | ||
| - | return <button onClick={() => setCount(c | + | |
| - | } | + | |
| - | </ | + | |
| - | ===== 6.11 总结 ===== | + | 本章全面介绍了 React 组件和 Hooks: |
| - | useEffect 是 React 函数组件中处理副作用的核心工具。本章详细介绍了: | + | **组件基础**: |
| + | * 函数组件和类组件的定义与区别 | ||
| + | * Props 传递、默认值和类型检查 | ||
| + | * 组件组合、children 和 render props | ||
| + | * 高阶组件和组件组织方式 | ||
| - | - useEffect 的基本语法和执行时机 | + | **核心 Hooks**: |
| - | - 清理函数的编写和作用 | + | * useState:状态管理 |
| - | - 数据获取的最佳实践和竞态条件处理 | + | * useEffect:副作用处理 |
| - | - 多个 useEffect 的分离原则 | + | * useRef:DOM 引用和持久化值 |
| - | - 依赖数组的正确使用 | + | * useCallback:缓存回调函数 |
| - | - useEffect 与 useLayoutEffect 的区别 | + | * useMemo:缓存计算结果 |
| - | - 自定义 Hook 的封装 | + | * useContext:跨组件状态共享 |
| - | - 常见陷阱和最佳实践 | + | * useReducer:复杂状态管理 |
| - | 掌握 useEffect 需要理解 React 的渲染流程和数据流。建议在开发时: | + | **其他 Hooks**: |
| + | * useId:生成唯一 ID | ||
| + | * useTransition:非紧急更新 | ||
| + | * useDeferredValue:延迟更新值 | ||
| - | - 保持依赖数组的完整性,使用 ESLint 辅助检查 | + | **自定义 Hooks**: |
| - | - 将不相关的副作用分离到不同的 useEffect 中 | + | * 封装可复用逻辑 |
| - | - 合理处理清理逻辑,避免内存泄漏 | + | * 常见的自定义 |
| - | - 对于复杂逻辑,考虑封装成自定义 | + | |
| - | - 优先使用 useEffect,只在必要时使用 useLayoutEffect | + | |
| - | useEffect 的强大之处在于它将副作用逻辑按照功能组织在一起,而不是分散在不同的生命周期方法中,这使得代码更加清晰、易于维护。随着实践的深入,你会越来越熟练地运用这一强大的工具来构建高质量的 React 应用。 | + | **最佳实践**: |
| + | * Hooks 使用规则 | ||
| + | * 性能优化策略 | ||
| + | * 常见陷阱与解决方案 | ||
| + | 掌握这些知识后,你可以使用函数组件和 Hooks 构建高质量的 React 应用。记住 Hooks 的核心原则:只在最顶层调用、只在 React 函数中调用、保持依赖数组完整。 | ||