第 150 章:Hooks 之 useRef / useMemo / useCallback
学习目标
- 掌握 useRef 引用 DOM 和保存可变值
- 理解 useMemo 缓存计算结果
- 理解 useCallback 缓存函数引用
- 区分三者使用场景
一、useRef 引用
1.1 引用 DOM 元素
tsx
'use client';
import { useRef } from 'react';
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
function focusInput() {
inputRef.current?.focus();
}
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>聚焦</button>
</>
);
}1.2 保存可变值(不触发渲染)
tsx
function Timer() {
const countRef = useRef(0); // 不会触发渲染
function start() {
setInterval(() => {
countRef.current++;
console.log(countRef.current); // 内部更新
}, 1000);
}
return <button onClick={start}>开始</button>;
}1.3 引用对比
| 场景 | useState | useRef |
|---|---|---|
| 触发渲染 | ✅ | ❌ |
| 用于 JSX | ✅ | ❌ |
| 用于定时器 ID | ❌ | ✅ |
| 用于 DOM 引用 | ❌ | ✅ |
二、useMemo 缓存计算
tsx
import { useMemo, useState } from 'react';
function ExpensiveList({ items, filter }: Props) {
const [count, setCount] = useState(0);
// ① 只在 items 或 filter 变化时重算
const filtered = useMemo(() => {
console.log('过滤计算');
return items.filter(x => x.includes(filter));
}, [items, filter]);
return (
<>
<button onClick={() => setCount(count + 1)}>+1 ({count})</button>
<List items={filtered} />
</>
);
}2.1 引用稳定
tsx
function Parent() {
const [count, setCount] = useState(0);
// ❌ 每次渲染都新建对象
const config = { theme: 'dark', count };
// ✅ 引用稳定
const config = useMemo(() => ({ theme: 'dark', count }), [count]);
return <Child config={config} />;
}三、useCallback 缓存函数
tsx
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// ❌ 每次渲染都是新函数,子组件会重渲染
const handleClick = () => console.log(count);
// ✅ 稳定引用
const handleClick = useCallback(() => {
console.log(count);
}, [count]);
return <Child onClick={handleClick} />;
}四、三者关系
useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。
五、使用场景
5.1 useRef 场景
tsx
// 场景 1:聚焦输入框
inputRef.current?.focus();
// 场景 2:滚动到指定位置
divRef.current?.scrollIntoView({ behavior: 'smooth' });
// 场景 3:保存上次值
const prevCount = useRef(count);
useEffect(() => {
prevCount.current = count;
});
// 场景 4:保存 setTimeout ID
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);5.2 useMemo 场景
tsx
// 场景 1:重计算(过滤、排序)
const filtered = useMemo(() => items.filter(x => x.match), [items, match]);
// 场景 2:稳定对象引用
const style = useMemo(() => ({ color, fontSize }), [color, fontSize]);
// 场景 3:派生状态
const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName]
);5.3 useCallback 场景
tsx
// 场景 1:传给 memo 的子组件
const handleClick = useCallback(() => doSomething(id), [id]);
return <MemoChild onClick={handleClick} />;
// 场景 2:作为 useEffect 依赖
const handler = useCallback(() => doSomething(id), [id]);
useEffect(() => {
handler();
}, [handler]);六、不要过度使用
6.1 简单计算不需要 useMemo
tsx
// ❌ 过度优化
const sum = useMemo(() => a + b, [a, b]);
// ✅ 直接算
const sum = a + b;6.2 简单函数不需要 useCallback
tsx
// ❌ 没意义
const handleClick = useCallback(() => setCount(0), []);
// ✅ 直接写
<button onClick={() => setCount(0)}>原则:只在你确定会引发性能问题的地方用。
七、完整示例
tsx
'use client';
import { useState, useRef, useMemo, useCallback } from 'react';
function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
// 派生数据
const filtered = useMemo(
() => todos.filter(t => t.text.includes(filter)),
[todos, filter]
);
// 稳定函数
const add = useCallback(() => {
const text = inputRef.current?.value.trim();
if (!text) return;
setTodos(ts => [...ts, { id: Date.now(), text, done: false }]);
if (inputRef.current) inputRef.current.value = '';
}, []);
return (
<>
<input ref={inputRef} />
<button onClick={add}>添加</button>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="过滤"
/>
<ul>
{filtered.map(t => <li key={t.id}>{t.text}</li>)}
</ul>
</>
);
}八、本章小结
| Hook | 作用 | 触发渲染 |
|---|---|---|
| useRef | DOM 引用/可变值 | ❌ |
| useMemo | 缓存值 | 依赖变化时 |
| useCallback | 缓存函数 | 依赖变化时 |
动手练习
- 写一个 usePrevious hook(用 useRef)
- 写一个搜索 + 列表,过滤用 useMemo
- 用 useCallback 优化一个 todo 列表
- 写一个视频播放器,useRef 控制 play/pause