跳转到内容

React Hooks 基础

建议学习 60-80 分钟更新于 2026/5/20

Hooks 是学习 React 函数组件绕不开的一部分。

如果说组件负责描述 UI,那么 Hooks 负责让函数组件拥有状态、副作用、引用、上下文和可复用逻辑。

这篇笔记按入门顺序整理:

  • Hooks 的概念。
  • useState 管理状态。
  • useEffect 处理副作用。
  • 自定义 Hook 复用逻辑。
  • useRef 获取 DOM 或保存可变值。
  • useContext 跨层级读取共享数据。

Hooks 本质上是一组特殊函数。

它们让函数组件可以使用 React 的能力,比如状态、生命周期相关逻辑、上下文、引用等。

在 React 体系里,组件主要有两种写法:

  • 类组件。
  • 函数组件。

早期函数组件通常只负责接收 props 并渲染 UI,没有自己的状态。后来 React 从 16.8 开始引入 Hooks,让函数组件也可以拥有状态和副作用处理能力。

可以这样理解:

Hooks 是让函数组件更强大、更灵活的一套“钩子”。

React 的设计理念可以简化成:

UI = f(data)

也就是说,UI 是数据计算后的结果。函数组件天然更贴近这个表达方式,而 Hooks 让函数组件不再只是“无状态组件”。

类组件没有被移除。

为了兼容已有项目,类组件仍然可以使用。但在新项目里,更常见的写法是函数组件加 Hooks。

需要注意:

  • 有了 Hooks 后,不能再简单说函数组件是无状态组件。
  • Hooks 只能在函数组件或自定义 Hook 中使用。
  • 类组件不能直接使用 Hooks。

Hooks 主要解决了两个问题:

  • 组件状态逻辑复用。
  • 类组件自身复杂度较高。

在 Hooks 出现之前,React 曾经通过一些模式复用逻辑:

  • mixins。
  • HOC 高阶组件。
  • render props。

这些方案能解决一部分问题,但也有各自的成本。

比如:

  • mixins 容易让数据来源不清晰。
  • HOC 容易形成组件嵌套。
  • render props 也可能让 JSX 结构变复杂。

Hooks 让逻辑复用更直接:把状态和副作用逻辑抽成自定义 Hook 即可。

类组件功能完整,但也有学习成本:

  • 要理解多个生命周期。
  • 要处理 this 指向。
  • 逻辑容易分散在不同生命周期方法里。
  • 同一业务逻辑可能被拆散。

Hooks 让函数组件可以用更轻的方式组织逻辑。

Hooks 有两条非常重要的规则:

  1. 只能在函数组件或自定义 Hook 中调用。
  2. 只能在顶层调用,不能放在 iffor、普通函数、事件函数或 try/catch 中。

正确写法:

good-hooks.jsx
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

错误写法:

bad-hooks.jsx
import { useState } from 'react';
function Counter({ visible }) {
if (visible) {
const [count, setCount] = useState(0);
}
return null;
}

为什么不能这样写?

因为 React 依赖 Hooks 的调用顺序来对应每一个状态。如果某次渲染调用了两个 Hook,下次渲染只调用一个 Hook,顺序就乱了。

useState 用来给函数组件添加状态。

基本步骤:

  1. 从 React 中导入 useState
  2. 调用 useState(initialState)
  3. 从返回数组中拿到状态值和更新函数。
  4. 在 JSX 中使用状态。
  5. 调用更新函数修改状态并触发重新渲染。

示例:

