React Hooks 基础
Hooks 是学习 React 函数组件绕不开的一部分。
如果说组件负责描述 UI,那么 Hooks 负责让函数组件拥有状态、副作用、引用、上下文和可复用逻辑。
这篇笔记按入门顺序整理:
- Hooks 的概念。
useState管理状态。useEffect处理副作用。- 自定义 Hook 复用逻辑。
useRef获取 DOM 或保存可变值。useContext跨层级读取共享数据。
Hooks 是什么
Section titled “Hooks 是什么”Hooks 本质上是一组特殊函数。
它们让函数组件可以使用 React 的能力,比如状态、生命周期相关逻辑、上下文、引用等。
在 React 体系里,组件主要有两种写法:
- 类组件。
- 函数组件。
早期函数组件通常只负责接收 props 并渲染 UI,没有自己的状态。后来 React 从 16.8 开始引入 Hooks,让函数组件也可以拥有状态和副作用处理能力。
可以这样理解:
Hooks 是让函数组件更强大、更灵活的一套“钩子”。
React 的设计理念可以简化成:
UI = f(data)也就是说,UI 是数据计算后的结果。函数组件天然更贴近这个表达方式,而 Hooks 让函数组件不再只是“无状态组件”。
Hooks 出现后类组件还在吗
Section titled “Hooks 出现后类组件还在吗”类组件没有被移除。
为了兼容已有项目,类组件仍然可以使用。但在新项目里,更常见的写法是函数组件加 Hooks。
需要注意:
- 有了 Hooks 后,不能再简单说函数组件是无状态组件。
- Hooks 只能在函数组件或自定义 Hook 中使用。
- 类组件不能直接使用 Hooks。
Hooks 解决了什么问题
Section titled “Hooks 解决了什么问题”Hooks 主要解决了两个问题:
- 组件状态逻辑复用。
- 类组件自身复杂度较高。
状态逻辑复用
Section titled “状态逻辑复用”在 Hooks 出现之前,React 曾经通过一些模式复用逻辑:
- mixins。
- HOC 高阶组件。
- render props。
这些方案能解决一部分问题,但也有各自的成本。
比如:
- mixins 容易让数据来源不清晰。
- HOC 容易形成组件嵌套。
- render props 也可能让 JSX 结构变复杂。
Hooks 让逻辑复用更直接:把状态和副作用逻辑抽成自定义 Hook 即可。
类组件自身的问题
Section titled “类组件自身的问题”类组件功能完整,但也有学习成本:
- 要理解多个生命周期。
- 要处理
this指向。 - 逻辑容易分散在不同生命周期方法里。
- 同一业务逻辑可能被拆散。
Hooks 让函数组件可以用更轻的方式组织逻辑。
Hooks 使用规则
Section titled “Hooks 使用规则”Hooks 有两条非常重要的规则:
- 只能在函数组件或自定义 Hook 中调用。
- 只能在顶层调用,不能放在
if、for、普通函数、事件函数或try/catch中。
正确写法:
import { useState } from 'react';
function Counter() { const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;}错误写法:
import { useState } from 'react';
function Counter({ visible }) { if (visible) { const [count, setCount] = useState(0); }
return null;}为什么不能这样写?
因为 React 依赖 Hooks 的调用顺序来对应每一个状态。如果某次渲染调用了两个 Hook,下次渲染只调用一个 Hook,顺序就乱了。
useState 基础
Section titled “useState 基础”useState 用来给函数组件添加状态。
基本步骤:
- 从 React 中导入
useState。 - 调用
useState(initialState)。 - 从返回数组中拿到状态值和更新函数。
- 在 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是初始状态。
状态读取和修改
Section titled “状态读取和修改”useState 提供的状态就是函数组件内部的局部状态,可以在组件函数中读取。
修改状态时,一定要使用 setState 函数。
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',});尤其是对象、数组这类引用类型,更新时应该创建新值,而不是直接改旧值。
组件更新过程
Section titled “组件更新过程”函数组件使用 useState 后,大致执行过程是这样的。
第一次渲染:
- 从头执行组件函数。
- 执行
useState(0)。 - React 使用
0作为初始状态。 - 渲染页面,此时
count是0。
第二次渲染:
- 点击按钮,调用
setCount(count + 1)。 - 状态变化,组件重新渲染。
- 组件函数再次从头执行。
- 再次执行
useState(0)。 - React 不再使用初始值
0,而是取内部保存的最新状态。 - 渲染页面,此时
count是1。
示例:
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
Section titled “多个 useState”一个组件中可以多次调用 useState。
每一次调用都提供一个独立状态。
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> );}多个状态适合表达相对独立的数据。
如果多个状态总是一起变化,也可以考虑合并成一个对象。
如果新状态依赖旧状态,推荐使用函数式更新。
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 更稳。
function handleClick() { setCount((prev) => prev + 1); setCount((prev) => prev + 1); setCount((prev) => prev + 1);}这样点击一次会加 3。
useState 初始化函数
Section titled “useState 初始化函数”如果初始状态需要复杂计算,可以给 useState 传入一个函数。
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 是什么
Section titled “useEffect 是什么”useEffect 用来处理副作用。
对 React 组件来说,主作用是:
根据 state 和 props 渲染 UI除此之外,很多操作都可以理解为副作用。
常见副作用:
- 请求接口。
- 修改页面标题。
- 操作 DOM。
- 绑定事件。
- 启动定时器。
- 读写
localStorage。 - 订阅 WebSocket。
useEffect 的作用就是让函数组件处理这些副作用。
useEffect 基础使用
Section titled “useEffect 基础使用”示例:点击按钮后同步修改页面标题。
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 依赖项
Section titled “useEffect 依赖项”useEffect 的第二个参数是依赖数组,用来控制副作用执行时机。
不传依赖数组
Section titled “不传依赖数组”useEffect(() => { console.log('每次渲染后都会执行');});执行时机:
- 首次渲染后执行。
- 每次组件更新后执行。
useEffect(() => { console.log('只在首次渲染后执行');}, []);执行时机:
- 首次渲染后执行一次。
常用于:
- 初始化请求。
- 绑定全局事件。
- 创建一次订阅。
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 回调的返回值中。
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 发送请求
Section titled “useEffect 发送请求”不要直接把 useEffect 的回调函数写成 async。
不推荐:
useEffect(async () => { const res = await fetch('/api/list');}, []);原因是 async 函数会返回 Promise,而 useEffect 期望返回 undefined 或清理函数。
推荐写法:在 Effect 内部定义异步函数。
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
Section titled “自定义 Hook”自定义 Hook 是把可复用的状态逻辑抽成函数。
命名规则:
use + 具体功能比如:
useWindowScrolluseLocalStorageuseOnlineStatus
自定义 Hook 里面可以继续调用 React Hooks。
自定义 Hook:获取滚动距离
Section titled “自定义 Hook:获取滚动距离”需求:
const y = useWindowScroll();实现:
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;}使用:
import { useWindowScroll } from './useWindowScroll';
function App() { const y = useWindowScroll();
return <div>当前滚动距离:{y}</div>;}这里要注意:
- 事件监听只需要绑定一次,所以依赖数组是
[]。 - 组件卸载时要移除事件监听。
return y比return [y]更简单;如果希望和useState风格一致,也可以返回数组。
自定义 Hook:同步 localStorage
Section titled “自定义 Hook:同步 localStorage”需求:
const [message, setMessage] = useLocalStorage('message', 'hello');实现:
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];}使用:
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.stringify 和 JSON.parse 做一层序列化。
useRef 是什么
Section titled “useRef 是什么”useRef 返回一个带有 current 属性的对象。
它常见用途有两个:
- 获取 DOM 元素。
- 保存不需要触发重新渲染的可变值。
基本写法:
const inputRef = useRef(null);useRef 和 useState 的重要区别:
| 对比项 | useState | useRef |
|---|---|---|
| 更新后是否重新渲染 | 会 | 不会 |
| 适合保存 | 会影响 UI 的数据 | DOM、定时器 ID、上一次值等 |
| 读取方式 | 直接读状态变量 | 读 ref.current |
useRef 获取 DOM
Section titled “useRef 获取 DOM”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;可以用它做聚焦:
import { useRef } from 'react';
function App() { const inputRef = useRef(null);
return ( <div> <input ref={inputRef} /> <button onClick={() => inputRef.current?.focus()}>聚焦</button> </div> );}useRef 保存可变值
Section titled “useRef 保存可变值”比如保存定时器 ID:
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 和组件实例
Section titled “ref 和组件实例”类组件有实例,所以可以通过 ref 获取类组件实例。
import React from 'react';
class Foo extends React.Component { sayHi = () => { console.log('say hi'); };
render() { return <div>Foo</div>; }}
export default Foo;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;函数组件没有类实例。
如果父组件需要调用子函数组件暴露的方法,通常使用 forwardRef 和 useImperativeHandle。不过这种命令式调用要谨慎使用,普通数据传递还是优先用 props 和状态。
useContext 是什么
Section titled “useContext 是什么”useContext 用于读取 Context 中的数据。
它适合跨层级传递稳定的共享数据,比如:
- 当前主题。
- 当前语言。
- 登录用户信息。
- 全局配置。
普通父子通信优先用 props,不要一上来就用 Context。
useContext 基础使用
Section titled “useContext 基础使用”步骤:
- 使用
createContext创建 Context 对象。 - 在上层组件通过 Provider 提供数据。
- 在下层组件通过
useContext读取数据。
示例:
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 使用注意点
Section titled “Context 使用注意点”Context 很方便,但不要滥用。
适合放 Context 的数据:
- 多层组件都需要。
- 数据语义稳定。
- 不想层层透传 props。
不适合放 Context 的数据:
- 只在父子组件之间使用的数据。
- 变化非常频繁的大对象。
- 完全可以由局部状态管理的数据。
当 Provider 的 value 变化时,读取这个 Context 的组件会重新渲染。
如果传对象或函数,要注意引用变化带来的重新渲染。
TodoMVC Hook 练习思路
Section titled “TodoMVC Hook 练习思路”原笔记里提到的练习仓库:
git clone https://gitee.com/react-course-series/react-tomvc-hook.git常见启动步骤:
yarnyarn mock-serveyarn start这个案例可以用来练习:
useState管理列表和输入框状态。useEffect初始化请求列表。- 自定义 Hook 拆分复用逻辑。
- 删除、搜索、过滤等常见交互。
接口大致包括:
| 功能 | 接口 | 方法 |
|---|---|---|
| 获取列表 | http://localhost:3001/data | GET |
| 删除 | http://localhost:3001/data/:id | DELETE |
| 搜索 | http://localhost:3001/data/?name=keyword | GET |
做这个练习时,建议先完成主流程:
- 页面加载时请求列表。
- 输入关键词搜索。
- 点击删除更新列表。
- 抽出请求函数。
- 再考虑自定义 Hook。
为什么 setState 后立刻打印还是旧值
Section titled “为什么 setState 后立刻打印还是旧值”状态更新会触发下一次渲染。
在当前事件函数里,读取到的仍然是本次渲染中的状态值。
setCount(count + 1);console.log(count); // 仍然是当前渲染里的旧值如果新状态依赖旧状态,用函数式更新:
setCount((prev) => prev + 1);为什么 Effect 一直执行
Section titled “为什么 Effect 一直执行”常见原因:
- 没有写依赖数组。
- 依赖项每次渲染都是新对象或新函数。
- Effect 内部更新了某个依赖,导致循环。
解决思路:
- 明确这个 Effect 是否真的需要。
- 补齐依赖项。
- 把不必要的对象或函数移到 Effect 内部。
- 必要时使用
useMemo或useCallback稳定引用。
为什么开发环境 Effect 执行两次
Section titled “为什么开发环境 Effect 执行两次”在 React Strict Mode 下,开发环境可能会额外执行一次 setup + cleanup,用来帮助发现副作用清理不完整的问题。
生产环境不会这样重复执行。
如果重复执行导致问题,通常说明清理逻辑不完整。
Hooks 入门可以先抓住这几句话:
useState让函数组件拥有状态。useEffect用来同步外部系统和处理副作用。- 依赖数组决定 Effect 什么时候重新执行。
- 清理函数用于取消订阅、移除事件、清除定时器。
- 自定义 Hook 用来复用状态逻辑。
useRef保存 DOM 或不触发渲染的可变值。useContext用来读取跨层级共享数据。- Hooks 必须在函数组件或自定义 Hook 顶层调用。
学 Hooks 不要只背 API。更重要的是理解:组件每次渲染都是一次函数执行,Hooks 让 React 能在多次函数执行之间保存状态和副作用关系。