React Hooks深度解析

前言

React Hooks彻底改变了React组件的编写方式,让函数组件拥有了类组件的所有能力。本文将深入探讨Hooks的原理、使用技巧和最佳实践,帮助你写出更优雅、更高效的React代码。


一、为什么需要Hooks

1.1 Class组件的痛点

问题1:逻辑复用困难

1
2
3
4
5
6
7
8
// 使用HOC导致wrapper hell
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是undefined!
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));

// 监听resize
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() {
// ❌ 每次渲染都会执行computeExpensiveValue
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(() => {
// 副作用:fetch数据
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);
}, []); // 空数组:只在mount和unmount时执行

return <div>Seconds: {seconds}</div>;
}

依赖数组详解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ❌ 缺少依赖,可能造成bug
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // userId变化时不会重新执行

// ✅ 正确声明依赖
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
// 创建Context
const ThemeContext = React.createContext('light');
const UserContext = React.createContext(null);

// Provider
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>
);
}

// 使用Context
function ThemedButton() {
// ✅ 使用useContext
const theme = useContext(ThemeContext);
const user = useContext(UserContext);

return (
<button className={theme}>
Hello, {user?.name}!
</button>
);
}

// ❌ Class组件需要使用Consumer
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
// 定义reducer
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;
}
}

// 使用useReducer
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 {
// 允许value是函数
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..."
/>
);
}

3.4 useMediaQuery

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>
);
}

// ✅ 使用React.memo优化
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([]);

// ✅ 使用useCallback缓存函数
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
// ❌ 问题:count永远是0
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1); // count被闭包捕获
}, 1000);

return () => clearInterval(timer);
}, []);

return <div>{count}</div>;
}

// ✅ 解决方案1:使用函数式更新
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1); // 使用最新的值
}, 1000);

return () => clearInterval(timer);
}, []);

return <div>{count}</div>;
}

// ✅ 解决方案2:添加依赖
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);

return () => clearInterval(timer);
}, [count]); // 依赖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); // setData导致重新渲染
}); // 没有依赖数组!
}

// ✅ 正确
function GoodComponent() {
const [data, setData] = useState([]);

useEffect(() => {
fetchData().then(setData);
}, []); // 空依赖数组
}

六、总结

核心要点

  1. useState:管理组件状态
  2. useEffect:处理副作用
  3. useContext:共享全局数据
  4. useReducer:复杂状态管理
  5. useCallback/useMemo:性能优化
  6. useRef:保存可变值和访问DOM

最佳实践

  • 遵循Hooks规则(顶层调用、只在函数组件中使用)
  • 正确声明依赖数组
  • 使用ESLint插件检查
  • 提取自定义Hooks复用逻辑
  • 合理使用性能优化API

掌握React Hooks,让你的代码更简洁、更易维护!


参考资料