Skip to content
第 150 / 250 章前端⏱ 10 分钟阅读

第 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 引用对比

场景useStateuseRef
触发渲染
用于 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作用触发渲染
useRefDOM 引用/可变值
useMemo缓存值依赖变化时
useCallback缓存函数依赖变化时

动手练习

  1. 写一个 usePrevious hook(用 useRef)
  2. 写一个搜索 + 列表,过滤用 useMemo
  3. 用 useCallback 优化一个 todo 列表
  4. 写一个视频播放器,useRef 控制 play/pause

推荐阅读


下一章:第 151 章:Hooks 之 useReducer / useContext

本站基于 VitePress 构建 · 由 Codebook 团队维护