什么是 Hooks?
Hooks 是 React 16.8 版本引入的一组函数。它们的核心思想是让你在不编写 class 的情况下使用 state 以及 React 的其他特性。
在 Hooks 出现之前,如果你想给一个组件添加 state 或者使用生命周期方法(如 componentDidMount
),你必须把它从一个简单的函数组件(Function Component)重写成一个类组件(Class Component)。Hooks 的出现彻底改变了这一点,让函数组件变得和类组件一样强大,甚至更灵活。
为什么需要 Hooks?(解决了什么问题)
-
告别
this
的困扰:在类组件中,this
的指向是一个常见的问题源头,你需要频繁地使用.bind(this)
或者箭头函数来确保this
指向组件实例。Hooks 在函数组件中使用,完全没有this
的问题。 -
更好地重用状态逻辑:以前,重用状态逻辑的主要方式是高阶组件(HOCs)和渲染属性(Render Props)。这两种模式虽然强大,但通常会导致组件树层级很深(所谓的 “Wrapper Hell”),使得代码难以追踪和理解。自定义 Hooks(Custom Hooks)可以让你将组件逻辑提取到可重用的函数中,代码更扁平、更清晰。
-
将相关逻辑聚合在一起:在类组件中,一个功能相关的逻辑往往被拆分到不同的生命周期方法中。例如,一个订阅功能的代码,可能需要在
componentDidMount
中设置订阅,在componentWillUnmount
中取消订阅,而更新订阅的逻辑又在componentDidUpdate
中。useEffect
Hook 让你能将这些相关的逻辑放在同一个“副作用”函数里,代码组织更合理。
Hooks 的两大黄金法则(必须遵守)
在使用 Hooks 之前,必须了解并遵守这两条规则,否则你的代码会出错。
-
只在顶层调用 Hooks:不要在循环、条件判断或嵌套函数中调用 Hook。必须保证 Hooks 在每次组件渲染时的调用顺序都是完全相同的。React 正是依赖这个调用顺序来正确地将 state 与对应的
useState
调用关联起来。 -
只在 React 函数中调用 Hooks:
- 在 React 函数组件中调用。
- 在自定义 Hook 中调用。
核心 Hooks 详解
下面我们来详细讲解最常用、最重要的几个 Hooks。
1. useState
:为组件添加状态
这是最基础也是最常用的 Hook。它允许函数组件拥有自己的 state。
- 作用:声明一个状态变量。
- 语法:
const [state, setState] = useState(initialState);
- 参数:
initialState
- 状态的初始值。这个初始值只在组件的第一次渲染时生效。 - 返回值:一个包含两个元素的数组:
state
:当前的状态值。setState
:一个用于更新该状态的函数。
示例:一个简单的计数器
import React, { useState } from 'react';
function Counter() {
// 声明一个名为 count 的 state 变量,初始值为 0
const [count, setCount] = useState(0);
// setState 可以接收一个新值,也可以接收一个函数
// 使用函数形式是更安全的选择,特别是当新状态依赖于旧状态时
const handleIncrement = () => {
setCount(prevCount => prevCount + 1);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me (Direct Value)</button>
<button onClick={handleIncrement}>Click me (Functional Update)</button>
</div>
);
}
2. useEffect
:处理副作用
副作用(Side Effects)是指在组件渲染过程中,与外部世界发生的交互。例如:数据获取、设置订阅、手动更改 DOM 等。useEffect
就是用来处理这些副作用的。
它可以看作是类组件中 componentDidMount
、componentDidUpdate
和 componentWillUnmount
这三个生命周期方法的组合。
- 作用:在函数组件中执行副作用操作。
- 语法:
useEffect(setup, dependencies?);
- 参数:
setup
:一个函数,包含了你的副作用逻辑。这个函数可以选择性地返回一个清理函数。dependencies
(可选):一个数组,包含了该 effect 所依赖的值(通常是 props 或 state)。
依赖数组 dependencies
的行为是 useEffect
的关键:
-
不提供依赖数组:
useEffect(() => { ... })
- 副作用函数在每次渲染后都会执行。这很容易导致性能问题或死循环,请谨慎使用。
-
提供一个空数组
[]
:useEffect(() => { ... }, [])
- 副作用函数仅在组件第一次渲染后执行一次。这等同于
componentDidMount
。
- 副作用函数仅在组件第一次渲染后执行一次。这等同于
-
提供包含值的数组
[dep1, dep2]
:useEffect(() => { ... }, [propA, stateB])
- 副作用函数在第一次渲染后执行,并且在依赖数组中的任何一个值发生变化后的下一次渲染时再次执行。这等同于
componentDidUpdate
中对特定值的检查。
- 副作用函数在第一次渲染后执行,并且在依赖数组中的任何一个值发生变化后的下一次渲染时再次执行。这等同于
清理函数 (Cleanup Function):
如果 useEffect
的第一个函数参数返回了另一个函数,React 会在组件卸载时以及下一次 effect 执行之前运行这个返回的函数,用于清理资源(如清除定时器、取消订阅等)。这等同于 componentWillUnmount
。
示例:从 API 获取数据
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
console.log('Effect is running for userId:', userId);
setLoading(true);
const fetchUser = async () => {
const response = await fetch(`https://api.example.com/users/${userId}`);
const userData = await response.json();
setUser(userData);
setLoading(false);
};
fetchUser();
// 清理函数(在这个例子中不是必须的,但作为演示)
return () => {
console.log('Cleaning up previous effect for userId:', userId);
// 可以在这里中止 fetch 请求等
};
}, [userId]); // 依赖数组!当 userId 变化时,重新获取数据
if (loading) return <p>Loading...</p>;
if (!user) return <p>User not found.</p>;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
3. useContext
:跨组件共享状态
useContext
让你能够订阅 React 的 context,从而避免了深层次的 “prop drilling”(属性逐层传递)。
- 作用:读取和订阅 context 的值。
- 语法:
const value = useContext(MyContext);
- 流程:
- 使用
React.createContext()
创建一个 Context 对象。 - 在组件树的上层,使用
<MyContext.Provider value={...}>
来提供 context 的值。 - 在任何下层组件中,使用
useContext(MyContext)
来获取这个值。
- 使用
示例:主题切换
// 1. 创建 Context
const ThemeContext = React.createContext('light');
// 2. App 组件使用 Provider 提供值
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
// 3. Toolbar 组件作为中间层,无需关心 theme
function Toolbar() {
return <ThemedButton />;
}
// 4. ThemedButton 组件使用 useContext 消费值
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext);
const toggleTheme = () => {
setTheme(currentTheme => (currentTheme === 'light' ? 'dark' : 'light'));
};
return (
<button
style={{ background: theme === 'dark' ? '#333' : '#FFF', color: theme === 'dark' ? '#FFF' : '#333' }}
onClick={toggleTheme}
>
I am a {theme} button
</button>
);
}
其他重要的 Hooks
4. useReducer
useState
的一个更强大的替代品。适用于管理包含多个子值的复杂 state 对象,或者下一个 state 依赖于前一个 state 的情况。它的工作方式与 Redux 的 reducer 非常相似。
const [state, dispatch] = useReducer(reducer, initialState);
5. useCallback
用于记忆化(memoize)一个函数。它返回一个函数的记忆化版本,该函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的子组件时,它非常有用,可以防止不必要的重新渲染。
const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);
6. useMemo
用于记忆化一个值。它只在某个依赖项改变时才重新计算记忆化的值。这个 Hook 用于优化开销大的计算。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useCallback
vs useMemo
:
useCallback(fn, deps)
等价于useMemo(() => fn, deps)
。useCallback
记忆函数本身,useMemo
记忆函数的返回值。
7. useRef
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数。它有两个主要用途:
- 访问 DOM 节点:你可以将 ref 对象附加到 JSX 元素的
ref
属性上,从而直接操作 DOM。 - 存储一个可变值:它像一个“盒子”,你可以在其中存放任何可变值。与
useState
不同,更新ref.current
的值不会触发组件的重新渲染。
示例:聚焦输入框
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的 text input 元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
自定义 Hooks (Custom Hooks)
这是 Hooks 最强大的特性之一。你可以将组件的逻辑(如 useState
和 useEffect
的组合)提取到一个可重用的函数中。
规则:自定义 Hook 是一个名称以 use
开头的 JavaScript 函数,它内部可以调用其他 Hook。
示例:创建一个 useWindowWidth
Hook
import { useState, useEffect } from 'react';
// 自定义 Hook
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
// 设置事件监听
window.addEventListener('resize', handleResize);
// 清理函数:在组件卸载时移除监听
return () => window.removeEventListener('resize', handleResize);
}, []); // 空依赖数组,只在 mount 和 unmount 时运行
return width;
}
// 在组件中使用自定义 Hook
function MyComponent() {
const width = useWindowWidth(); // 非常简洁!
return <p>Window width is: {width}px</p>;
}
function AnotherComponent() {
const width = useWindowWidth();
return <div>{width > 768 ? 'Desktop view' : 'Mobile view'}</div>;
}
在这个例子中,我们把所有关于监听窗口宽度的逻辑(state、effect、cleanup)都封装在了 useWindowWidth
这个 Hook 中,任何组件都可以通过一行代码来复用它。
总结
Hook | 主要用途 |
---|---|
useState | 在函数组件中添加和管理 state。 |
useEffect | 处理副作用,如数据获取、订阅、DOM 操作。 |
useContext | 订阅并读取 Context,实现跨组件状态共享。 |
useReducer | useState 的替代方案,用于更复杂的 state 逻辑。 |
useCallback | 记忆化一个回调函数,用于性能优化。 |
useMemo | 记忆化一个计算值,用于性能优化。 |
useRef | 获取 DOM 元素的引用,或存储一个不触发渲染的可变值。 |
自定义 Hook | 提取和重用有状态的逻辑。 |
Hooks 从根本上改变了我们编写和组织 React 组件的方式,使其更函数式、更简洁、更易于复用和测试。它们是现代 React 开发的基石。