React Hooks深度解析
前言
React Hooks彻底改变了React组件的编写方式,让函数组件拥有了类组件的所有能力。本文将深入探讨Hooks的原理、使用技巧和最佳实践,帮助你写出更优雅、更高效的React代码。
一、为什么需要Hooks
1.1 Class组件的痛点
问题1:逻辑复用困难
1 2 3 4 5 6 7 8
| export default withRouter( withAuth( withTheme( withLoading(MyComponent) ) ) );
|
问题2:生命周期复杂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class UserProfile extends React.Component { componentDidMount() { this.subscribeUser(); this.startTimer(); window.addEventListener('resize', this.handleResize); } componentWillUnmount() { this.unsubscribeUser(); this.stopTimer(); window.removeEventListener('resize', this.handleResize); } }
|
问题3:this绑定问题
1 2 3 4 5 6 7 8 9 10 11
| class Counter extends React.Component { handleClick() { this.setState({ count: this.state.count + 1 }); } render() { return <button onClick={this.handleClick.bind(this)}>Click</button>; } }
|
1.2 Hooks的优势
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function UserProfile() { const [user, setUser] = useState(null); const [windowSize, setWindowSize] = useState(window.innerWidth); useEffect(() => { const unsubscribe = subscribeUser(data => setUser(data)); const handleResize = () => setWindowSize(window.innerWidth); window.addEventListener('resize', handleResize); return () => { unsubscribe(); window.removeEventListener('resize', handleResize); }; }, []); return <div>{user?.name}</div>; }
|
二、核心Hooks详解
2.1 useState
基础用法:
1 2 3 4 5 6 7 8 9 10 11
| function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>+1</button> <button onClick={() => setCount(c => c + 1)}>+1 (函数式)</button> </div> ); }
|
惰性初始化:
1 2 3 4 5 6 7
| function ExpensiveComponent() { const [value, setValue] = useState(computeExpensiveValue()); const [value, setValue] = useState(() => computeExpensiveValue()); }
|
对象状态更新:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| function Form() { const [formData, setFormData] = useState({ username: '', email: '', password: '' }); const handleChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }; return ( <form> <input name="username" value={formData.username} onChange={handleChange} /> <input name="email" value={formData.email} onChange={handleChange} /> </form> ); }
|
2.2 useEffect
基础副作用:
1 2 3 4 5 6 7 8 9 10
| function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { fetchUser(userId).then(data => setUser(data)); }, [userId]); return <div>{user?.name}</div>; }
|
清理副作用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function Timer() { const [seconds, setSeconds] = useState(0); useEffect(() => { const interval = setInterval(() => { setSeconds(s => s + 1); }, 1000); return () => clearInterval(interval); }, []); return <div>Seconds: {seconds}</div>; }
|
依赖数组详解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| useEffect(() => { fetchUser(userId).then(setUser); }, []);
useEffect(() => { fetchUser(userId).then(setUser); }, [userId]);
useEffect(() => { console.log('Every render'); });
|
异步处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| function SearchResults({ query }) { const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { let cancelled = false; const fetchResults = async () => { setLoading(true); try { const data = await searchAPI(query); if (!cancelled) { setResults(data); } } catch (error) { if (!cancelled) { console.error(error); } } finally { if (!cancelled) { setLoading(false); } } }; fetchResults(); return () => { cancelled = true; }; }, [query]); if (loading) return <div>Loading...</div>; return ( <ul> {results.map(item => ( <li key={item.id}>{item.title}</li> ))} </ul> ); }
|
2.3 useContext
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| const ThemeContext = React.createContext('light'); const UserContext = React.createContext(null);
function App() { const [theme, setTheme] = useState('light'); const [user, setUser] = useState(null); return ( <ThemeContext.Provider value={theme}> <UserContext.Provider value={user}> <Toolbar /> </UserContext.Provider> </ThemeContext.Provider> ); }
function ThemedButton() { const theme = useContext(ThemeContext); const user = useContext(UserContext); return ( <button className={theme}> Hello, {user?.name}! </button> ); }
class ThemedButtonOld extends React.Component { render() { return ( <ThemeContext.Consumer> {theme => ( <UserContext.Consumer> {user => ( <button className={theme}> Hello, {user?.name}! </button> )} </UserContext.Consumer> )} </ThemeContext.Consumer> ); } }
|
2.4 useReducer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| function todoReducer(state, action) { switch (action.type) { case 'ADD_TODO': return [...state, { id: Date.now(), text: action.payload, completed: false }]; case 'TOGGLE_TODO': return state.map(todo => todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo ); case 'DELETE_TODO': return state.filter(todo => todo.id !== action.payload); default: return state; } }
function TodoList() { const [todos, dispatch] = useReducer(todoReducer, []); const [input, setInput] = useState(''); const handleAdd = () => { dispatch({ type: 'ADD_TODO', payload: input }); setInput(''); }; return ( <div> <input value={input} onChange={e => setInput(e.target.value)} onKeyPress={e => e.key === 'Enter' && handleAdd()} /> <button onClick={handleAdd}>Add</button> <ul> {todos.map(todo => ( <li key={todo.id}> <input type="checkbox" checked={todo.completed} onChange={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })} /> <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}> {todo.text} </span> <button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}> Delete </button> </li> ))} </ul> </div> ); }
|
2.5 useCallback 和 useMemo
useCallback:缓存函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| function Parent() { const [count, setCount] = useState(0); const [text, setText] = useState(''); const handleClick = () => { console.log('Clicked'); }; const handleClickMemo = useCallback(() => { console.log('Clicked'); }, []); return ( <div> <input value={text} onChange={e => setText(e.target.value)} /> {/* text变化时,Child不会重新渲染 */} <Child onClick={handleClickMemo} /> </div> ); }
const Child = React.memo(({ onClick }) => { console.log('Child rendered'); return <button onClick={onClick}>Click</button>; });
|
useMemo:缓存计算结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function ProductList({ products, filter }) { const filtered = products .filter(p => p.category === filter) .sort((a, b) => b.price - a.price); const filteredProducts = useMemo(() => { console.log('Recomputing...'); return products .filter(p => p.category === filter) .sort((a, b) => b.price - a.price); }, [products, filter]); return ( <ul> {filteredProducts.map(product => ( <li key={product.id}>{product.name} - ${product.price}</li> ))} </ul> ); }
|
2.6 useRef
保存可变值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| function Timer() { const [count, setCount] = useState(0); const intervalRef = useRef(null); const start = () => { if (intervalRef.current) return; intervalRef.current = setInterval(() => { setCount(c => c + 1); }, 1000); }; const stop = () => { clearInterval(intervalRef.current); intervalRef.current = null; }; useEffect(() => { return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; }, []); return ( <div> <p>Count: {count}</p> <button onClick={start}>Start</button> <button onClick={stop}>Stop</button> </div> ); }
|
访问DOM元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function TextInput() { const inputRef = useRef(null); const focus = () => { inputRef.current?.focus(); }; return ( <div> <input ref={inputRef} /> <button onClick={focus}>Focus Input</button> </div> ); }
|
三、自定义Hooks
3.1 useFetch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| function useFetch(url, options = {}) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; const fetchData = async () => { setLoading(true); setError(null); try { const response = await fetch(url, options); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const json = await response.json(); if (!cancelled) { setData(json); } } catch (e) { if (!cancelled) { setError(e.message); } } finally { if (!cancelled) { setLoading(false); } } }; fetchData(); return () => { cancelled = true; }; }, [url, JSON.stringify(options)]); return { data, loading, error }; }
function UserProfile({ userId }) { const { data: user, loading, error } = useFetch( `https://api.example.com/users/${userId}` ); if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error}</div>; return <div>{user?.name}</div>; }
|
3.2 useLocalStorage
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| 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; } }); const setValue = (value) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { console.error(error); } }; return [storedValue, setValue]; }
function App() { const [name, setName] = useLocalStorage('name', ''); return ( <input value={name} onChange={e => setName(e.target.value)} /> ); }
|
3.3 useDebounce
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| function useDebounce(value, delay) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timer = setTimeout(() => { setDebouncedValue(value); }, delay); return () => clearTimeout(timer); }, [value, delay]); return debouncedValue; }
function SearchBox() { const [searchTerm, setSearchTerm] = useState(''); const debouncedSearchTerm = useDebounce(searchTerm, 500); useEffect(() => { if (debouncedSearchTerm) { searchAPI(debouncedSearchTerm).then(setResults); } }, [debouncedSearchTerm]); return ( <input value={searchTerm} onChange={e => setSearchTerm(e.target.value)} placeholder="Search..." /> ); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| function useMediaQuery(query) { const [matches, setMatches] = useState( () => window.matchMedia(query).matches ); useEffect(() => { const mediaQuery = window.matchMedia(query); const handler = (e) => setMatches(e.matches); mediaQuery.addEventListener('change', handler); return () => { mediaQuery.removeEventListener('change', handler); }; }, [query]); return matches; }
function ResponsiveComponent() { const isMobile = useMediaQuery('(max-width: 768px)'); const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)'); const isDesktop = useMediaQuery('(min-width: 1025px)'); return ( <div> {isMobile && <MobileLayout />} {isTablet && <TabletLayout />} {isDesktop && <DesktopLayout />} </div> ); }
|
四、性能优化
4.1 避免不必要的渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function Parent() { const [count, setCount] = useState(0); return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <ExpensiveChild /> </div> ); }
const ExpensiveChild = React.memo(() => { console.log('ExpensiveChild rendered'); return <div>Expensive Component</div>; });
|
4.2 正确使用useCallback
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| function TodoList() { const [todos, setTodos] = useState([]); const addTodo = useCallback((text) => { setTodos(prev => [...prev, { id: Date.now(), text, completed: false }]); }, []); const toggleTodo = useCallback((id) => { setTodos(prev => prev.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo )); }, []); return ( <div> <TodoInput onAdd={addTodo} /> {todos.map(todo => ( <TodoItem key={todo.id} todo={todo} onToggle={toggleTodo} /> ))} </div> ); }
const TodoItem = React.memo(({ todo, onToggle }) => { console.log('TodoItem rendered:', todo.id); return ( <li> <input type="checkbox" checked={todo.completed} onChange={() => onToggle(todo.id)} /> {todo.text} </li> ); });
|
4.3 虚拟滚动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| function useVirtualScroll(items, itemHeight, containerHeight) { const [scrollTop, setScrollTop] = useState(0); const visibleStart = Math.floor(scrollTop / itemHeight); const visibleEnd = Math.ceil((scrollTop + containerHeight) / itemHeight); const visibleItems = items.slice(visibleStart, visibleEnd); const totalHeight = items.length * itemHeight; const offsetY = visibleStart * itemHeight; return { visibleItems, totalHeight, offsetY, onScroll: (e) => setScrollTop(e.target.scrollTop) }; }
function LargeList({ items }) { const { visibleItems, totalHeight, offsetY, onScroll } = useVirtualScroll( items, 50, 600 ); return ( <div style={{ height: 600, overflow: 'auto' }} onScroll={onScroll} > <div style={{ height: totalHeight, position: 'relative' }}> <div style={{ transform: `translateY(${offsetY}px)` }}> {visibleItems.map(item => ( <div key={item.id} style={{ height: 50 }}> {item.text} </div> ))} </div> </div> </div> ); }
|
五、常见陷阱与解决方案
5.1 闭包陷阱
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| function Counter() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(timer); }, []); return <div>{count}</div>; }
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { setCount(c => c + 1); }, 1000); return () => clearInterval(timer); }, []); return <div>{count}</div>; }
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(timer); }, [count]); return <div>{count}</div>; }
|
5.2 无限循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function BadComponent() { const [data, setData] = useState([]); useEffect(() => { fetchData().then(setData); }); }
function GoodComponent() { const [data, setData] = useState([]); useEffect(() => { fetchData().then(setData); }, []); }
|
六、总结
核心要点
- useState:管理组件状态
- useEffect:处理副作用
- useContext:共享全局数据
- useReducer:复杂状态管理
- useCallback/useMemo:性能优化
- useRef:保存可变值和访问DOM
最佳实践
- 遵循Hooks规则(顶层调用、只在函数组件中使用)
- 正确声明依赖数组
- 使用ESLint插件检查
- 提取自定义Hooks复用逻辑
- 合理使用性能优化API
掌握React Hooks,让你的代码更简洁、更易维护!
参考资料