这是本文档旧的修订版!
第五章:React 组件基础
5.1 函数组件
函数组件是定义 React 组件最简单的方式。它是一个接收 props 对象并返回 React 元素的 JavaScript 函数。
function Welcome(props) { return <h1>Hello, {props.name}</h1>; }
特点:
- 简洁:纯函数,没有生命周期方法和 this
- 无状态(Hooks 之前):只能接收 props,不能管理自己的 state
- 性能:没有实例化开销,渲染更快
使用箭头函数:
const Welcome = (props) => { return <h1>Hello, {props.name}</h1>; }; // 简写形式(隐式返回) const Welcome = (props) => <h1>Hello, {props.name}</h1>; // 解构 props const Welcome = ({ name }) => <h1>Hello, {name}</h1>;
5.2 类组件
类组件使用 ES6 的 class 语法定义,继承自 React.Component。
import React from 'react'; class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } }
特点:
- 可以使用 state 管理组件内部状态
- 可以使用生命周期方法
- 有 this 关键字
- 需要 render() 方法返回 JSX
5.3 组件组合
组件可以相互组合,形成组件树。
function Welcome(props) { return <h1>Hello, {props.name}</h1>; } function App() { return ( <div> <Welcome name="张三" /> <Welcome name="李四" /> <Welcome name="王五" /> </div> ); }
提取组件:
当 UI 的一部分被多次使用,或者自身足够复杂时,可以将其提取为独立的组件。
// 原始代码 function Comment(props) { return ( <div className="Comment"> <div className="UserInfo"> <img className="Avatar" src={props.author.avatarUrl} alt={props.author.name} /> <div className="UserInfo-name"> {props.author.name} </div> </div> <div className="Comment-text"> {props.text} </div> <div className="Comment-date"> {formatDate(props.date)} </div> </div> ); } // 提取 Avatar 组件 function Avatar(props) { return ( <img className="Avatar" src={props.user.avatarUrl} alt={props.user.name} /> ); } // 提取 UserInfo 组件 function UserInfo(props) { return ( <div className="UserInfo"> <Avatar user={props.user} /> <div className="UserInfo-name"> {props.user.name} </div> </div> ); } // 重构后的 Comment 组件 function Comment(props) { return ( <div className="Comment"> <UserInfo user={props.author} /> <div className="Comment-text"> {props.text} </div> <div className="Comment-date"> {formatDate(props.date)} </div> </div> ); }
5.4 Props 详解
Props(properties 的缩写)是组件之间传递数据的方式。
传递 props:
function App() { return <User name="张三" age={25} isAdmin={true} />; }
接收 props:
function User(props) { return ( <div> <p>姓名:{props.name}</p> <p>年龄:{props.age}</p> <p>{props.isAdmin ? '管理员' : '普通用户'}</p> </div> ); } // 使用解构 function User({ name, age, isAdmin }) { return ( <div> <p>姓名:{name}</p> <p>年龄:{age}</p> <p>{isAdmin ? '管理员' : '普通用户'}</p> </div> ); }
Props 的只读性:
组件不能修改自己的 props。props 对于组件来说是只读的。
// 错误!props 是只读的 function Welcome(props) { props.name = 'Hello'; // 错误! return <h1>{props.name}</h1>; }
5.5 默认 Props
为 props 设置默认值:
函数组件:
function Button({ text = '点击', type = 'button' }) { return <button type={type}>{text}</button>; } // 或者 function Button(props) { const { text = '点击', type = 'button' } = props; return <button type={type}>{text}</button>; }
类组件:
class Button extends React.Component { static defaultProps = { text: '点击', type: 'button' }; render() { return <button type={this.props.type}>{this.props.text}</button>; } } // 或者在组件外部 Button.defaultProps = { text: '点击', type: 'button' };
5.6 PropTypes 类型检查
PropTypes 用于运行时类型检查。
安装:
npm install prop-types
使用:
import PropTypes from 'prop-types'; function User({ name, age, email }) { return ( <div> <p>{name}</p> <p>{age}</p> <p>{email}</p> </div> ); } User.propTypes = { name: PropTypes.string.isRequired, age: PropTypes.number, email: PropTypes.string, isAdmin: PropTypes.bool, hobbies: PropTypes.array, address: PropTypes.object, onClick: PropTypes.func, element: PropTypes.element, nodes: PropTypes.node, user: PropTypes.instanceOf(User), shape: PropTypes.shape({ color: PropTypes.string, fontSize: PropTypes.number }), oneOf: PropTypes.oneOf(['News', 'Photos']), oneOfType: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]), arrayOf: PropTypes.arrayOf(PropTypes.number), objectOf: PropTypes.objectOf(PropTypes.number), any: PropTypes.any }; User.defaultProps = { age: 18 };
TypeScript 替代方案:
如果使用 TypeScript,可以使用接口定义 props 类型,获得编译时类型检查。
5.7 children prop
children 是一个特殊的 prop,用于在组件标签之间传递内容。
function Card({ title, children }) { return ( <div className="card"> <div className="card-header">{title}</div> <div className="card-body">{children}</div> </div> ); } // 使用 function App() { return ( <Card title="欢迎使用"> <p>这是卡片的内容</p> <button>了解更多</button> </Card> ); }
children 的类型:
- React 元素
- 字符串或数字
- 数组(Fragment)
- 函数(render props)
- null、undefined、boolean(不渲染)
5.8 render props 模式
render props 是一种在 React 组件之间使用一个值为函数的 prop 共享代码的技术。
class MouseTracker extends React.Component { state = { x: 0, y: 0 }; handleMouseMove = (event) => { this.setState({ x: event.clientX, y: event.clientY }); }; render() { return ( <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}> {this.props.render(this.state)} </div> ); } } // 使用 function App() { return ( <MouseTracker render={({ x, y }) => ( <p>鼠标位置:({x}, {y})</p> )} /> ); }
5.9 高阶组件(HOC)
高阶组件是一个函数,接收一个组件并返回一个新的组件。
function withLogger(WrappedComponent) { return class extends React.Component { componentDidMount() { console.log('Component mounted:', WrappedComponent.name); } render() { return <WrappedComponent {...this.props} />; } }; } // 使用 const EnhancedComponent = withLogger(MyComponent);
常见的 HOC 用途:
- 权限控制
- 日志记录
- 数据获取
- 样式增强
5.10 组件的组织方式
按功能组织:
src/ ├── components/ # 可复用的 UI 组件 │ ├── Button/ │ │ ├── Button.js │ │ ├── Button.css │ │ └── Button.test.js │ └── Card/ ├── features/ # 功能模块 │ ├── User/ │ │ ├── UserList.js │ │ ├── UserForm.js │ │ └── userSlice.js │ └── Product/ ├── hooks/ # 自定义 Hooks ├── utils/ # 工具函数 ├── api/ # API 调用 └── App.js
按类型组织:
src/ ├── components/ # 所有组件 ├── containers/ # 容器组件 ├── actions/ # Redux actions ├── reducers/ # Redux reducers ├── utils/ # 工具函数 └── App.js
5.11 总结
本章介绍了组件的基础知识:
- 函数组件和类组件
- 组件组合
- Props 传递和使用
- 默认 Props 和类型检查
- children 和 render props
- 高阶组件
组件是 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,用于在函数组件中添加状态。
基本用法:
import { useState } from 'react'; function Counter() { // 声明一个叫 "count" 的 state 变量,初始值为 0 const [count, setCount] = useState(0); return ( <div> <p>你点击了 {count} 次</p> <button onClick={() => setCount(count + 1)}> 点击我 </button> </div> ); }
解构赋值说明:
`const [count, setCount] = useState(0)` 是数组解构: - count: 当前状态值 - setCount: 更新状态的函数
函数式更新:
当新状态依赖于旧状态时,使用函数式更新可以避免闭包问题。
function Counter() { const [count, setCount] = useState(0); // 正确:使用函数式更新 const increment = () => { setCount(prevCount => prevCount + 1); }; return ( <div> <p>{count}</p> <button onClick={increment}>增加</button> </div> ); }
对象状态:
当 state 是对象时,更新时需要展开旧值。
function UserForm() { const [user, setUser] = useState({ name: '', age: 0, email: '' }); const handleChange = (field, value) => { setUser(prevUser => ({ ...prevUser, // 展开旧值 [field]: value // 更新特定字段 })); }; return ( <div> <input value={user.name} onChange={e => handleChange('name', e.target.value)} placeholder="姓名" /> </div> ); }
延迟初始化:
当初始值计算开销较大时,可以传入函数进行延迟初始化。
function ExpensiveComponent() { // 只会在首次渲染时执行 computeExpensiveValue const [value, setValue] = useState(() => computeExpensiveValue()); // ... }
5.14 useEffect - 副作用管理
useEffect 是 React Hooks 中最重要、最基础的 Hook 之一,用于在函数组件中执行副作用操作。
基本语法:
import { useEffect } from 'react'; function Example() { useEffect(() => { // 副作用逻辑 console.log('组件挂载或更新'); // 可选的清理函数 return () => { console.log('清理工作'); }; }, [/* 依赖数组 */]); return <div>示例组件</div>; }
执行时机:
// 1. 每次渲染后都执行 useEffect(() => { console.log('每次渲染都会执行'); }); // 2. 只在挂载时执行(空依赖数组) useEffect(() => { console.log('只在挂载时执行'); fetchData(); }, []); // 3. 依赖变化时执行 useEffect(() => { console.log(`count 变化为: ${count}`); document.title = `点击了 ${count} 次`; }, [count]);
清理函数(Cleanup):
function ChatRoom({ roomId }) { useEffect(() => { // 建立连接 const connection = createConnection(roomId); connection.connect(); console.log(`连接到房间: ${roomId}`); // 返回清理函数 return () => { connection.disconnect(); console.log(`断开房间: ${roomId} 的连接`); }; }, [roomId]); return <div>聊天房间: {roomId}</div>; }
数据获取模式:
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(`/api/users/${userId}`, { signal: controller.signal }); const data = await response.json(); if (!isCancelled) { setUser(data); setError(null); } } catch (err) { if (!isCancelled && err.name !== 'AbortError') { setError(err.message); } } finally { if (!isCancelled) { setLoading(false); } } } fetchUser(); return () => { isCancelled = true; controller.abort(); }; }, [userId]); if (loading) return <div>加载中...</div>; if (error) return <div>错误: {error}</div>; return ( <div> <h2>{user.name}</h2> <p>{user.email}</p> </div> ); }
useEffect 与 useLayoutEffect:
import { useLayoutEffect } from 'react'; 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 </div> ); }
- useEffect:在浏览器绘制完成后异步执行,不会阻塞视觉更新 - useLayoutEffect:在浏览器绘制之前同步执行,会阻塞视觉更新
绝大多数情况下应该使用 useEffect,只有在需要同步测量/修改 DOM 时才使用 useLayoutEffect。
5.15 useRef - DOM 引用与持久化值
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。这个对象在组件的整个生命周期内保持不变。
访问 DOM 元素:
import { useRef } from 'react'; function TextInput() { const inputRef = useRef(null); const focusInput = () => { // 直接访问 DOM 元素 inputRef.current.focus(); }; return ( <div> <input ref={inputRef} type="text" placeholder="输入内容" /> <button onClick={focusInput}>聚焦输入框</button> </div> ); }
保存上一次的值:
function Counter() { const [count, setCount] = useState(0); const prevCountRef = useRef(); useEffect(() => { // 在渲染后保存当前值,供下一次渲染使用 prevCountRef.current = count; }); const prevCount = prevCountRef.current; return ( <div> <p>当前: {count}, 上一次: {prevCount}</p> <button onClick={() => setCount(c => c + 1)}>增加</button> </div> ); }
保存定时器 ID:
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> <p>{seconds} 秒</p> <button onClick={start}>开始</button> <button onClick={stop}>停止</button> <button onClick={reset}>重置</button> </div> ); }
转发 ref(forwardRef):
import { forwardRef, useRef } from 'react'; // 子组件转发 ref 到内部 DOM 元素 const FancyInput = forwardRef((props, ref) => { return <input ref={ref} className="fancy-input" {...props} />; }); // 父组件使用 function Parent() { const inputRef = useRef(null); const focusInput = () => { inputRef.current.focus(); }; return ( <div> <FancyInput ref={inputRef} placeholder="输入内容" /> <button onClick={focusInput}>聚焦</button> </div> ); }
useImperativeHandle 自定义暴露的实例值:
import { forwardRef, useRef, useImperativeHandle } from 'react'; const FancyInput = forwardRef((props, ref) => { 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> <FancyInput ref={fancyInputRef} /> <button onClick={handleClick}>操作输入框</button> </div> ); }
5.16 useCallback - 缓存回调函数
useCallback 返回一个 memoized 回调函数。只有当依赖项发生变化时,才会返回新的函数。
基本用法:
import { useCallback } from 'react'; function Parent() { const [count, setCount] = useState(0); const [text, setText] = useState(''); // 有 useCallback:只在 count 变化时创建新函数 const handleClick = useCallback(() => { console.log('count:', count); }, [count]); return ( <div> <button onClick={() => setCount(c => c + 1)}>增加</button> <input value={text} onChange={e => setText(e.target.value)} /> <Child onClick={handleClick} /> </div> ); }
配合 React.memo 使用:
import { memo, useCallback, useState } from 'react'; // 子组件使用 React.memo 进行浅比较 const Child = memo(({ onClick, label }) => { console.log(`${label} 渲染`); return <button onClick={onClick}>{label}</button>; }); 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> <p>Count1: {count1}, Count2: {count2}</p> <Child onClick={handleClick1} label="增加 Count1" /> <Child onClick={handleClick2} label="增加 Count2" /> </div> ); }
何时使用 useCallback:
- 函数作为 props 传递给子组件,且子组件使用了 React.memo
- 函数作为 useEffect 的依赖项
- 函数被其他 Hooks(如 useMemo)依赖
- 函数是一个复杂计算或创建成本较高
避免过度使用:
// 不需要 useCallback 的情况:简单函数直接内联 function SimpleComponent() { const [count, setCount] = useState(0); // 简单函数,不需要 useCallback const increment = () => setCount(c => c + 1); return <button onClick={increment}>{count}</button>; }
5.17 useMemo - 缓存计算结果
useMemo 返回一个 memoized 值。只有当依赖项发生变化时,才会重新计算。
基本用法:
import { useMemo } from 'react'; 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}>{item.name}</li>)} </ul> ); }
对象/数组的稳定性:
function Chart({ data, options }) { // 使用 useMemo 保持对象的引用稳定 const chartOptions = useMemo(() => ({ responsive: true, scales: { y: { beginAtZero: options.startFromZero } } }), [options.startFromZero]); // chartOptions 的引用只在依赖变化时改变 useEffect(() => { console.log('options 变化,重新初始化图表'); initChart(chartOptions); }, [chartOptions]); return <canvas id="chart" />; }
复杂数据的处理:
function DataTable({ rows, sortKey, sortOrder }) { const sortedRows = useMemo(() => { return [...rows].sort((a, b) => { const aVal = a[sortKey]; const bVal = b[sortKey]; if (sortOrder === 'asc') { return aVal > bVal ? 1 : -1; } else { return aVal < bVal ? 1 : -1; } }); }, [rows, sortKey, sortOrder]); return ( <table> <tbody> {sortedRows.map(row => ( <tr key={row.id}> <td>{row.name}</td> <td>{row.value}</td> </tr> ))} </tbody> </table> ); }
useMemo vs useCallback:
// useCallback(fn, deps) 等同于 useMemo(() => fn, deps) // useCallback - 缓存函数 const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]); // useMemo - 缓存值 const memoizedValue = useMemo(() => { return computeExpensiveValue(a, b); }, [a, b]);
5.18 useContext - 跨组件状态共享
useContext 让我们无需为每层组件手动添加 props,就能在组件树间进行数据传递。
创建和使用 Context:
import { createContext, useContext, useState } from 'react'; // 1. 创建 Context const ThemeContext = createContext('light'); // 2. 提供 Context function App() { const [theme, setTheme] = useState('light'); return ( <ThemeContext.Provider value={{ theme, setTheme }}> <Toolbar /> </ThemeContext.Provider> ); } // 3. 消费 Context function Toolbar() { return ( <div> <ThemeButton /> </div> ); } function ThemeButton() { // 使用 useContext 获取上下文值 const { theme, setTheme } = useContext(ThemeContext); return ( <button style={{ background: theme === 'dark' ? '#333' : '#fff' }} onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} > 当前主题: {theme} </button> ); }
多 Context 使用:
const ThemeContext = createContext('light'); const UserContext = createContext(null); function App() { const [theme, setTheme] = useState('light'); const [user, setUser] = useState({ name: '张三' }); return ( <ThemeContext.Provider value={{ theme, setTheme }}> <UserContext.Provider value={{ user, setUser }}> <Layout /> </UserContext.Provider> </ThemeContext.Provider> ); } function Layout() { const { theme } = useContext(ThemeContext); const { user } = useContext(UserContext); return ( <div className={theme}> <h1>欢迎, {user.name}</h1> </div> ); }
Context 分割优化:
// 不好的做法:整个 context 变化导致所有消费者重新渲染 const AppContext = createContext({ theme: 'light', user: null, setTheme: () => {}, setUser: () => {} }); // 更好的做法:将经常变化和很少变化的数据分开 const ThemeContext = createContext({ theme: 'light', setTheme: () => {} }); const UserContext = createContext({ user: null, setUser: () => {} }); // 最佳做法:使用自定义 Hook 封装 context function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error('useTheme 必须在 ThemeProvider 内部使用'); } return context; }
5.19 useReducer - 复杂状态管理
useReducer 是 useState 的替代方案,适用于 state 逻辑较复杂或包含多个子值的情况。
基本用法:
import { useReducer } from 'react'; // 定义 reducer 函数 function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; case 'reset': return { count: 0 }; default: throw new Error('未知 action'); } } function Counter() { const [state, dispatch] = useReducer(reducer, { count: 0 }); return ( <div> <p>Count: {state.count}</p> <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> <button onClick={() => dispatch({ type: 'reset' })}>重置</button> </div> ); }
复杂状态管理:
const initialState = { loading: false, data: null, error: null }; function dataReducer(state, action) { switch (action.type) { case 'FETCH_START': return { ...state, loading: true, error: null }; case 'FETCH_SUCCESS': return { ...state, loading: false, data: action.payload }; case 'FETCH_ERROR': return { ...state, loading: false, error: action.payload }; default: return state; } } function DataComponent() { const [state, dispatch] = useReducer(dataReducer, initialState); const fetchData = async () => { dispatch({ type: 'FETCH_START' }); try { const response = await fetch('/api/data'); const data = await response.json(); dispatch({ type: 'FETCH_SUCCESS', payload: data }); } catch (error) { dispatch({ type: 'FETCH_ERROR', payload: error.message }); } }; return ( <div> {state.loading && <p>加载中...</p>} {state.error && <p>错误: {state.error}</p>} {state.data && <p>数据: {JSON.stringify(state.data)}</p>} <button onClick={fetchData}>获取数据</button> </div> ); }
useReducer vs useState:
| 场景 | 推荐方案 |
| —— | ——— |
| 简单状态(单个值) | useState |
| 复杂状态(多个子值相互依赖) | useReducer |
| 需要优化性能,state 更新逻辑复杂 | useReducer |
| 需要复用状态逻辑 | useReducer + 自定义 Hook |
5.20 其他常用 Hooks
useId - 生成唯一 ID:
import { useId } from 'react'; function Form() { const id = useId(); return ( <div> <label htmlFor={id + '-name'}>姓名:</label> <input id={id + '-name'} type="text" /> <label htmlFor={id + '-email'}>邮箱:</label> <input id={id + '-email'} type="email" /> </div> ); }
useTransition - 非紧急更新:
import { useTransition, useState } from 'react'; function TabContainer() { const [isPending, startTransition] = useTransition(); const [tab, setTab] = useState('home'); const selectTab = (nextTab) => { // 将状态更新标记为低优先级 startTransition(() => { setTab(nextTab); }); }; return ( <div> {isPending && <p>加载中...</p>} <TabButton onClick={() => selectTab('home')}>首页</TabButton> <TabButton onClick={() => selectTab('about')}>关于</TabButton> {tab === 'home' && <HomeTab />} {tab === 'about' && <AboutTab />} </div> ); }
useDeferredValue - 延迟更新值:
import { useDeferredValue, useState } from 'react'; function SearchResults() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); return ( <div> <input value={query} onChange={e => setQuery(e.target.value)} placeholder="搜索..." /> {/* 使用 deferredQuery 显示结果,保持输入流畅 */} <SlowList query={deferredQuery} /> </div> ); }
5.21 自定义 Hooks
自定义 Hooks 是提取组件逻辑到可复用函数的方式。自定义 Hook 是一个函数,其名称以 “use” 开头,内部可以调用其他 Hooks。
useFetch - 数据获取:
import { useState, useEffect } from 'react'; 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 !== 'AbortError') { 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('/api/users'); if (loading) return <div>加载中...</div>; if (error) return <div>错误: {error}</div>; return ( <ul> {users.map(user => <li key={user.id}>{user.name}</li>)} </ul> ); }
useLocalStorage - 本地存储同步:
import { useState, useEffect } from 'react'; function useLocalStorage(key, initialValue) { // 获取初始值 const [storedValue, setStoredValue] = useState(() => { 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, JSON.stringify(storedValue)); } catch (error) { console.error(error); } }, [key, storedValue]); return [storedValue, setStoredValue]; } // 使用 function App() { const [name, setName] = useLocalStorage('name', ''); return ( <input value={name} onChange={e => setName(e.target.value)} placeholder="输入你的名字" /> ); }
useDebounce - 防抖:
import { useState, useEffect } from 'react'; function useDebounce(value, delay) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timer = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(timer); }; }, [value, delay]); return debouncedValue; } // 使用 function SearchInput() { const [input, setInput] = useState(''); const debouncedInput = useDebounce(input, 500); useEffect(() => { if (debouncedInput) { performSearch(debouncedInput); } }, [debouncedInput]); return ( <input value={input} onChange={(e) => setInput(e.target.value)} placeholder="搜索..." /> ); }
usePrevious - 获取上一次的值:
import { useRef, useEffect } from 'react'; 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> <p>当前: {count}, 上一次: {prevCount}</p> <button onClick={() => setCount(c => c + 1)}>增加</button> </div> ); }
useOnClickOutside - 点击外部关闭:
import { useEffect, useRef } from 'react'; function useOnClickOutside(ref, handler) { useEffect(() => { const listener = (event) => { // 如果点击的是 ref 元素内部,不执行 handler if (!ref.current || ref.current.contains(event.target)) { return; } handler(event); }; document.addEventListener('mousedown', listener); document.addEventListener('touchstart', listener); return () => { document.removeEventListener('mousedown', listener); document.removeEventListener('touchstart', listener); }; }, [ref, handler]); } // 使用 function Dropdown() { const ref = useRef(); const [isOpen, setIsOpen] = useState(false); useOnClickOutside(ref, () => setIsOpen(false)); return ( <div ref={ref}> <button onClick={() => setIsOpen(!isOpen)}>切换</button> {isOpen && <div className="dropdown">下拉内容</div>} </div> ); }
5.22 Hooks 最佳实践
1. 只在最顶层调用 Hooks:
// 正确 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:
// 正确 - 在函数组件中 function MyComponent() { const [state, setState] = useState(0); } // 正确 - 在自定义 Hook 中 function useMyHook() { const [state, setState] = useState(0); }
3. 使用 ESLint 插件:
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
4. 分离不相关的副作用:
// 好的做法:分离关注点 function Example() { // 处理订阅 useEffect(() => { const subscription = subscribe(); return () => subscription.unsubscribe(); }, []); // 处理 DOM 操作 useEffect(() => { document.title = '新标题'; }, []); }
5. 避免过度优化:
// 不需要 useMemo 的简单计算 const doubled = count * 2; // 不需要 useCallback 的简单函数 const handleClick = () => setCount(c => c + 1);
5.23 常见陷阱与解决方案
陷阱 1:useEffect 无限循环
// 错误:setState 导致重新渲染,形成无限循环 useEffect(() => { setCount(count + 1); }, [count]); // 正确:使用函数式更新 useEffect(() => { const timer = setTimeout(() => { setCount(c => c + 1); }, 1000); return () => clearTimeout(timer); }, []);
陷阱 2:遗漏依赖
// 错误:遗漏了 count 依赖 useEffect(() => { console.log(`当前计数: ${count}`); }, []); // 警告:count 应该在依赖数组中 // 正确:添加所有依赖 useEffect(() => { console.log(`当前计数: ${count}`); }, [count]);
陷阱 3:对象和数组依赖
// 问题: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 函数中调用、保持依赖数组完整。