counter.jsx
import { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
export default App;

useState 返回一个数组:

const [count, setCount] = useState(0);

其中:

  • count 是当前状态。
  • setCount 是修改状态的方法。
  • 0 是初始状态。

useState 提供的状态就是函数组件内部的局部状态,可以在组件函数中读取。

修改状态时,一定要使用 setState 函数。

read-update-state.jsx
import { useState } from 'react';
function App() {
const [name, setName] = useState('xiaoxi');
return (
<div>
<p>{name}</p>
<button onClick={() => setName('veyliss')}>修改名称</button>
</div>
);
}

注意:不要直接修改状态。

错误示例:

user.name = 'veyliss';
setUser(user);

推荐写法:

setUser({
...user,
name: 'veyliss',
});

尤其是对象、数组这类引用类型,更新时应该创建新值,而不是直接改旧值。

函数组件使用 useState 后,大致执行过程是这样的。

第一次渲染:

  1. 从头执行组件函数。
  2. 执行 useState(0)
  3. React 使用 0 作为初始状态。
  4. 渲染页面,此时 count0

第二次渲染:

  1. 点击按钮,调用 setCount(count + 1)
  2. 状态变化,组件重新渲染。
  3. 组件函数再次从头执行。
  4. 再次执行 useState(0)
  5. React 不再使用初始值 0,而是取内部保存的最新状态。
  6. 渲染页面,此时 count1

示例:

rerender.jsx
import { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
console.log('render:', count);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
export default App;

重点:

useState 的初始值只在组件第一次渲染时生效。

后续重新渲染时,React 会读取最新状态,而不是重新使用初始值。

一个组件中可以多次调用 useState

每一次调用都提供一个独立状态。

multiple-state.jsx
import { useState } from 'react';
function Profile() {
const [name, setName] = useState('xiaoxi');
const [age, setAge] = useState(18);
const [tags, setTags] = useState([]);
return (
<div>
<p>{name}</p>
<p>{age}</p>
<button onClick={() => setTags([...tags, 'React'])}>添加标签</button>
</div>
);
}

多个状态适合表达相对独立的数据。

如果多个状态总是一起变化,也可以考虑合并成一个对象。

如果新状态依赖旧状态,推荐使用函数式更新。

functional-update.jsx
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount((prev) => prev + 1);
}
return <button onClick={handleClick}>{count}</button>;
}

函数式更新的参数 prev 表示上一次状态。

在连续更新时,它比直接使用 count + 1 更稳。

multiple-update.jsx
function handleClick() {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
}

这样点击一次会加 3

如果初始状态需要复杂计算,可以给 useState 传入一个函数。

lazy-initial-state.jsx
import { useState } from 'react';
function Counter({ initialCount }) {
const [count, setCount] = useState(() => {
return initialCount;
});
return (
<button onClick={() => setCount((prev) => prev + 1)}>
{count}
</button>
);
}

这个函数只会在初始化时使用。

适合场景:

  • 初始数据需要计算。
  • 初始数据来自 localStorage
  • 初始数据创建成本较高。

普通数据直接写:

const [count, setCount] = useState(0);

复杂初始值用函数:

const [value, setValue] = useState(() => expensiveCreateValue());

useEffect 用来处理副作用。

对 React 组件来说,主作用是:

根据 state 和 props 渲染 UI

除此之外,很多操作都可以理解为副作用。

常见副作用:

  • 请求接口。
  • 修改页面标题。
  • 操作 DOM。
  • 绑定事件。
  • 启动定时器。
  • 读写 localStorage
  • 订阅 WebSocket。

useEffect 的作用就是让函数组件处理这些副作用。

示例:点击按钮后同步修改页面标题。

effect-title.jsx
import { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `当前已点击 ${count}`;
});
return (
<button onClick={() => setCount((prev) => prev + 1)}>
{count}
</button>
);
}
export default App;

没有依赖数组时,useEffect 会在每次渲染后执行。

useEffect 的第二个参数是依赖数组,用来控制副作用执行时机。

useEffect(() => {
console.log('每次渲染后都会执行');
});

执行时机:

  • 首次渲染后执行。
  • 每次组件更新后执行。
useEffect(() => {
console.log('只在首次渲染后执行');
}, []);

执行时机:

  • 首次渲染后执行一次。

常用于:

  • 初始化请求。
  • 绑定全局事件。
  • 创建一次订阅。
effect-deps.jsx
import { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState('xiaoxi');
useEffect(() => {
console.log('count 变化了:', count);
}, [count]);
return (
<>
<button onClick={() => setCount((prev) => prev + 1)}>{count}</button>
<button onClick={() => setName('veyliss')}>{name}</button>
</>
);
}

执行时机:

  • 首次渲染后执行。
  • count 变化后执行。
  • name 变化不会触发这个 Effect。

注意:

Effect 中用到的响应式数据,通常都应该写进依赖数组。

响应式数据包括:

  • props
  • state
  • 组件函数内部声明的变量和函数。

如果副作用创建了定时器、事件监听、订阅连接,就需要清理。

清理函数写在 useEffect 回调的返回值中。

effect-cleanup.jsx
import { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
const timerId = window.setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
return () => {
window.clearInterval(timerId);
};
}, []);
return <div>{count}</div>;
}
export default App;

清理函数的执行时机:

  • 组件卸载时执行。
  • 依赖变化导致下一次 Effect 执行前,会先执行上一次的清理函数。

这点很重要。

