
React 学习笔记:脱围机制
使用 ref 引用值
核心概念
ref 用于让组件"记住"某些信息,但不触发重新渲染。它是 React 单向数据流的"脱围机制"。
useRef 基本用法
import { useRef } from 'react';
const ref = useRef(initialValue);
// 返回:{ current: initialValue }
// 修改 ref 不会触发重新渲染
ref.current = ref.current + 1;
ref vs state 对比
| 特性 | ref | state |
|---|---|---|
| 触发渲染 | ❌ 不触发 | ✅ 触发 |
| 可变性 | 可直接修改 | 必须用 setter |
| 读取时机 | 渲染期间读取不可靠 | 每次渲染有快照 |
| 适用场景 | 外部系统交互 | 影响 UI 的数据 |
使用原则
// ✅ 信息用于渲染 → 用 state
const [count, setCount] = useState(0);
// ✅ 信息仅被事件处理需要 → 用 ref
const intervalRef = useRef(null);
典型用例:保存 timeout/interval ID
const intervalRef = useRef(null);
function handleStart() {
intervalRef.current = setInterval(() => { /* ... */ }, 10);
}
function handleStop() {
clearInterval(intervalRef.current); // 需要 ID 来清除
}
useRef 内部原理
// React 内部实现
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref; // 每次渲染返回同一个对象
}
最佳实践
| 规则 | 说明 |
|---|---|
| 脱围机制 | 用于与外部系统/API 交互 |
| 避免渲染中读写 | 如需渲染,请用 state |
| 可变对象 | ref.current 可立即修改 |
核心要点
| 要点 | 说明 |
|---|---|
| useRef | 返回 { current: value } 对象 |
| 不触发渲染 | 修改 ref.current 不会重渲染 |
| 跨渲染保留 | 值在重渲染间保持不变 |
| 适用场景 | DOM 引用、timer ID、非渲染数据 |
使用 ref 操作 DOM
基本用法
import { useRef } from 'react';
const inputRef = useRef(null);
// 传递给 JSX
<input ref={inputRef} />
// React 会将 DOM 节点放入 inputRef.current
inputRef.current.focus();
典型场景
1. 聚焦输入框
function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>聚焦</button>
</>
);
}
2. 滚动到元素
firstCatRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
3. 播放/暂停视频
function VideoPlayer() {
const ref = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
function handleClick() {
isPlaying ? ref.current.pause() : ref.current.play();
setIsPlaying(!isPlaying);
}
return (
<>
<button onClick={handleClick}>{isPlaying ? '暂停' : '播放'}</button>
<video ref={ref} />
</>
);
}
ref 回调:管理动态列表
当 ref 数量不确定时,使用 ref 回调 + Map:
const itemsRef = useRef(new Map());
// 在 JSX 中使用 ref 回调
{items.map(item => (
<li key={item.id} ref={(node) => {
if (node) {
itemsRef.current.set(item.id, node);
} else {
itemsRef.current.delete(item.id);
}
}}>
{item.name}
</li>
))}
// 访问特定节点
itemsRef.current.get(itemId).scrollIntoView();
跨组件访问 DOM
子组件暴露 DOM 节点:
// 子组件通过 ref 转发暴露 input
function MyInput({ ref }) {
return <input ref={ref} />;
}
// 父组件可直接访问
function Form() {
const inputRef = useRef(null);
return <MyInput ref={inputRef} />;
}
使用 useImperativeHandle 限制暴露:
function MyInput({ ref }) {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
realInputRef.current.focus();
}
}));
return <input ref={realInputRef} />;
}
React 更新时机
| 阶段 | 说明 |
|---|---|
| 渲染阶段 | 计算 UI,不要访问 ref |
| 提交阶段 | 更新 DOM,设置 ref.current |
更新顺序:ref.current = null → 更新 DOM → ref.current = node
最佳实践
| 规则 | 说明 |
|---|---|
| 非破坏性操作 | 聚焦、滚动、测量 ✅ |
| 避免修改 DOM | 手动删除/修改可能与 React 冲突 ❌ |
| 事件处理器中使用 | 避免在渲染期间访问 ref |
| flushSync | 需要同步更新时强制刷新 |
核心要点
| 要点 | 说明 |
|---|---|
| ref 属性 | <div ref={myRef}> 让 React 存储 DOM 引用 |
| 浏览器 API | 通过 ref.current 调用 focus、scrollIntoView 等 |
| 跨组件 | 通过 props 传递 ref 或 useImperativeHandle |
| 安全修改 | 只修改 React 无理由更新的部分 |
使用 Effect 进行同步
Effect vs 事件
| 类型 | 触发时机 | 用途 |
|---|---|---|
| 事件处理程序 | 用户交互(点击、输入等) | 响应特定操作 |
| Effect | 渲染本身引起 | 与外部系统同步 |
编写 Effect 3 步法
步骤 1:声明 Effect
import { useEffect } from 'react';
useEffect(() => {
// 每次渲染后执行
});
步骤 2:指定依赖项
useEffect(() => {
// 仅在 a 或 b 变化时执行
}, [a, b]);
useEffect(() => {
// 仅挂载时执行
}, []);
步骤 3:添加清理函数
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect(); // 清理
};
}, []);
依赖项规则
| 写法 | 行为 |
|---|---|
useEffect(fn) |
每次渲染后运行 |
useEffect(fn, []) |
仅挂载时运行 |
useEffect(fn, [a, b]) |
挂载时 + a 或 b 变化时运行 |
依赖由 Effect 内部代码决定,不可随意选择。
清理函数执行时机
组件卸载 → 执行清理
依赖变化 → 执行旧清理 → 执行新 Effect
常见使用场景
1. 同步外部状态
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, []);
2. 订阅事件
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
3. 获取数据(处理竞态)
useEffect(() => {
let ignore = false;
fetchBio(person).then(result => {
if (!ignore) setBio(result);
});
return () => { ignore = true; };
}, [person]);
开发环境运行两次
React 严格模式会挂载→卸载→重新挂载,验证清理函数是否正确。
正确处理: 确保清理函数让 Effect 可安全重新运行,而非阻止执行两次。
不适用 Effect 的场景
| 场景 | 正确做法 |
|---|---|
| 初始化应用 | 放在组件外部 |
| 响应用户交互 | 放在事件处理程序 |
| 状态间转换 | 可能不需要 Effect |
核心要点
| 要点 | 说明 |
|---|---|
| 声明 | useEffect(() => {}, [deps]) |
| 依赖 | 决定 Effect 何时重新运行 |
| 清理 | 返回函数在重新运行前/卸载时调用 |
| 开发模式 | 运行两次验证清理是否正确 |
| 避免滥用 | 不要用于状态间转换 |
你可能不需要 Effect
核心原则
Effect 是脱围机制,只用于与外部系统同步。如果只是基于 props/state 更新,不要用 Effect。
常见误用及替代方案
1. 根据 props/state 更新 state
// ❌ 误用 Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ✅ 直接计算
const fullName = firstName + ' ' + lastName;
2. 缓存昂贵计算
// ❌ 误用 Effect + state
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);
// ✅ 使用 useMemo
const visibleTodos = useMemo(
() => getFilteredTodos(todos, filter),
[todos, filter]
);
3. props 变化时重置 state
// ❌ 误用 Effect
useEffect(() => {
setComment('');
}, [userId]);
// ✅ 使用 key 重置
<Comment key={userId} userId={userId} />
4. 响应用户交互
// ❌ 误用 Effect
useEffect(() => {
if (isInCart) showNotification();
}, [isInCart]);
// ✅ 在事件处理程序中
function handleBuy() {
buyProduct();
showNotification();
}
5. 链式计算
// ❌ 误用多个 Effect 相互触发
useEffect(() => {
setGoldCardCount(cards.filter(c => c >= 90).length);
}, [cards]);
useEffect(() => {
setRound(goldCardCount + 1);
}, [goldCardCount]);
// ✅ 在渲染期间计算
const goldCardCount = cards.filter(c => c >= 90).length;
const round = goldCardCount + 1;
判断标准
| 场景 | 放哪里 |
|---|---|
| 组件显示时执行 | Effect |
| 用户交互触发 | 事件处理程序 |
| props/state 变化 | 渲染期间计算 |
| 与外部系统同步 | Effect |
何时使用 Effect
| 适用 | 不适用 |
|---|---|
| 连接/断开外部服务 | 根据 props 更新 state |
| 订阅浏览器事件 | 缓存计算结果 |
| 控制非 React 组件 | 响应用户交互 |
| 获取数据(需清理) | 链式状态更新 |
核心要点
| 要点 | 说明 |
|---|---|
| 渲染期间能算 | 不用 Effect |
| 能用事件处理 | 不用 Effect |
| 能用 useMemo | 不用 Effect |
| 能用 key 重置 | 不用 Effect |
| 外部系统同步 | 才用 Effect |
响应式 Effect 的生命周期
核心概念
Effect 与组件有不同的生命周期。组件有挂载、更新、卸载三个阶段,但 Effect 只做两件事:开始同步和停止同步。当依赖项变化时,这个循环可能发生多次。
Effect 的生命周期
组件挂载 → Effect 开始同步(连接)
依赖变化 → Effect 停止同步(清理) → Effect 开始同步(新连接)
组件卸载 → Effect 停止同步(清理)
为什么同步需要多次进行
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect(); // 停止同步
};
}, [roomId]);
}
当 roomId 从 "general" 变为 "travel" 时:
- 调用清理函数,断开与
"general"的连接 - 执行新的 Effect,连接到
"travel"聊天室
React 重新同步 Effect 的过程
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 调用清理函数 | 使用旧的 props/state 停止同步 |
| 2 | 执行 Effect 主体 | 使用新的 props/state 开始同步 |
从 Effect 的角度思考
不要从组件角度(挂载/更新/卸载)思考 Effect,而是关注启动/停止周期:
开始同步 roomId="general" → 停止同步
开始同步 roomId="travel" → 停止同步
开始同步 roomId="music" → 停止同步
React 如何验证 Effect 可以重新同步
开发环境中,React 会挂载 → 卸载 → 重新挂载组件,验证清理函数是否正确实现。
// 开发环境会看到三个日志:
// "开始同步" (首次挂载)
// "停止同步" (清理)
// "开始同步" (重新挂载)
依赖项的工作原理
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // 只有 roomId 变化时才重新同步
| 比较方式 | 说明 |
|---|---|
| Object.is | React 用此方法比较依赖项 |
| 初始渲染 | ["general"] |
| 更新渲染 | ["travel"] |
| 结果 | 值不同,触发重新同步 |
响应式值
组件内部声明的所有变量都是响应式的:
| 类型 | 示例 | 是否响应式 |
|---|---|---|
| Props | roomId |
✅ 是 |
| State | serverUrl |
✅ 是 |
| Context | settings |
✅ 是 |
| 计算值 | selectedServerUrl ?? settings.default |
✅ 是 |
| 组件外常量 | const url = '...' |
❌ 否 |
每个 Effect 代表独立的同步过程
// ❌ 错误:混合不同职责
useEffect(() => {
logVisit(roomId); // 分析事件
const conn = createConnection(roomId);
conn.connect(); // 连接
return () => conn.disconnect();
}, [roomId]);
// ✅ 正确:拆分为独立 Effect
useEffect(() => {
logVisit(roomId);
}, [roomId]);
useEffect(() => {
const conn = createConnection(roomId);
conn.connect();
return () => conn.disconnect();
}, [roomId]);
依赖项规则
// ✅ 正确:声明所有依赖
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);
// ✅ 正确:非响应式值可省略
const serverUrl = 'https://localhost:1234'; // 组件外,非响应式
const roomId = 'general'; // 组件外,非响应式
useEffect(() => {
// ...
}, []);
避免重新同步的方法
| 方法 | 示例 | 说明 |
|---|---|---|
| 移到组件外 | const url = '...' |
不再响应渲染 |
| 移到 Effect 内 | useEffect(() => { const url = '...' }) |
不在渲染期间计算 |
核心要点
| 要点 | 说明 |
|---|---|
| Effect 生命周期 | 只关注开始/停止同步,不关注组件挂载/卸载 |
| 重新同步时机 | 依赖项变化时(Object.is 比较) |
| 清理函数 | 使用旧值停止同步,然后执行新 Effect 开始同步 |
| 响应式值 | props、state、组件体内变量都必须声明为依赖项 |
| 独立同步过程 | 每个 Effect 应代表一个独立的同步逻辑 |
| 开发环境 | 会多执行一次验证清理函数正确性 |
将事件从 Effect 中分开
核心概念
事件处理函数和 Effect 有不同的触发方式:
- 事件处理函数:只在响应特定用户交互时运行(手动触发)
- Effect:每当需要保持同步时自动运行(响应式触发)
事件处理函数 vs Effect
| 特性 | 事件处理函数 | Effect |
|---|---|---|
| 触发时机 | 用户交互(点击、输入等) | 依赖项变化时 |
| 逻辑类型 | 非响应式 | 响应式 |
| 运行次数 | 只在交互时运行一次 | 可能多次运行 |
| 适用场景 | 处理特定操作 | 保持同步 |
选择标准
// ✅ 事件处理函数:响应用户交互
function handleSendClick() {
sendMessage(message); // 只在点击按钮时运行
}
// ✅ Effect:保持同步
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // roomId 变化时重新运行
响应式值
组件内部声明的变量都是响应式的:
const serverUrl = '...'; // ❌ 非响应式(组件外)
function ChatRoom({ roomId }) { // ✅ 响应式(props)
const [message, setMessage] = useState(''); // ✅ 响应式(state)
}
响应式 vs 非响应式逻辑
| 逻辑类型 | 示例 | 是否响应式 |
|---|---|---|
| 事件处理函数内部 | sendMessage(message) |
❌ 非响应式 |
| Effect 内部 | createConnection(roomId) |
✅ 响应式 |
问题场景
// ❌ 问题:theme 变化会导致聊天重连
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme); // theme 是响应式的
});
connection.connect();
return () => connection.disconnect();
}, [roomId, theme]); // theme 变化也会触发 Effect
解决方案:useEffectEvent
使用 useEffectEvent 从 Effect 中提取非响应式逻辑:
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
// Effect Event:非响应式逻辑
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected(); // 调用 Effect Event
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ 不需要 theme 作为依赖项
}
Effect Event 特性
| 特性 | 说明 |
|---|---|
| 非响应式 | 内部逻辑不会因依赖变化重新运行 |
| 最新值 | 始终能读取最新的 props 和 state |
| 依赖项 | 不需要添加到 Effect 的依赖项中 |
| 声明位置 | 必须在使用它的 Effect 旁边声明 |
Effect Event 使用示例
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
// ✅ Effect Event:读取最新的 numberOfItems
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});
// ✅ 只响应 url 变化
useEffect(() => {
onVisit(url);
}, [url]); // numberOfItems 不需要在这里
}
Effect Event 局限性
// ❌ 错误:不要传递 Effect Event
useTimer(onTick, 1000);
// ✅ 正确:在 Effect 旁边声明
function useTimer(callback, delay) {
const onTick = useEffectEvent(() => {
callback();
});
useEffect(() => {
const id = setInterval(() => {
onTick(); // ✅ 局部调用
}, delay);
return () => clearInterval(id);
}, [delay]);
}
核心要点
| 要点 | 说明 |
|---|---|
| 事件处理函数 | 非响应式,只在用户交互时运行 |
| Effect | 响应式,依赖项变化时自动运行 |
| useEffectEvent | 提取 Effect 中的非响应式逻辑 |
| Effect Event | 不需要添加到依赖项,始终读取最新值 |
| 声明位置 | 必须在使用它的 Effect 旁边声明 |
| 适用场景 | 需要在响应式 Effect 中执行非响应式操作 |
移除 Effect 依赖
核心原则
依赖应该和代码保持一致。每个被 Effect 使用的响应式值,必须在依赖中声明。要改变依赖,首先要改变代码。
依赖规则
// ✅ 正确:声明所有依赖
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
// ❌ 错误:缺少依赖
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 缺少 roomId
移除依赖的方法
| 方法 | 说明 |
|---|---|
| 移到组件外 | 证明它不是响应式值 |
| 移到 Effect 内 | 不再是 Effect 的依赖 |
| 使用 useState 更新函数 | 避免在 Effect 中读取 state |
| 使用 useEffectEvent | 读取最新值而不响应变化 |
问题场景与解决方案
1. 代码应该移到事件处理程序中吗?
// ❌ 问题:Effect 中有特定事件的逻辑
useEffect(() => {
if (submitted) {
post('/api/register');
showNotification('Successfully registered!', theme);
}
}, [submitted, theme]);
// ✅ 解决:移到事件处理程序
function handleSubmit() {
post('/api/register');
showNotification('Successfully registered!', theme);
}
2. Effect 是否在做几件不相关的事情?
// ❌ 问题:单个 Effect 同步两个独立逻辑
useEffect(() => {
fetch(`/api/cities?country=${country}`);
fetch(`/api/areas?city=${city}`);
}, [country, city]); // city 变化会导致 cities 重新获取
// ✅ 解决:拆分为独立 Effect
useEffect(() => {
fetch(`/api/cities?country=${country}`);
}, [country]);
useEffect(() => {
fetch(`/api/areas?city=${city}`);
}, [city]);
3. 是否在读取一些状态来计算下一个状态?
// ❌ 问题:Effect 读取 messages 来更新 messages
useEffect(() => {
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]); // 每条消息都会重连
});
}, [roomId, messages]);
// ✅ 解决:使用 state 更新函数
useEffect(() => {
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]); // 不需要 messages 依赖
});
}, [roomId]);
4. 你想读取一个值而不对其变化做出"反应"吗?
// ❌ 问题:isMuted 变化会导致重连
useEffect(() => {
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) playSound();
});
}, [roomId, isMuted]);
// ✅ 解决:使用 useEffectEvent
const onMessage = useEffectEvent(receivedMessage => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) playSound();
});
useEffect(() => {
connection.on('message', onMessage);
}, [roomId]);
5. 包装来自 props 的事件处理程序
// ❌ 问题:父组件每次渲染都传递不同的函数
<ChatRoom onReceiveMessage={msg => handleReceive(msg)} />
// ✅ 解决:使用 useEffectEvent 包装
const onMessage = useEffectEvent(receivedMessage => {
onReceiveMessage(receivedMessage);
});
useEffect(() => {
connection.on('message', onMessage);
}, [roomId]); // 不需要 onReceiveMessage 依赖
对象和函数作为依赖的问题
// ❌ 问题:每次渲染都创建新对象
const options = { serverUrl, roomId };
useEffect(() => {
const connection = createConnection(options);
connection.connect();
}, [options]); // 每次渲染都会重连
解决方案
| 方法 | 适用场景 | 示例 |
|---|---|---|
| 移到组件外 | 静态对象/函数 | const options = { ... } |
| 移到 Effect 内 | 动态对象/函数 | useEffect(() => { const options = {...} }) |
| 提取原始值 | 从 props 接收的对象 | const { roomId } = options |
// ✅ 方法1:移到 Effect 内
useEffect(() => {
const options = { serverUrl, roomId };
const connection = createConnection(options);
connection.connect();
}, [roomId]); // 只依赖 roomId
// ✅ 方法2:提取原始值
function ChatRoom({ options }) {
const { roomId, serverUrl } = options;
useEffect(() => {
const connection = createConnection({ roomId, serverUrl });
connection.connect();
}, [roomId, serverUrl]);
}
核心要点
| 要点 | 说明 |
|---|---|
| 依赖一致性 | 依赖必须与代码中使用的响应式值一致 |
| 改变依赖 | 必须先改变代码,而不是直接修改依赖列表 |
| 拆分 Effect | 每个 Effect 应代表一个独立的同步过程 |
| state 更新函数 | 使用 setState(prev => ...) 避免读取 state |
| useEffectEvent | 读取最新值而不引起重新同步 |
| 对象/函数依赖 | 移到组件外或 Effect 内,避免无意中变化 |
使用自定义 Hook 复用逻辑
核心概念
自定义 Hook 是一个以 use 开头的函数,用于在组件间共享状态逻辑(而非状态本身)。它能隐藏外部系统交互的复杂细节,让组件专注于目标。
自定义 Hook 基本结构
// 命名必须以 use 开头
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() { setIsOnline(true); }
function handleOffline() { setIsOnline(false); }
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
使用自定义 Hook
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
function SaveButton() {
const isOnline = useOnlineStatus();
return (
<button disabled={!isOnline}>
{isOnline ? 'Save progress' : 'Reconnecting...'}
</button>
);
}
自定义 Hook 共享的是状态逻辑,不是状态本身
// ❌ 错误理解:两个组件共享同一个 isOnline
// ✅ 正确理解:两个组件各自调用 useOnlineStatus,各自有独立的状态
function StatusBar() {
const isOnline = useOnlineStatus(); // 独立的状态
}
function SaveButton() {
const isOnline = useOnlineStatus(); // 独立的状态
}
| 概念 | 说明 |
|---|---|
| 共享的是 | 状态逻辑(如何追踪、更新) |
| 不共享的是 | 状态本身(每个调用独立) |
| 效果 | 同时更新(因为使用相同的外部值同步) |
在 Hook 之间传递响应值
// Hook 接收响应式参数
function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
// 组件传递最新的 props
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({ roomId, serverUrl });
}
把事件处理函数传到自定义 Hook
// Hook 接收事件处理函数
function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
const onMessage = useEffectEvent(onReceiveMessage);
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
connection.on('message', (msg) => {
onMessage(msg); // 使用 Effect Event
});
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ 不需要 onReceiveMessage 依赖
}
// 组件传递事件处理函数
function ChatRoom({ roomId }) {
useChatRoom({
roomId,
onReceiveMessage(msg) {
showNotification('New message: ' + msg);
}
});
}
什么时候使用自定义 Hook
| 场景 | 建议 |
|---|---|
| 简单 state 包装 | 不需要(如 useFormInput) |
| 复杂 Effect 逻辑 | 推荐提取到自定义 Hook |
| 多组件共享 Effect | 必须提取到自定义 Hook |
自定义 Hook 帮助迁移
// 旧实现:useState + useEffect
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => { /* ... */ }, []);
return isOnline;
}
// 新实现:useSyncExternalStore(更好的 API)
function useOnlineStatus() {
return useSyncExternalStore(
callback => { /* 订阅 */ },
() => navigator.onLine
);
}
| 优势 | 说明 |
|---|---|
| 数据流清晰 | 输入 → Hook → 输出 |
| 组件专注 | 只关心目标,不关心实现 |
| 易于迁移 | 升级 Hook 实现,组件无需修改 |
不止一个方法可以做到
// 方法1:自定义 Hook + useEffect
function useFadeIn(ref) {
useEffect(() => {
// 动画逻辑
}, []);
}
// 方法2:JavaScript 类封装
class FadeIn {
constructor(element) { /* ... */ }
start() { /* ... */ }
}
// 方法3:纯 CSS 动画
// .fade-in { animation: fadeIn 1s ease-in; }
核心要点
| 要点 | 说明 |
|---|---|
| 命名规则 | 必须以 use 开头 |
| 共享内容 | 状态逻辑,不是状态本身 |
| 参数传递 | 接收响应式值,随组件重新渲染 |
| 事件处理 | 使用 useEffectEvent 包装 |
| 使用场景 | 复杂 Effect 逻辑、多组件共享 |
| 优势 | 隐藏实现细节、便于迁移 |