差别
这里会显示出您选择的修订版和当前版本之间的差别。
| 后一修订版 | 前一修订版 | ||
| react:组件基础 [2026/03/13 15:50] – 创建 张叶安 | react:组件基础 [2026/04/07 11:03] (当前版本) – [5.19 useReducer - 复杂状态管理] 张叶安 | ||
|---|---|---|---|
| 行 1: | 行 1: | ||
| - | ====== 第五章:组件基础 ====== | + | ====== 第五章:React 组件基础 ====== |
| ===== 5.1 函数组件 ===== | ===== 5.1 函数组件 ===== | ||
| 行 428: | 行 428: | ||
| 组件是 React 的核心概念,理解组件的各种用法是掌握 React 的关键。 | 组件是 React 的核心概念,理解组件的各种用法是掌握 React 的关键。 | ||
| + | |||
| + | ===== 5.12 React Hooks 简介 ===== | ||
| + | |||
| + | Hooks 是 React 16.8 引入的新特性,它让我们在函数组件中使用 state 和其他 React 特性,而无需编写类组件。 | ||
| + | |||
| + | **为什么需要 Hooks**: | ||
| + | |||
| + | * 类组件中的 this 指向难以理解 | ||
| + | * 生命周期方法中常常包含不相关的逻辑,而相关逻辑分散在不同生命周期中 | ||
| + | * 高阶组件和 render props 会导致组件嵌套地狱 | ||
| + | * 复用状态逻辑困难 | ||
| + | |||
| + | **Hooks 使用规则**: | ||
| + | |||
| + | * 只在最顶层调用 Hooks,不要在循环、条件或嵌套函数中调用 | ||
| + | * 只在 React 函数组件或自定义 Hooks 中调用 Hooks | ||
| + | |||
| + | ===== 5.13 useState - 状态管理 ===== | ||
| + | |||
| + | useState 是最常用的 Hook,用于在函数组件中添加状态。 | ||
| + | |||
| + | **基本用法**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { useState } from ' | ||
| + | |||
| + | function Counter() { | ||
| + | // 声明一个叫 " | ||
| + | const [count, setCount] = useState(0); | ||
| + | |||
| + | return ( | ||
| + | <div> | ||
| + | < | ||
| + | <button onClick={() => setCount(count + 1)}> | ||
| + | 点击我 | ||
| + | </ | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **解构赋值说明**: | ||
| + | |||
| + | `const [count, setCount] = useState(0)` 是数组解构: | ||
| + | - count: 当前状态值 | ||
| + | - setCount: 更新状态的函数 | ||
| + | |||
| + | **函数式更新**: | ||
| + | |||
| + | 当新状态依赖于旧状态时,使用函数式更新可以避免闭包问题。 | ||
| + | |||
| + | <code javascript> | ||
| + | function Counter() { | ||
| + | const [count, setCount] = useState(0); | ||
| + | |||
| + | // 正确:使用函数式更新 | ||
| + | const increment = () => { | ||
| + | setCount(prevCount => prevCount + 1); | ||
| + | }; | ||
| + | |||
| + | return ( | ||
| + | <div> | ||
| + | < | ||
| + | <button onClick={increment}> | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **对象状态**: | ||
| + | |||
| + | 当 state 是对象时,更新时需要展开旧值。 | ||
| + | |||
| + | <code javascript> | ||
| + | function UserForm() { | ||
| + | const [user, setUser] = useState({ | ||
| + | name: '', | ||
| + | age: 0, | ||
| + | email: '' | ||
| + | }); | ||
| + | |||
| + | const handleChange = (field, value) => { | ||
| + | setUser(prevUser => ({ | ||
| + | ...prevUser, | ||
| + | [field]: value // 更新特定字段 | ||
| + | })); | ||
| + | }; | ||
| + | |||
| + | return ( | ||
| + | <div> | ||
| + | < | ||
| + | value={user.name} | ||
| + | onChange={e => handleChange(' | ||
| + | placeholder=" | ||
| + | /> | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **延迟初始化**: | ||
| + | |||
| + | 当初始值计算开销较大时,可以传入函数进行延迟初始化。 | ||
| + | |||
| + | <code javascript> | ||
| + | function ExpensiveComponent() { | ||
| + | // 只会在首次渲染时执行 computeExpensiveValue | ||
| + | const [value, setValue] = useState(() => computeExpensiveValue()); | ||
| + | | ||
| + | // ... | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ===== 5.14 useEffect - 副作用管理 ===== | ||
| + | |||
| + | useEffect 是 React Hooks 中最重要、最基础的 Hook 之一,用于在函数组件中执行副作用操作。 | ||
| + | |||
| + | **基本语法**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { useEffect } from ' | ||
| + | |||
| + | function Example() { | ||
| + | useEffect(() => { | ||
| + | // 副作用逻辑 | ||
| + | console.log(' | ||
| + | | ||
| + | // 可选的清理函数 | ||
| + | return () => { | ||
| + | console.log(' | ||
| + | }; | ||
| + | }, [/* 依赖数组 */]); | ||
| + | | ||
| + | return < | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **执行时机**: | ||
| + | |||
| + | <code javascript> | ||
| + | // 1. 每次渲染后都执行 | ||
| + | useEffect(() => { | ||
| + | console.log(' | ||
| + | }); | ||
| + | |||
| + | // 2. 只在挂载时执行(空依赖数组) | ||
| + | useEffect(() => { | ||
| + | console.log(' | ||
| + | fetchData(); | ||
| + | }, []); | ||
| + | |||
| + | // 3. 依赖变化时执行 | ||
| + | useEffect(() => { | ||
| + | console.log(`count 变化为: ${count}`); | ||
| + | document.title = `点击了 ${count} 次`; | ||
| + | }, [count]); | ||
| + | </ | ||
| + | |||
| + | **清理函数(Cleanup)**: | ||
| + | |||
| + | <code javascript> | ||
| + | function ChatRoom({ roomId }) { | ||
| + | useEffect(() => { | ||
| + | // 建立连接 | ||
| + | const connection = createConnection(roomId); | ||
| + | connection.connect(); | ||
| + | console.log(`连接到房间: | ||
| + | | ||
| + | // 返回清理函数 | ||
| + | return () => { | ||
| + | connection.disconnect(); | ||
| + | console.log(`断开房间: | ||
| + | }; | ||
| + | }, [roomId]); | ||
| + | | ||
| + | return < | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **数据获取模式**: | ||
| + | |||
| + | <code javascript> | ||
| + | function UserProfile({ userId }) { | ||
| + | const [user, setUser] = useState(null); | ||
| + | const [loading, setLoading] = useState(true); | ||
| + | const [error, setError] = useState(null); | ||
| + | | ||
| + | useEffect(() => { | ||
| + | let isCancelled = false; | ||
| + | const controller = new AbortController(); | ||
| + | | ||
| + | async function fetchUser() { | ||
| + | try { | ||
| + | setLoading(true); | ||
| + | const response = await fetch(`/ | ||
| + | signal: controller.signal | ||
| + | }); | ||
| + | const data = await response.json(); | ||
| + | | ||
| + | if (!isCancelled) { | ||
| + | setUser(data); | ||
| + | setError(null); | ||
| + | } | ||
| + | } catch (err) { | ||
| + | if (!isCancelled && err.name !== ' | ||
| + | setError(err.message); | ||
| + | } | ||
| + | } finally { | ||
| + | if (!isCancelled) { | ||
| + | setLoading(false); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | fetchUser(); | ||
| + | | ||
| + | return () => { | ||
| + | isCancelled = true; | ||
| + | controller.abort(); | ||
| + | }; | ||
| + | }, [userId]); | ||
| + | | ||
| + | if (loading) return < | ||
| + | if (error) return < | ||
| + | | ||
| + | return ( | ||
| + | <div> | ||
| + | < | ||
| + | < | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **useEffect 与 useLayoutEffect**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { useLayoutEffect } from ' | ||
| + | |||
| + | 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(() => { | ||
| + | // 在渲染后保存当前值,供下一次渲染使用 | ||
| + | prevCountRef.current = count; | ||
| + | }); | ||
| + | |||
| + | const prevCount = prevCountRef.current; | ||
| + | |||
| + | return ( | ||
| + | <div> | ||
| + | < | ||
| + | <button onClick={() => setCount(c => c + 1)}> | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **保存定时器 ID**: | ||
| + | |||
| + | <code javascript> | ||
| + | function Timer() { | ||
| + | const [seconds, setSeconds] = useState(0); | ||
| + | const [isRunning, setIsRunning] = useState(false); | ||
| + | const intervalRef = useRef(null); | ||
| + | |||
| + | const start = () => { | ||
| + | if (!isRunning) { | ||
| + | setIsRunning(true); | ||
| + | intervalRef.current = setInterval(() => { | ||
| + | setSeconds(s => s + 1); | ||
| + | }, 1000); | ||
| + | } | ||
| + | }; | ||
| + | |||
| + | const stop = () => { | ||
| + | if (isRunning) { | ||
| + | setIsRunning(false); | ||
| + | clearInterval(intervalRef.current); | ||
| + | } | ||
| + | }; | ||
| + | |||
| + | const reset = () => { | ||
| + | stop(); | ||
| + | setSeconds(0); | ||
| + | }; | ||
| + | |||
| + | useEffect(() => { | ||
| + | // 组件卸载时清理定时器 | ||
| + | return () => { | ||
| + | if (intervalRef.current) { | ||
| + | clearInterval(intervalRef.current); | ||
| + | } | ||
| + | }; | ||
| + | }, []); | ||
| + | |||
| + | return ( | ||
| + | <div> | ||
| + | < | ||
| + | <button onClick={start}> | ||
| + | <button onClick={stop}> | ||
| + | <button onClick={reset}> | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **转发 ref(forwardRef)**: | ||
| + | |||
| + | <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 ( | ||
| + | <div> | ||
| + | < | ||
| + | <button onClick={focusInput}> | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **useImperativeHandle 自定义暴露的实例值**: | ||
| + | |||
| + | <code javascript> | ||
| + | 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 Parent() { | ||
| + | const fancyInputRef = useRef(null); | ||
| + | |||
| + | const handleClick = () => { | ||
| + | fancyInputRef.current.focus(); | ||
| + | console.log(fancyInputRef.current.getValue()); | ||
| + | fancyInputRef.current.clear(); | ||
| + | }; | ||
| + | |||
| + | return ( | ||
| + | <div> | ||
| + | < | ||
| + | <button onClick={handleClick}> | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ===== 5.16 useCallback - 缓存回调函数 ===== | ||
| + | |||
| + | useCallback 返回一个 memoized 回调函数。只有当依赖项发生变化时,才会返回新的函数。 | ||
| + | |||
| + | **基本用法**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { useCallback } from ' | ||
| + | |||
| + | function Parent() { | ||
| + | const [count, setCount] = useState(0); | ||
| + | const [text, setText] = useState('' | ||
| + | |||
| + | // 有 useCallback:只在 count 变化时创建新函数 | ||
| + | const handleClick = useCallback(() => { | ||
| + | console.log(' | ||
| + | }, [count]); | ||
| + | |||
| + | return ( | ||
| + | <div> | ||
| + | <button onClick={() => setCount(c => c + 1)}> | ||
| + | <input value={text} onChange={e => setText(e.target.value)} /> | ||
| + | <Child onClick={handleClick} /> | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **配合 React.memo 使用**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { memo, useCallback, | ||
| + | |||
| + | // 子组件使用 React.memo 进行浅比较 | ||
| + | const Child = memo(({ onClick, label }) => { | ||
| + | console.log(`${label} 渲染`); | ||
| + | return <button onClick={onClick}> | ||
| + | }); | ||
| + | |||
| + | function Parent() { | ||
| + | const [count1, setCount1] = useState(0); | ||
| + | const [count2, setCount2] = useState(0); | ||
| + | |||
| + | // 使用 useCallback 缓存函数 | ||
| + | const handleClick1 = useCallback(() => { | ||
| + | setCount1(c => c + 1); | ||
| + | }, []); | ||
| + | |||
| + | const handleClick2 = useCallback(() => { | ||
| + | setCount2(c => c + 1); | ||
| + | }, []); | ||
| + | |||
| + | return ( | ||
| + | <div> | ||
| + | < | ||
| + | <Child onClick={handleClick1} label=" | ||
| + | <Child onClick={handleClick2} label=" | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **何时使用 useCallback**: | ||
| + | |||
| + | * 函数作为 props 传递给子组件,且子组件使用了 React.memo | ||
| + | * 函数作为 useEffect 的依赖项 | ||
| + | * 函数被其他 Hooks(如 useMemo)依赖 | ||
| + | * 函数是一个复杂计算或创建成本较高 | ||
| + | |||
| + | **避免过度使用**: | ||
| + | |||
| + | <code javascript> | ||
| + | // 不需要 useCallback 的情况:简单函数直接内联 | ||
| + | function SimpleComponent() { | ||
| + | const [count, setCount] = useState(0); | ||
| + | |||
| + | // 简单函数,不需要 useCallback | ||
| + | const increment = () => setCount(c => c + 1); | ||
| + | |||
| + | return <button onClick={increment}> | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | |||
| + | ===== 5.17 useMemo - 缓存计算结果 ===== | ||
| + | |||
| + | useMemo 返回一个 memoized 值。只有当依赖项发生变化时,才会重新计算。 | ||
| + | |||
| + | **基本用法**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { useMemo } from ' | ||
| + | |||
| + | function ExpensiveComponent({ data, filter }) { | ||
| + | // 使用 useMemo 缓存昂贵的计算结果 | ||
| + | const filteredData = useMemo(() => { | ||
| + | console.log(' | ||
| + | return data.filter(item => item.name.includes(filter)); | ||
| + | }, [data, filter]); | ||
| + | |||
| + | return ( | ||
| + | <ul> | ||
| + | {filteredData.map(item => <li key={item.id}> | ||
| + | </ul> | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **对象/ | ||
| + | |||
| + | <code javascript> | ||
| + | function Chart({ data, options }) { | ||
| + | // 使用 useMemo 保持对象的引用稳定 | ||
| + | const chartOptions = useMemo(() => ({ | ||
| + | responsive: true, | ||
| + | scales: { | ||
| + | y: { beginAtZero: | ||
| + | } | ||
| + | }), [options.startFromZero]); | ||
| + | |||
| + | // chartOptions 的引用只在依赖变化时改变 | ||
| + | useEffect(() => { | ||
| + | console.log(' | ||
| + | initChart(chartOptions); | ||
| + | }, [chartOptions]); | ||
| + | |||
| + | return <canvas id=" | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **复杂数据的处理**: | ||
| + | |||
| + | <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}> | ||
| + | < | ||
| + | < | ||
| + | </tr> | ||
| + | ))} | ||
| + | </ | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **useMemo vs useCallback**: | ||
| + | |||
| + | <code javascript> | ||
| + | // useCallback(fn, | ||
| + | |||
| + | // useCallback - 缓存函数 | ||
| + | const memoizedCallback = useCallback(() => { | ||
| + | doSomething(a, | ||
| + | }, [a, b]); | ||
| + | |||
| + | // useMemo - 缓存值 | ||
| + | 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 App() { | ||
| + | const [theme, setTheme] = useState(' | ||
| + | |||
| + | return ( | ||
| + | < | ||
| + | <Toolbar /> | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | // 3. 消费 Context | ||
| + | function Toolbar() { | ||
| + | return ( | ||
| + | <div> | ||
| + | < | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | function ThemeButton() { | ||
| + | // 使用 useContext 获取上下文值 | ||
| + | const { theme, setTheme } = useContext(ThemeContext); | ||
| + | |||
| + | return ( | ||
| + | <button | ||
| + | style={{ background: theme === ' | ||
| + | onClick={() => setTheme(theme === ' | ||
| + | > | ||
| + | 当前主题: | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **多 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> | ||
| + | // 不好的做法:整个 context 变化导致所有消费者重新渲染 | ||
| + | const AppContext = createContext({ | ||
| + | theme: ' | ||
| + | user: null, | ||
| + | setTheme: () => {}, | ||
| + | setUser: () => {} | ||
| + | }); | ||
| + | |||
| + | // 更好的做法:将经常变化和很少变化的数据分开 | ||
| + | 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; | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | |||
| + | ===== 5.19 useReducer - 复杂状态管理 ===== | ||
| + | |||
| + | useReducer 是 useState 的替代方案,适用于 state 逻辑较复杂或包含多个子值的情况。 | ||
| + | |||
| + | **基本用法**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { useReducer } from ' | ||
| + | |||
| + | // 定义 reducer 函数 | ||
| + | function reducer(state, | ||
| + | switch (action.type) { | ||
| + | case ' | ||
| + | return { count: state.count + 1 }; | ||
| + | case ' | ||
| + | return { count: state.count - 1 }; | ||
| + | case ' | ||
| + | return { count: 0 }; | ||
| + | default: | ||
| + | throw new Error(' | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function Counter() { | ||
| + | const [state, dispatch] = useReducer(reducer, | ||
| + | |||
| + | return ( | ||
| + | <div> | ||
| + | < | ||
| + | <button onClick={() => dispatch({ type: ' | ||
| + | <button onClick={() => dispatch({ type: ' | ||
| + | <button onClick={() => dispatch({ type: ' | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **复杂状态管理**: | ||
| + | |||
| + | <code javascript> | ||
| + | const initialState = { | ||
| + | loading: false, | ||
| + | data: null, | ||
| + | error: null | ||
| + | }; | ||
| + | |||
| + | function dataReducer(state, | ||
| + | switch (action.type) { | ||
| + | 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 () => { | ||
| + | dispatch({ type: ' | ||
| + | try { | ||
| + | const response = await fetch('/ | ||
| + | const data = await response.json(); | ||
| + | dispatch({ type: ' | ||
| + | } catch (error) { | ||
| + | dispatch({ type: ' | ||
| + | } | ||
| + | }; | ||
| + | |||
| + | return ( | ||
| + | <div> | ||
| + | {state.loading && < | ||
| + | {state.error && < | ||
| + | {state.data && < | ||
| + | <button onClick={fetchData}> | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **useReducer vs useState**: | ||
| + | |||
| + | | 场景 | 推荐方案 | | ||
| + | | 简单状态(单个值) | useState | | ||
| + | | 复杂状态(多个子值相互依赖) | useReducer | | ||
| + | | 需要优化性能,state 更新逻辑复杂 | useReducer | | ||
| + | | 需要复用状态逻辑 | useReducer + 自定义 Hook | | ||
| + | |||
| + | ===== 5.20 其他常用 Hooks ===== | ||
| + | |||
| + | **useId - 生成唯一 ID**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { useId } from ' | ||
| + | |||
| + | function Form() { | ||
| + | const id = useId(); | ||
| + | | ||
| + | return ( | ||
| + | <div> | ||
| + | <label htmlFor={id + ' | ||
| + | <input id={id + ' | ||
| + | | ||
| + | <label htmlFor={id + ' | ||
| + | <input id={id + ' | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **useTransition - 非紧急更新**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { useTransition, | ||
| + | |||
| + | function TabContainer() { | ||
| + | const [isPending, startTransition] = useTransition(); | ||
| + | const [tab, setTab] = useState(' | ||
| + | |||
| + | const selectTab = (nextTab) => { | ||
| + | // 将状态更新标记为低优先级 | ||
| + | startTransition(() => { | ||
| + | setTab(nextTab); | ||
| + | }); | ||
| + | }; | ||
| + | |||
| + | return ( | ||
| + | <div> | ||
| + | {isPending && < | ||
| + | < | ||
| + | < | ||
| + | {tab === ' | ||
| + | {tab === ' | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **useDeferredValue - 延迟更新值**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { useDeferredValue, | ||
| + | |||
| + | function SearchResults() { | ||
| + | const [query, setQuery] = useState('' | ||
| + | const deferredQuery = useDeferredValue(query); | ||
| + | |||
| + | return ( | ||
| + | <div> | ||
| + | < | ||
| + | value={query} | ||
| + | onChange={e => setQuery(e.target.value)} | ||
| + | placeholder=" | ||
| + | /> | ||
| + | {/* 使用 deferredQuery 显示结果,保持输入流畅 */} | ||
| + | < | ||
| + | </ | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | |||
| + | ===== 5.21 自定义 Hooks ===== | ||
| + | |||
| + | 自定义 Hooks 是提取组件逻辑到可复用函数的方式。自定义 Hook 是一个函数,其名称以 " | ||
| + | |||
| + | **useFetch - 数据获取**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { useState, useEffect } from ' | ||
| + | |||
| + | function useFetch(url) { | ||
| + | const [data, setData] = useState(null); | ||
| + | const [loading, setLoading] = useState(true); | ||
| + | const [error, setError] = useState(null); | ||
| + | |||
| + | useEffect(() => { | ||
| + | const controller = new AbortController(); | ||
| + | | ||
| + | async function fetchData() { | ||
| + | try { | ||
| + | setLoading(true); | ||
| + | const response = await fetch(url, { signal: controller.signal }); | ||
| + | if (!response.ok) { | ||
| + | throw new Error(`HTTP 错误: ${response.status}`); | ||
| + | } | ||
| + | const result = await response.json(); | ||
| + | setData(result); | ||
| + | setError(null); | ||
| + | } catch (err) { | ||
| + | if (err.name !== ' | ||
| + | setError(err.message); | ||
| + | setData(null); | ||
| + | } | ||
| + | } finally { | ||
| + | setLoading(false); | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | fetchData(); | ||
| + | | ||
| + | return () => controller.abort(); | ||
| + | }, [url]); | ||
| + | |||
| + | return { data, loading, error }; | ||
| + | } | ||
| + | |||
| + | // 使用 | ||
| + | function UserList() { | ||
| + | const { data: users, loading, error } = useFetch('/ | ||
| + | | ||
| + | if (loading) return < | ||
| + | if (error) return < | ||
| + | | ||
| + | return ( | ||
| + | <ul> | ||
| + | {users.map(user => <li key={user.id}> | ||
| + | </ul> | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **useLocalStorage - 本地存储同步**: | ||
| + | |||
| + | <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, | ||
| + | const [debouncedValue, | ||
| + | |||
| + | useEffect(() => { | ||
| + | const timer = setTimeout(() => { | ||
| + | setDebouncedValue(value); | ||
| + | }, delay); | ||
| + | |||
| + | return () => { | ||
| + | clearTimeout(timer); | ||
| + | }; | ||
| + | }, [value, delay]); | ||
| + | |||
| + | return debouncedValue; | ||
| + | } | ||
| + | |||
| + | // 使用 | ||
| + | function SearchInput() { | ||
| + | const [input, setInput] = useState('' | ||
| + | const debouncedInput = useDebounce(input, | ||
| + | |||
| + | useEffect(() => { | ||
| + | if (debouncedInput) { | ||
| + | performSearch(debouncedInput); | ||
| + | } | ||
| + | }, [debouncedInput]); | ||
| + | |||
| + | return ( | ||
| + | <input | ||
| + | value={input} | ||
| + | onChange={(e) => setInput(e.target.value)} | ||
| + | placeholder=" | ||
| + | /> | ||
| + | ); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | **usePrevious - 获取上一次的值**: | ||
| + | |||
| + | <code javascript> | ||
| + | import { useRef, useEffect } from ' | ||
| + | |||
| + | function usePrevious(value) { | ||
| + | const ref = useRef(); | ||
| + | | ||
| + | useEffect(() => { | ||
| + | ref.current = value; | ||
| + | }); | ||
| + | | ||
| + | return ref.current; | ||
| + | } | ||
| + | |||
| + | // 使用 | ||
| + | function Counter() { | ||
| + | const [count, setCount] = useState(0); | ||
| + | const prevCount = usePrevious(count); | ||
| + | | ||
| + | return ( | ||
| + | <div> | ||
| + | < | ||
| + | <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> | ||
| + | // 错误:setState 导致重新渲染,形成无限循环 | ||
| + | useEffect(() => { | ||
| + | setCount(count + 1); | ||
| + | }, [count]); | ||
| + | |||
| + | // 正确:使用函数式更新 | ||
| + | useEffect(() => { | ||
| + | const timer = setTimeout(() => { | ||
| + | setCount(c => c + 1); | ||
| + | }, 1000); | ||
| + | return () => clearTimeout(timer); | ||
| + | }, []); | ||
| + | </ | ||
| + | |||
| + | **陷阱 2:遗漏依赖** | ||
| + | |||
| + | <code javascript> | ||
| + | // 错误:遗漏了 count 依赖 | ||
| + | useEffect(() => { | ||
| + | console.log(`当前计数: | ||
| + | }, []); // 警告:count 应该在依赖数组中 | ||
| + | |||
| + | // 正确:添加所有依赖 | ||
| + | useEffect(() => { | ||
| + | console.log(`当前计数: | ||
| + | }, [count]); | ||
| + | </ | ||
| + | |||
| + | **陷阱 3:对象和数组依赖** | ||
| + | |||
| + | <code javascript> | ||
| + | // 问题:options 每次都是新对象,导致 effect 每次都执行 | ||
| + | useEffect(() => { | ||
| + | fetchData(options); | ||
| + | }, [options]); | ||
| + | |||
| + | // 方案1:展开为原始值 | ||
| + | useEffect(() => { | ||
| + | fetchData({ page, size }); | ||
| + | }, [page, size]); | ||
| + | |||
| + | // 方案2:使用 useMemo | ||
| + | const options = useMemo(() => ({ page, size }), [page, size]); | ||
| + | </ | ||
| + | |||
| + | ===== 5.24 总结 ===== | ||
| + | |||
| + | 本章全面介绍了 React 组件和 Hooks: | ||
| + | |||
| + | **组件基础**: | ||
| + | * 函数组件和类组件的定义与区别 | ||
| + | * Props 传递、默认值和类型检查 | ||
| + | * 组件组合、children 和 render props | ||
| + | * 高阶组件和组件组织方式 | ||
| + | |||
| + | **核心 Hooks**: | ||
| + | * useState:状态管理 | ||
| + | * useEffect:副作用处理 | ||
| + | * useRef:DOM 引用和持久化值 | ||
| + | * useCallback:缓存回调函数 | ||
| + | * useMemo:缓存计算结果 | ||
| + | * useContext:跨组件状态共享 | ||
| + | * useReducer:复杂状态管理 | ||
| + | |||
| + | **其他 Hooks**: | ||
| + | * useId:生成唯一 ID | ||
| + | * useTransition:非紧急更新 | ||
| + | * useDeferredValue:延迟更新值 | ||
| + | |||
| + | **自定义 Hooks**: | ||
| + | * 封装可复用逻辑 | ||
| + | * 常见的自定义 Hooks 模式 | ||
| + | |||
| + | **最佳实践**: | ||
| + | * Hooks 使用规则 | ||
| + | * 性能优化策略 | ||
| + | * 常见陷阱与解决方案 | ||
| + | |||
| + | 掌握这些知识后,你可以使用函数组件和 Hooks 构建高质量的 React 应用。记住 Hooks 的核心原则:只在最顶层调用、只在 React 函数中调用、保持依赖数组完整。 | ||