Hooks(React 16.8 引入)是 React 现代开发的核心:它让函数组件拥有了状态管理、副作用处理、上下文消费、引用保存等全部能力,彻底终结了类组件的生命周期混乱。本文从每个内置 Hook 的深度用法,到自定义 Hook 设计模式,再到性能陷阱与最佳实践,帮你建立对 Hooks 的系统性理解。
一、Hooks 核心规则
Hooks 依赖调用顺序来维护内部状态,因此必须遵守两条铁律:
- 只在最顶层调用:不在 if/for/while 中调用
- 只在 React 函数中调用:函数组件或自定义 Hooks
function ValidComponent() {
const [count, setCount] = useState(0); // ✅
const [name, setName] = useState(''); // ✅
useEffect(() => {}, [count]); // ✅
return <div>{count}</div>;
}
function InvalidComponent() {
if (condition) {
const [state] = useState(0); // ❌ 在 if 中
}
return <div></div>;
}
# 安装 ESLint 插件自动检查
npm install -D eslint-plugin-react-hooks
// .eslintrc.js
module.exports = {
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
};
二、useState:状态管理
2.1 基础用法
const [count, setCount] = useState(0);
// 更新
setCount(count + 1); // 替换为新值
setCount(c => c + 1); // 函数式更新(基于前值,更安全)
2.2 对象状态更新
// ❌ 错误:直接修改对象
const [user, setUser] = useState({ name: 'Alice', age: 25 });
user.age = 26; // 未触发重新渲染!
// ✅ 正确:全新对象
setUser({ ...user, age: 26 });
// 或者用 immer
import { useImmer } from 'use-immer';
const [user, updateUser] = useImmer({ name: 'Alice', age: 25 });
updateUser(draft => { draft.age = 26; });
2.3 延迟初始化(避免重复计算)
// ❌ 每次渲染都调用 expensiveCalculation
const [state, setState] = useState(expensiveCalculation());
// ✅ 只在首次渲染调用
const [state, setState] = useState(() => expensiveCalculation());
2.4 状态同步问题:useState 是异步的
function BuggyComponent() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
console.log(count); // ❌ 仍是旧值(0)
// 修改在下次渲染时才生效
}
// ✅ 函数式更新解决
function handleClickFixed() {
setCount(c => {
const next = c + 1;
console.log(next); // 正确
return next;
});
}
}
三、useEffect:副作用处理
3.1 三种执行时机
useEffect(() => {
// 组件挂载时执行
console.log('mounted');
return () => {
// 组件卸载时执行(清理)
console.log('unmounted');
};
}, []); // 空依赖数组 = 只在挂载/卸载执行
useEffect(() => {
// 依赖变化时执行(包括首次挂载)
console.log('count changed:', count);
}, [count]); // 依赖数组
useEffect(() => {
// 每次渲染后都执行
console.log('every render');
}); // 无依赖数组
3.2 常见副作用场景
// 数据获取
useEffect(() => {
let cancelled = false;
async function fetchData() {
const res = await fetch(`/api/user/${userId}`);
const data = await res.json();
if (!cancelled) setUser(data);
}
fetchData();
return () => { cancelled = true; };
}, [userId]);
// 订阅
useEffect(() => {
const subscription = api.subscribe(handleUpdate);
return () => subscription.unsubscribe();
}, []);
// DOM 操作
useEffect(() => {
document.title = `Messages (${unreadCount})`;
}, [unreadCount]);
// 定时器
useEffect(() => {
const id = setInterval(() => setTime(Date.now()), 1000);
return () => clearInterval(id);
}, []);
3.3 避免无限循环
// ❌ 无限循环:setState → 重新渲染 → effect → setState
useEffect(() => {
setCount(count + 1);
}, [count]);
// ✅ 条件更新
useEffect(() => {
if (count < 10) setCount(count + 1);
}, [count]);
// ✅ 或用 useRef 标记
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) {
setCount(prev => prev + 1);
} else {
didMount.current = true;
}
}, [trigger]);
3.4 useEffect 的闭包陷阱
function StaleClosure() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
console.log(count); // ❌ 永远输出 0(挂载时的值)
}, 3000);
return () => clearTimeout(timer);
}, []); // count 不在依赖数组中
// ✅ 正确:把 count 加入依赖
useEffect(() => {
const timer = setTimeout(() => {
console.log(count);
}, 3000);
return () => clearTimeout(timer);
}, [count]);
}
规则:React Hooks ESLint 插件的
exhaustive-deps规则会自动检测缺失的依赖,建议开启。
四、useContext:跨层级数据共享
// 创建上下文
const ThemeContext = createContext('light');
// Provider 包裹子树
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
// 消费上下文
function Toolbar() {
const theme = useContext(ThemeContext);
return <div className={theme}>Toolbar</div>;
}
Context 性能陷阱
// ❌ 问题:value 每次渲染都是新对象,导致所有消费者重新渲染
function App() {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}> {/* 新对象 */}
<DeepTree />
</UserContext.Provider>
);
}
// ✅ 方案 1:useMemo 缓存
const value = useMemo(() => ({ user, setUser }), [user]);
return <UserContext.Provider value={value}>...</UserContext.Provider>;
// ✅ 方案 2:拆分多个 Context
const UserStateContext = createContext(null);
const UserDispatchContext = createContext(null);
// 状态变化和 dispatch 分离,减少不必要的重渲染
五、useRef:DOM 引用与持久化值
5.1 DOM 引用
function InputFocus() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Focus</button>
</>
);
}
5.2 保存上一次的值(不触发渲染)
function Counter() {
const [count, setCount] = useState(0);
const prevCount = useRef(count);
useEffect(() => {
prevCount.current = count; // 更新 ref,不触发渲染
});
return (
<div>
Now: {count}, Before: {prevCount.current}
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
5.3 保存定时器/订阅 ID
function Timer() {
const timerId = useRef(null);
const start = () => {
timerId.current = setInterval(() => console.log('tick'), 1000);
};
const stop = () => {
clearInterval(timerId.current);
};
return <><button onClick={start}>Start</button><button onClick={stop}>Stop</button></>;
}
六、useMemo & useCallback:性能优化
6.1 useMemo:缓存计算结果
// ❌ 每次渲染都排序(大数据量时卡顿)
const sorted = items.sort((a, b) => b.score - a.score);
// ✅ 只在 items 变化时排序
const sorted = useMemo(
() => items.sort((a, b) => b.score - a.score),
[items]
);
6.2 useCallback:缓存函数引用
// ❌ 每次渲染都是新函数,子组件 memo 失效
function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => console.log(count); // 新引用
return <Child onClick={handleClick} />;
}
const Child = memo(function Child({ onClick }) {
return <button onClick={onClick}>Click</button>;
});
// ✅ 缓存函数引用
function ParentFixed() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => console.log(count), [count]);
return <Child onClick={handleClick} />;
}
6.3 什么时候不需要 useMemo/useCallback
// ❌ 过度使用:简单计算不需要缓存
const double = useMemo(() => count * 2, [count]); // 乘法成本极低
// ✅ 直接使用
const double = count * 2;
// ❌ 不要给所有函数加 useCallback
const handleClick = useCallback(() => {}, []); // 空函数的缓存没意义
缓存的适用场景:
- 计算成本高的派生数据(排序、筛选、图表数据)
- 传递给
memo子组件的函数和对象 - 作为其他 Hooks 的依赖
七、useReducer:复杂状态逻辑
const initialState = { count: 0, step: 1 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step };
case 'decrement':
return { ...state, count: state.count - state.step };
case 'setStep':
return { ...state, step: action.payload };
default:
throw new Error('Unknown action');
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
Step: <input
value={state.step}
onChange={e => dispatch({ type: 'setStep', payload: Number(e.target.value) })}
/>
</>
);
}
useReducer 适用场景:
- 多个相关状态需要一起更新
- 状态逻辑复杂(条件分支多)
- 想复用状态逻辑到其他组件(抽为自定义 Hook)
八、自定义 Hooks:逻辑复用
8.1 设计原则
自定义 Hook 是命名以 use 开头的函数,可以调用其他 Hooks:
// useLocalStorage.ts
export function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback((value) => {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
}, [key]);
return [storedValue, setValue];
}
// 使用
function Demo() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>{theme}</button>;
}
8.2 常用自定义 Hook 大全
// useDebounce — 防抖
function useDebounce(value, delay) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// useFetch — 数据获取
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
fetch(url)
.then(r => r.json())
.then(data => { if (!cancelled) setData(data); })
.catch(err => { if (!cancelled) setError(err); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
// useClickOutside — 点击外部关闭
defunction useClickOutside(ref, handler) {
useEffect(() => {
const listener = (e) => {
if (!ref.current || ref.current.contains(e.target)) return;
handler();
};
document.addEventListener('mousedown', listener);
return () => document.removeEventListener('mousedown', listener);
}, [ref, handler]);
}
// useMediaQuery — 响应式
function useMediaQuery(query) {
const [matches, setMatches] = useState(false);
useEffect(() => {
const mql = window.matchMedia(query);
setMatches(mql.matches);
mql.addEventListener('change', (e) => setMatches(e.matches));
return () => mql.removeEventListener('change', setMatches);
}, [query]);
return matches;
}
九、Hooks 性能陷阱与修复
陷阱 1:状态批量更新
// React 18 自动批处理
function BatchDemo() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React 18 只重渲染一次
}
}
陷阱 2:Context 引起的全局重渲染
修复:使用状态拆分 + use-context-selector
陷阱 3:useEffect 依赖数组遗漏
修复:开启 ESLint exhaustive-deps 规则,认真处理每个警告。
常见问题(FAQ)
自定义 Hook 和工具函数有什么区别?
自定义 Hook 内部可以调用 React Hooks(useState/useEffect 等),因此能参与 React 的渲染周期。普通工具函数不能调用 Hooks。
为什么 useEffect 中没有 async 函数?
useEffect 的回调不能直接是 async(因为它需要返回清理函数,Promise 会干扰)。但可以在内部定义 async 函数:
useEffect(() => {
async function fetchData() { /* ... */ }
fetchData();
}, []);
useMemo 和 useCallback 的区别?
useMemo:缓存值useCallback:缓存函数(本质是useMemo(() => fn, deps))
useLayoutEffect 和 useEffect 区别?
useEffect:浏览器绘制后异步执行(不阻塞渲染)useLayoutEffect:浏览器绘制前同步执行(用于测量 DOM 布局)
99% 场景用 useEffect,只有需要测量 DOM 尺寸并同步调整时才用 useLayoutEffect。
相关阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。