比如你监听了 window.scroll,组件卸载时不移除,就可能造成内存泄漏或重复监听。

不要直接把 useEffect 的回调函数写成 async

不推荐:

useEffect(async () => {
const res = await fetch('/api/list');
}, []);

原因是 async 函数会返回 Promise,而 useEffect 期望返回 undefined 或清理函数。

推荐写法:在 Effect 内部定义异步函数。

effect-fetch.jsx
import { useEffect, useState } from 'react';
function ChannelList() {
const [channels, setChannels] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
async function fetchChannels() {
try {
const response = await fetch('http://geek.itheima.net/v1_0/channels', {
signal: controller.signal,
});
const data = await response.json();
setChannels(data.data?.channels ?? []);
} finally {
setLoading(false);
}
}
fetchChannels();
return () => {
controller.abort();
};
}, []);
if (loading) return <p>加载中...</p>;
return (
<ul>
{channels.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}

这里使用 AbortController 是为了组件卸载时取消还没完成的请求。

自定义 Hook 是把可复用的状态逻辑抽成函数。

命名规则:

use + 具体功能

比如:

  • useWindowScroll
  • useLocalStorage
  • useOnlineStatus

自定义 Hook 里面可以继续调用 React Hooks。

需求:

const y = useWindowScroll();

实现:

useWindowScroll.js
import { useEffect, useState } from 'react';
export function useWindowScroll() {
const [y, setY] = useState(() => window.scrollY);
useEffect(() => {
function handleScroll() {
setY(window.scrollY);
}
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return y;
}

使用:

App.jsx
import { useWindowScroll } from './useWindowScroll';
function App() {
const y = useWindowScroll();
return <div>当前滚动距离:{y}</div>;
}

这里要注意:

  • 事件监听只需要绑定一次,所以依赖数组是 []
  • 组件卸载时要移除事件监听。
  • return yreturn [y] 更简单;如果希望和 useState 风格一致,也可以返回数组。

需求:

const [message, setMessage] = useLocalStorage('message', 'hello');

实现:

useLocalStorage.js
import { useEffect, useState } from 'react';
export function useLocalStorage(key, defaultValue) {
const [value, setValue] = useState(() => {
const localValue = window.localStorage.getItem(key);
return localValue ?? defaultValue;
});
useEffect(() => {
window.localStorage.setItem(key, value);
}, [key, value]);
return [value, setValue];
}

使用:

App.jsx
import { useLocalStorage } from './useLocalStorage';
function App() {
const [message, setMessage] = useLocalStorage('message', 'hello');
return (
<input
value={message}
onChange={(event) => setMessage(event.target.value)}
/>
);
}

这里使用 useState 初始化函数读取 localStorage,可以避免每次渲染都读取本地存储。

如果要保存对象,可以用 JSON.stringifyJSON.parse 做一层序列化。

useRef 返回一个带有 current 属性的对象。

它常见用途有两个:

  • 获取 DOM 元素。
  • 保存不需要触发重新渲染的可变值。

基本写法:

const inputRef = useRef(null);

useRefuseState 的重要区别:

对比项useStateuseRef
更新后是否重新渲染不会
适合保存会影响 UI 的数据DOM、定时器 ID、上一次值等
读取方式直接读状态变量ref.current
ref-dom.jsx
import { useEffect, useRef } from 'react';
function App() {
const h1Ref = useRef(null);
useEffect(() => {
console.log(h1Ref.current);
}, []);
return (
<div>
<h1 ref={h1Ref}>this is h1</h1>
</div>
);
}
export default App;

可以用它做聚焦:

focus-input.jsx
import { useRef } from 'react';
function App() {
const inputRef = useRef(null);
return (
<div>
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>聚焦</button>
</div>
);
}

比如保存定时器 ID:

ref-timer.jsx
import { useRef } from 'react';
function Timer() {
const timerRef = useRef(null);
function start() {
timerRef.current = window.setInterval(() => {
console.log('running');
}, 1000);
}
function stop() {
window.clearInterval(timerRef.current);
}
return (
<>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
</>
);
}

修改 ref.current 不会触发组件重新渲染。

所以如果数据要展示到页面上,应该用 useState;如果只是保存某个过程值,可以用 useRef

类组件有实例,所以可以通过 ref 获取类组件实例。

Foo.jsx
import React from 'react';
class Foo extends React.Component {
sayHi = () => {
console.log('say hi');
};
render() {
return <div>Foo</div>;
}
}
export default Foo;
App.jsx
import { useEffect, useRef } from 'react';
import Foo from './Foo';
function App() {
const fooRef = useRef(null);
useEffect(() => {
fooRef.current?.sayHi();
}, []);
return <Foo ref={fooRef} />;
}
export default App;

函数组件没有类实例。

如果父组件需要调用子函数组件暴露的方法,通常使用 forwardRefuseImperativeHandle。不过这种命令式调用要谨慎使用,普通数据传递还是优先用 props 和状态。

useContext 用于读取 Context 中的数据。

它适合跨层级传递稳定的共享数据,比如:

  • 当前主题。
  • 当前语言。
  • 登录用户信息。
  • 全局配置。

普通父子通信优先用 props,不要一上来就用 Context。

步骤:

  1. 使用 createContext 创建 Context 对象。
  2. 在上层组件通过 Provider 提供数据。
  3. 在下层组件通过 useContext 读取数据。

示例:

context.jsx
import { createContext, useContext } from 'react';
const UserContext = createContext('游客');
function Foo() {
return (
<div>
Foo
<Bar />
</div>
);
}
function Bar() {
const name = useContext(UserContext);
return <div>Bar: {name}</div>;
}
function App() {
return (
<UserContext.Provider value="xiaoxi">
<Foo />
</UserContext.Provider>
);
}
export default App;

useContext(UserContext) 会向上查找最近的 Provider。

如果找不到 Provider,就使用 createContext(defaultValue) 中的默认值。

Context 很方便,但不要滥用。

适合放 Context 的数据:

  • 多层组件都需要。
  • 数据语义稳定。
  • 不想层层透传 props。

不适合放 Context 的数据:

  • 只在父子组件之间使用的数据。
  • 变化非常频繁的大对象。
  • 完全可以由局部状态管理的数据。

当 Provider 的 value 变化时,读取这个 Context 的组件会重新渲染。

如果传对象或函数,要注意引用变化带来的重新渲染。

原笔记里提到的练习仓库:

Terminal window
git clone https://gitee.com/react-course-series/react-tomvc-hook.git

常见启动步骤:

Terminal window
yarn
yarn mock-serve
yarn start

这个案例可以用来练习:

  • useState 管理列表和输入框状态。
  • useEffect 初始化请求列表。
  • 自定义 Hook 拆分复用逻辑。
  • 删除、搜索、过滤等常见交互。

接口大致包括:

功能接口方法
获取列表http://localhost:3001/dataGET
删除http://localhost:3001/data/:idDELETE
搜索http://localhost:3001/data/?name=keywordGET

做这个练习时,建议先完成主流程:

  1. 页面加载时请求列表。
  2. 输入关键词搜索。
  3. 点击删除更新列表。
  4. 抽出请求函数。
  5. 再考虑自定义 Hook。

为什么 setState 后立刻打印还是旧值

Section titled “为什么 setState 后立刻打印还是旧值”

状态更新会触发下一次渲染。

在当前事件函数里,读取到的仍然是本次渲染中的状态值。

setCount(count + 1);
console.log(count); // 仍然是当前渲染里的旧值

如果新状态依赖旧状态,用函数式更新:

setCount((prev) => prev + 1);

常见原因:

  • 没有写依赖数组。
  • 依赖项每次渲染都是新对象或新函数。
  • Effect 内部更新了某个依赖,导致循环。

解决思路:

  • 明确这个 Effect 是否真的需要。
  • 补齐依赖项。
  • 把不必要的对象或函数移到 Effect 内部。
  • 必要时使用 useMemouseCallback 稳定引用。

在 React Strict Mode 下,开发环境可能会额外执行一次 setup + cleanup,用来帮助发现副作用清理不完整的问题。

生产环境不会这样重复执行。

如果重复执行导致问题,通常说明清理逻辑不完整。

Hooks 入门可以先抓住这几句话:

  • useState 让函数组件拥有状态。
  • useEffect 用来同步外部系统和处理副作用。
  • 依赖数组决定 Effect 什么时候重新执行。
  • 清理函数用于取消订阅、移除事件、清除定时器。
  • 自定义 Hook 用来复用状态逻辑。
  • useRef 保存 DOM 或不触发渲染的可变值。
  • useContext 用来读取跨层级共享数据。
  • Hooks 必须在函数组件或自定义 Hook 顶层调用。

学 Hooks 不要只背 API。更重要的是理解:组件每次渲染都是一次函数执行,Hooks 让 React 能在多次函数执行之间保存状态和副作用关系。