React Hook 核心指南:从实战到源码,彻底掌握状态与副作用
Hook 的出现让函数组件拥有了管理状态和副作用的能力,极大地提升了代码的可读性和复用性。今天,我想和大家分享 React 中最核心、最常用的几个 Hook:useState
、useEffect
、useContext
、useReducer
、useCallback
、useMemo
和 useRef
。我会从基本用法讲起,结合实际例子,深入探讨它们的原理和最佳实践,并手写简化版实现,最后附上高频面试题。希望这篇博客能让你对 React Hook 有更深刻的理解。
1. useState:管理组件状态
1.1 基本概念
useState
是最基础的 Hook,用于在函数组件中添加状态。它接收一个初始状态值,返回一个包含当前状态和更新函数的数组。
const [state, setState] = useState(initialState);
-
state
:当前状态值。 -
setState
:更新状态的函数。 -
initialState
:初始状态,可以是任意值(原始值、对象、函数等)。
1.2 使用场景
计数器是最经典的例子:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>当前计数: {count}</p>
<button onClick={() => setCount(count + 1)}>
增加
</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>
减少
</button>
</div>
);
}
关键点:
-
setState
是异步的,多次调用会进行批处理。 - 更新函数可以接收一个函数,该函数的参数是前一个状态值(
prevState
),这在需要基于前一个状态更新时非常有用。
1.3 手写实现(简化版)
// 模拟 React 内部状态存储
let hooks = [];
let currentHookIndex = 0;
function useState(initialValue) {
const hookIndex = currentHookIndex;
// 如果是第一次调用,初始化状态
if (!hooks[hookIndex]) {
hooks[hookIndex] = initialValue;
}
// 返回当前状态和更新函数
const setState = (newValue) => {
// 如果传入的是函数,执行它
const value = typeof newValue === 'function'
? newValue(hooks[hookIndex])
: newValue;
hooks[hookIndex] = value;
// 模拟触发重新渲染
render();
};
return [hooks[hookIndex], setState];
}
// 模拟组件渲染
function render() {
currentHookIndex = 0; // 重置索引
// 重新执行组件函数
App();
}
function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState('张三');
console.log('Count:', count, 'Name:', name);
}
// 初始渲染
App(); // Count: 0 Name: 张三
setCount(1); // Count: 1 Name: 张三
setName('李四'); // Count: 1 Name: 李四
注意: 这只是一个极度简化的模型,真实的 React 使用 Fiber
节点和 Hook
链表来管理状态。
1.4 注意事项
-
不要在条件或循环中使用 Hook:React 依赖 Hook 的调用顺序来正确匹配状态。违反规则会导致
Invalid hook call
错误。 -
状态更新是异步的:
setState
后立即读取状态可能得不到最新值。 -
函数式更新:当新状态依赖于前一个状态时,使用函数形式
setState(prev => prev + 1)
。
2. useEffect:处理副作用
2.1 基本概念
useEffect
用于在函数组件中执行副作用操作,如数据获取、订阅、手动 DOM 操作等。它替代了类组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
。
useEffect(() => {
// 执行副作用
return () => {
// 清理副作用(可选)
};
}, [dependencies]); // 依赖数组
2.2 使用场景
场景一:数据获取
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 模拟 API 请求
const fetchUser = async () => {
setLoading(true);
try {
// 假设这里调用 API
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('获取用户信息失败:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // 仅当 userId 变化时重新执行
if (loading) return <div>加载中...</div>;
if (!user) return <div>用户不存在</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
场景二:订阅和清理
import React, { useState, useEffect } from 'react';
function MouseTracker() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
// 添加事件监听器
window.addEventListener('mousemove', handleMouseMove);
// 返回清理函数
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, []); // 空依赖数组,只在挂载时执行一次
return (
<div>
鼠标位置: {position.x}, {position.y}
</div>
);
}
2.3 依赖数组详解
-
[]
:只在组件挂载时执行一次(类似componentDidMount
)。 -
[dep1, dep2]
:当dep1
或dep2
变化时执行。 - 不传:每次组件重新渲染时都执行(不推荐,容易造成性能问题或无限循环)。
2.4 手写实现(简化版)
let dependencies = [];
let cleanup = null;
function useEffect(callback, deps) {
const hasChanged = !deps ||
deps.some((dep, index) => !dependencies[index] || dep !== dependencies[index]);
if (hasChanged) {
// 执行清理函数(如果存在)
if (cleanup) {
cleanup();
}
// 执行副作用
cleanup = callback();
// 更新依赖
dependencies = deps || [];
}
}
// 模拟组件
function Component({ count }) {
useEffect(() => {
console.log('Effect 执行,count:', count);
return () => {
console.log('清理 Effect,count:', count);
};
}, [count]);
return <div>Count: {count}</div>;
}
// 初始渲染
Component({ count: 0 }); // Effect 执行,count: 0
// 更新
Component({ count: 1 }); // 清理 Effect,count: 0 -> Effect 执行,count: 1
2.5 注意事项
-
必须返回函数或
undefined
:清理函数必须是同步的。 -
避免无限循环:确保依赖数组正确,避免在
useEffect
内部更新依赖项。 -
异步操作:不能直接在
useEffect
回调中使用async
,需要在内部创建异步函数。
// ❌ 错误
useEffect(async () => {
const data = await fetchData();
}, []);
// ✅ 正确
useEffect(() => {
const fetchData = async () => {
const data = await fetchData();
};
fetchData();
}, []);
3. useContext:跨组件传递数据
3.1 基本概念
useContext
用于订阅 React 的 Context
。它接收一个 context
对象(React.createContext
的返回值),并返回当前 context
的值。
const value = useContext(MyContext);
3.2 使用场景
主题切换是经典例子:
import React, { createContext, useContext, useState } from 'react';
// 创建 Context
const ThemeContext = createContext();
// 主题提供者组件
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 使用 Context 的组件
function Header() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<header style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
<h1>我的网站</h1>
<button onClick={toggleTheme}>
切换到 {theme === 'light' ? '暗色' : '亮色'} 主题
</button>
</header>
);
}
// 根组件
function App() {
return (
<ThemeProvider>
<Header />
<MainContent />
</ThemeProvider>
);
}
3.3 手写实现(简化版)
// 模拟 Context
const contextStack = [];
function createContext(defaultValue) {
return { defaultValue };
}
function Provider({ context, value, children }) {
contextStack.push(value);
const result = children;
contextStack.pop();
return result;
}
function useContext(context) {
return contextStack[contextStack.length - 1] || context.defaultValue;
}
// 使用
const ThemeContext = createContext('light');
function ThemeProvider({ children }) {
const [theme] = useState('dark');
return Provider({ context: ThemeContext, value: theme, children });
}
function ThemeDisplay() {
const theme = useContext(ThemeContext);
return <div>当前主题: {theme}</div>;
}
3.4 注意事项
-
useContext
会订阅Context
的变化,当Provider
的value
变化时,所有使用该Context
的组件都会重新渲染。 - 避免将
Context
用于频繁变化的状态,可能导致性能问题。
4. useReducer:管理复杂状态逻辑
4.1 基本概念
useReducer
是 useState
的替代方案,适用于状态逻辑较复杂的情况(如包含多个子值或下一个状态依赖于前一个状态)。它接收一个 reducer
函数和初始状态,返回当前状态和 dispatch
函数。
const [state, dispatch] = useReducer(reducer, initialState);
-
reducer
:(state, action) => newState
,纯函数。 -
initialState
:初始状态。
4.2 使用场景
管理表单或购物车状态:
import React, { useReducer } from 'react';
// 定义 action 类型
const ADD_ITEM = 'ADD_ITEM';
const REMOVE_ITEM = 'REMOVE_ITEM';
const UPDATE_QUANTITY = 'UPDATE_QUANTITY';
// Reducer 函数
function cartReducer(state, action) {
switch (action.type) {
case ADD_ITEM:
return {
...state,
items: [...state.items, action.payload]
};
case REMOVE_ITEM:
return {
...state,
items: state.items.filter(item => item.id !== action.payload.id)
};
case UPDATE_QUANTITY:
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
)
};
default:
return state;
}
}
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
const addItem = (item) => {
dispatch({ type: ADD_ITEM, payload: item });
};
const removeItem = (id) => {
dispatch({ type: REMOVE_ITEM, payload: { id } });
};
return (
<div>
<h2>购物车</h2>
<ul>
{state.items.map(item => (
<li key={item.id}>
{item.name} - 数量: {item.quantity}
<button onClick={() => removeItem(item.id)}>删除</button>
</li>
))}
</ul>
<button onClick={() => addItem({ id: Date.now(), name: '商品', quantity: 1 })}>
添加商品
</button>
</div>
);
}
4.3 手写实现(简化版)
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
const dispatch = (action) => {
const newState = reducer(state, action);
setState(newState);
};
return [state, dispatch];
}
// 使用
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
default:
return state;
}
}, { count: 0 });
4.4 注意事项
-
useReducer
适合状态更新逻辑复杂或需要处理多个 action 的场景。 -
reducer
必须是纯函数。
5. useCallback:缓存函数
5.1 基本概念
useCallback
返回一个记忆化的回调函数。它接收一个内联回调函数和依赖数组,只有当依赖项变化时,才会返回新的函数。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
5.2 使用场景
避免子组件不必要的重新渲染:
import React, { useState, useCallback } from 'react';
// 子组件(使用 React.memo 优化)
const ExpensiveComponent = React.memo(({ onClick, value }) => {
console.log('ExpensiveComponent 渲染');
return (
<button onClick={onClick}>
点击我 ({value})
</button>
);
});
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// 使用 useCallback 缓存函数
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []); // 依赖数组为空,函数不会变化
return (
<div>
<p>计数: {count}</p>
<input value={text} onChange={e => setText(e.target.value)} />
{/* 传递缓存的函数 */}
<ExpensiveComponent onClick={handleClick} value={count} />
</div>
);
}
如果没有 useCallback
,每次 Parent
重新渲染时,handleClick
都会是一个新的函数,导致 ExpensiveComponent
重新渲染。
5.3 手写实现(简化版)
let memoizedCallback = null;
let deps = null;
function useCallback(callback, dependencies) {
const hasChanged = !deps ||
dependencies.some((dep, index) => !deps[index] || dep !== deps[index]);
if (hasChanged) {
memoizedCallback = callback;
deps = dependencies;
}
return memoizedCallback;
}
5.4 注意事项
- 只在需要传递给子组件且子组件使用
React.memo
时才使用useCallback
。 - 过度使用
useCallback
可能导致内存占用增加。
6. useMemo:缓存计算结果
6.1 基本概念
useMemo
返回一个记忆化的值。它接收一个计算函数和依赖数组,只有当依赖项变化时,才会重新计算。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
6.2 使用场景
优化昂贵的计算:
import React, { useState, useMemo } from 'react';
function Fibonacci({ n }) {
// 模拟昂贵的计算
const fibonacci = (num) => {
if (num <= 1) return num;
return fibonacci(num - 1) + fibonacci(num - 2);
};
// 使用 useMemo 缓存计算结果
const result = useMemo(() => fibonacci(n), [n]);
return <div>斐波那契数列第 {n} 项: {result}</div>;
}
function App() {
const [n, setN] = useState(10);
const [input, setInput] = useState('');
return (
<div>
<input value={input} onChange={e => setInput(e.target.value)} placeholder="输入文本" />
<Fibonacci n={n} />
<button onClick={() => setN(prev => prev + 1)}>增加 n</button>
</div>
);
}
如果没有 useMemo
,每次 input
变化导致 App
重新渲染时,都会重新计算 fibonacci(n)
。
6.3 手写实现(简化版)
let memoizedValue = null;
let deps = null;
function useMemo(factory, dependencies) {
const hasChanged = !deps ||
dependencies.some((dep, index) => !deps[index] || dep !== deps[index]);
if (hasChanged) {
memoizedValue = factory();
deps = dependencies;
}
return memoizedValue;
}
6.4 注意事项
- 只在计算确实昂贵时使用
useMemo
。 - 不要为了“优化”而使用,React 的重新渲染本身并不慢。
7. useRef:获取 DOM 节点或保存可变值
7.1 基本概念
useRef
返回一个可变的 ref
对象,其 .current
属性被初始化为传入的参数。ref
对象在组件的整个生命周期内保持不变。
const refContainer = useRef(initialValue);
7.2 使用场景
场景一:访问 DOM 元素
import React, { useRef, useEffect } from 'react';
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// 直接操作 DOM
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>
聚焦输入框
</button>
</>
);
}
场景二:保存可变值(不触发重新渲染)
import React, { useState, useEffect, useRef } from 'react';
function Timer() {
const [count, setCount] = useState(0);
// 使用 ref 保存计时器 ID
const intervalRef = useRef();
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => {
// 清理时使用 ref
clearInterval(intervalRef.current);
};
}, []);
return <div>计数: {count}</div>;
}
7.3 手写实现(简化版)
function useRef(initialValue) {
const refObject = { current: initialValue };
return refObject;
}
7.4 注意事项
-
useRef
返回的对象在组件生命周期内是同一个。 - 修改
.current
不会触发组件重新渲染。 - 可以用于保存任何可变值,如定时器 ID、上一次的 props 等。
8. 常见面试题
-
useState
的setState
是同步还是异步?- 在 React 事件处理中是异步批处理的;在
setTimeout
或原生事件中是同步的。
- 在 React 事件处理中是异步批处理的;在
-
useEffect
的清理函数在什么时候执行?- 在组件卸载时,或在下一次
useEffect
执行前(如果依赖变化)。
- 在组件卸载时,或在下一次
-
useCallback
和useMemo
的区别是什么?-
useCallback
缓存函数,useMemo
缓存值。useCallback(fn, deps)
等价于useMemo(() => fn, deps)
。
-
-
如何在
useEffect
中使用async
函数?- 在
useEffect
回调内部定义并立即调用一个async
函数。
- 在
-
useRef
和createRef
的区别?-
useRef
在函数组件中使用,每次渲染返回同一个对象;createRef
每次调用都返回新对象,通常在类组件中使用。
-
-
为什么不能在条件中使用 Hook?
- React 依赖 Hook 的调用顺序来正确匹配状态,条件使用会破坏这个顺序。
-
useReducer
适合什么场景?- 状态逻辑复杂、包含多个子值、或下一个状态依赖于前一个状态时。
-
useContext
会导致性能问题吗?- 如果
Provider
的value
频繁变化,可能会导致所有订阅的组件重新渲染。可以使用useMemo
缓存value
或拆分Context
。
- 如果
结语
React Hook 让函数组件变得强大而灵活。掌握这些核心 Hook 的用法、原理和最佳实践,是成为一名优秀 React 开发者的关键。记住:
-
useState
:管理简单状态。 -
useEffect
:处理副作用,注意依赖和清理。 -
useContext
:跨层级传递数据。 -
useReducer
:管理复杂状态逻辑。 -
useCallback
/useMemo
:性能优化,按需使用。 -
useRef
:访问 DOM 或保存可变值。
在实际项目中,多思考、多实践,你会发现 Hook 的组合能解决各种复杂的 UI 问题。希望这篇博客能成为你深入 React 的坚实阶梯。