Hooks 是 React 16.8 引入的特性,它允许你在函数组件中使用状态和其他 React 特性,彻底告别类组件。基础 Hooks 是所有 React 开发者必须熟练掌握的核心工具。
useState:让函数组件拥有状态
useState 是最核心的 Hook,它让函数组件能够声明和更新内部状态。每次状态改变,组件都会重新渲染。
基本用法:
const [state, setState] = useState(initialValue);
state:当前的状态值setState:更新状态的函数initialValue:状态的初始值(可以是任意类型,也可以传入一个函数进行惰性初始化)
实际示例:
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
return (
<div>
<p>点击次数:{count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
<button onClick={() => setCount(0)}>重置</button>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="请输入姓名"
/>
</div>
);
}
关键注意事项:
- 状态是不可变的:更新状态时必须传入新值,React 用
Object.is比较新旧值判断是否重渲染。如果状态是对象或数组,请始终创建新对象/数组,而不能直接修改原值:
// ❌ 错误:直接修改原数组
arr.push(newItem);
setArr(arr);
// ✅ 正确:创建新数组
setArr([...arr, newItem]);
- 函数式更新:当新状态依赖旧状态时,建议使用函数式更新,避免闭包陷阱:
setCount(prevCount => prevCount + 1);
这在连续调用或异步场景中尤其重要。
- 惰性初始化:如果初始状态需要复杂计算,传递一个函数,React 只会在首次渲染时执行它:
const [data, setData] = useState(() => {
const saved = localStorage.getItem('data');
return saved ? JSON.parse(saved) : defaultData;
});
- 状态更新是异步的:在同一个事件处理函数中多次调用
setState,React 会批量处理,你无法立即拿到最新值。如果需要在更新后立即执行操作,请使用useEffect监听该状态。
useEffect:处理副作用
副作用是指组件渲染之外的操作,例如数据请求、订阅事件、操作 DOM、设置定时器等。useEffect 让你在函数组件中统一管理这些副作用。
基本用法:
useEffect(() => {
// 副作用逻辑
return () => {
// 清理函数(可选)
};
}, [依赖项数组]);
三种典型使用场景:
- 不传递依赖项:每次渲染后都执行(几乎不用,容易死循环)
- 传递空数组
[]:只在组件挂载时执行一次,类似componentDidMount - 传递具体依赖:当依赖项变化时重新执行
实际示例:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false; // 防止竞态
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) {
setUser(data);
setLoading(false);
}
});
return () => {
cancelled = true; // 组件卸载或依赖变化时取消
};
}, [userId]); // userId 变化时重新请求
if (loading) return <p>加载中...</p>;
return <div>{user.name}</div>;
}
清理函数的重要性:
如果副作用创建了需要手动清除的资源(如定时器、订阅、事件监听),必须在清理函数中处理,否则会导致内存泄漏:
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);
return () => clearInterval(timer); // 组件卸载时清除定时器
}, []);
常见的依赖项陷阱:
- 如果你在
useEffect中使用了组件内的变量或函数,而没有将它列入依赖数组,React 会报警告,并且可能访问到过期的闭包变量。 - 不要为了消除警告而随意添加依赖,要确保逻辑正确。如果某个函数只在
useEffect中使用且不变,可以将其定义在useEffect内部。
useRef:跨渲染周期的持久引用
useRef 返回一个可变的 ref 对象,其 .current 属性在组件的整个生命周期内保持不变。它有两个核心用途:访问 DOM 元素和存储不触发重渲染的变量。
1. 获取 DOM 节点引用:
function FocusInput() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus(); // 直接操作 DOM
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>聚焦输入框</button>
</div>
);
}
2. 存储可变值(不触发重渲染):
当你需要在组件多次渲染之间保存一个值,但它的变化不需要导致界面更新时,使用 useRef 而不是 useState:
function Timer() {
const timerIdRef = useRef(null);
const [seconds, setSeconds] = useState(0);
const start = () => {
if (timerIdRef.current) return; // 防止重复启动
timerIdRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
};
const stop = () => {
clearInterval(timerIdRef.current);
timerIdRef.current = null;
};
// 组件卸载时清理
useEffect(() => {
return () => clearInterval(timerIdRef.current);
}, []);
return (
<div>
<p>{seconds} 秒</p>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
</div>
);
}
这里 timerIdRef 用于保存定时器 ID,它不会导致组件重渲染,完美避开了 useState 的“值变化就渲染”的特性。
与 useState 的区别:
useState的值变化会触发重渲染。useRef的.current变化不会触发重渲染。useRef的值在渲染之间持久存在,而普通变量在每次渲染时都重新创建。
useContext:穿越组件树的共享数据
useContext 让你在组件树中直接读取 Context 的值,无需通过 Props 逐层传递。它解决了“Props 钻探”问题。
使用步骤:
- 创建 Context:
const ThemeContext = createContext('light');
- 在组件树上层提供值:
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
- 在任意子组件中消费:
function Toolbar() {
return <ThemedButton />;
}
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
style={{ background: theme === 'dark' ? '#333' : '#eee' }}
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
切换主题
</button>
);
}
ThemedButton 不需要从父组件接收 Props,直接通过 Context 获取主题和切换函数,中间组件 Toolbar 无需关心这些数据。
性能注意事项:
Context 的 Provider 值变化时,所有使用该 Context 的组件都会重新渲染。如果 Context 值是一个对象/数组,每次父组件重渲染都会创建新对象,导致所有消费者不必要地渲染。解决方案:
- 将 Context 拆分得更细,让不同职责的数据分开传递。
- 使用
useMemo缓存 Context 的 value:
const value = useMemo(() => ({ theme, setTheme }), [theme]);
- 或者使用轻量级状态管理库(如 Zustand)来替代大规模 Context。
适用场景:
- 全局主题、语言、认证用户信息等很少改变的数据
- 组件库中的配置信息
- 浅层组件树中的共享数据
不建议将所有状态都丢进 Context,过度使用会降低组件复用性和性能。遵循“就近原则”,能通过 Props 解决的就不要滥用 Context。
这四种基础 Hook 是构建 React 应用的基石。下面章节会进一步介绍性能优化 Hooks 和进阶 Hooks,帮助你应对更复杂的场景。