React Hooks 深入解析
什么是 Hooks?
Hooks 是 React 16.8 引入的新特性,让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
为什么需要 Hooks?
- 逻辑复用困难:在 class 组件中,逻辑复用通常需要高阶组件或 render props
- 复杂组件难以理解:生命周期方法中经常包含不相关的逻辑
- class 的困惑:需要理解 JavaScript 中 this 的工作方式
基础 Hooks
useState
javascriptimport { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
useEffect
javascriptimport { useState, useEffect } from 'react'; function DataFetcher() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { // 组件挂载时执行 fetch('/api/data') .then(response => response.json()) .then(data => { setData(data); setLoading(false); }); // 清理函数(可选) return () => { // 组件卸载时执行清理 }; }, []); // 依赖数组,空数组表示只执行一次 if (loading) return <div>Loading...</div>; return <div>{JSON.stringify(data)}</div>; }
常用 Hooks
useContext
javascriptimport { createContext, useContext } from 'react'; const ThemeContext = createContext('light'); function App() { return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } function Toolbar() { const theme = useContext(ThemeContext); return <div>Current theme: {theme}</div>; }
useReducer
javascriptimport { useReducer } from 'react'; function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, { count: 0 }); return ( <> Count: {state.count} <button onClick={() => dispatch({ type: 'decrement' })}>-</button> <button onClick={() => dispatch({ type: 'increment' })}>+</button> </> ); }
自定义 Hooks
自定义 Hook 是一个函数,其名称以 "use" 开头,函数内部可以调用其他的 Hook。
useLocalStorage
javascriptimport { useState, useEffect } from 'react'; function useLocalStorage(key, initialValue) { // 从 localStorage 读取初始值 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', 'John'); return ( <input value={name} onChange={e => setName(e.target.value)} /> ); }
Hooks 规则
- 只在最顶层使用 Hook:不要在循环、条件或嵌套函数中调用 Hook
- 只在 React 函数中调用 Hook:在 React 函数组件或自定义 Hook 中调用
常见面试题
1. useState 和 useReducer 的区别
| 特性 | useState | useReducer |
|---|---|---|
| 适用场景 | 简单的状态逻辑 | 复杂的状态逻辑 |
| 状态更新 | 直接设置新值 | 通过 dispatch action |
| 性能优化 | 自动合并更新 | 需要手动优化 |
| 测试 | 相对简单 | 更容易测试 |
2. useEffect 的依赖数组
javascriptuseEffect(() => { // 1. 没有依赖数组:每次渲染都执行 }); useEffect(() => { // 2. 空依赖数组:只执行一次(componentDidMount) }, []); useEffect(() => { // 3. 有依赖数组:依赖变化时执行 }, [count, name]);
3. 实现 usePrevious Hook
javascriptimport { useRef, useEffect } from 'react'; function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }, [value]); return ref.current; } // 使用示例 function Counter() { const [count, setCount] = useState(0); const prevCount = usePrevious(count); return ( <div> <p>Now: {count}, before: {prevCount}</p> <button onClick={() => setCount(count + 1)}>+</button> </div> ); }
最佳实践
- 按逻辑关注点组织 Hooks:将相关的逻辑放在同一个 useEffect 中
- 提取自定义 Hooks:当逻辑复用时,提取为自定义 Hook
- 使用 useCallback 和 useMemo 优化性能:避免不必要的重新渲染
- 保持 Hook 的纯净:不要在 Hook 中执行副作用(除了 useEffect)
- 使用 eslint-plugin-react-hooks:自动检查 Hook 规则
性能优化
useMemo
javascriptimport { useMemo } from 'react'; function ExpensiveComponent({ a, b }) { const result = useMemo(() => { // 昂贵的计算 return computeExpensiveValue(a, b); }, [a, b]); // 只有 a 或 b 变化时重新计算 return <div>{result}</div>; }
useCallback
javascriptimport { useCallback } from 'react'; function ParentComponent() { const [count, setCount] = useState(0); const handleClick = useCallback(() => { console.log('Clicked!', count); }, [count]); // 只有 count 变化时重新创建函数 return <ChildComponent onClick={handleClick} />; }
常见陷阱
- 无限循环:在 useEffect 中更新依赖的状态
- 过时闭包:在异步回调中访问过时的状态
- 条件渲染中的 Hook:违反 Hook 规则
- 忘记清理:在 useEffect 中创建了需要清理的资源