HookReact 18+

useRef: 引用 DOM 和值

useRef 让你能够引用一个不需要渲染的值,或者直接访问 DOM 元素

什么是 useRef?

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

TypeScript
import { useRef } from 'react';

const ref = useRef(initialValue);

主要用途

  • 访问 DOM 元素
  • 保存不需要触发重新渲染的可变值
  • 存储定时器 ID
  • 存储之前的 props 或 state
ref 不会触发重新渲染

改变 ref 的值不会触发组件重新渲染。这是 ref 与 state 的主要区别。

访问 DOM 元素

React 支持一个特殊的属性 ref,可以附加到任何组件或元素上。

基本示例: 聚焦输入框

TypeScript
import { useRef, useEffect } from 'react';

function Form() {
  const inputRef = useRef(null);

  useEffect(() => {
    // 组件挂载后自动聚焦
    inputRef.current.focus();
  }, []);

  return (
    <div>
      <label>
        姓名:
        <input ref={inputRef} type="text" />
      </label>
    </div>
  );
}

示例: 滚动到元素

TypeScript
function ChatMessages() {
  const messagesEndRef = useRef(null);

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  useEffect(() => {
    scrollToBottom();
  }, [/* 依赖项 */]);

  return (
    <div>
      {/* 消息列表 */}
      {messages.map(msg => (
        <div key={msg.id}>{msg.text}</div>
      ))}

      {/* 用于滚动定位的元素 */}
      <div ref={messagesEndRef} />
    </div>
  );
}
ref 只能在某些元素上使用
  • 原生 HTML 元素: ✅ 可以使用 ref
  • 函数组件: ❌ 需要使用 forwardRef
  • 类组件: ✅ 可以使用 ref

使用 forwardRef 暴露 ref

TypeScript
import { forwardRef } from 'react';

// 使用 forwardRef 让父组件可以访问子组件的 DOM
const MyInput = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});

function Form() {
  const inputRef = useRef(null);

  const handleClick = () => {
    inputRef.current.focus();
  };

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>聚焦输入框</button>
    </>
  );
}

ref 回调

TypeScript
function Form() {
  const inputRef = useCallback((node) => {
    if (node) {
      // node 是 DOM 元素
      node.focus();
    }
  }, []);

  return <input ref={inputRef} />;
}

保存可变值

ref 可以保存任何可变值,类似于类组件的实例属性。改变 ref.current 不会触发重新渲染。

示例:定时器 ID

TypeScript
import { useState, useEffect, useRef } from 'react';

function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    return () => clearInterval(intervalRef.current);
  }, []);

  const stop = () => {
    clearInterval(intervalRef.current);
  };

  return (
    <div>
      <div>已经运行: {seconds} 秒</div>
      <button onClick={stop}>停止</button>
    </div>
  );
}

示例:存储之前的值

TypeScript
function usePrevious(value) {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
}

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>当前: {count}</p>
      <p>之前: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}
ref vs state
特性refstate
改变后不触发重新渲染触发重新渲染
访问方式ref.current直接访问状态值
异步更新同步更新异步批处理
用途DOM 引用、定时器 ID渲染数据

useRef vs useState

问题:状态过期

TypeScript
// ❌ 使用 state 导致问题
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      // 这里看到的 count 始终是 0(闭包陷阱)
      console.log(count);
      setCount(count + 1);
    }, 1000);

    return () => clearInterval(timer);
  }, []); // 空依赖数组

  return <div>{count}</div>;
}
TypeScript
// ✅ 使用 ref 解决
function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(0);

  useEffect(() => {
    const timer = setInterval(() => {
      // 使用 ref 获取最新值
      countRef.current += 1;
      setCount(countRef.current);
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  return <div>{count}</div>;
}
何时使用 ref 而不是 state?
  • 当值的改变不需要触发重新渲染时
  • 当需要在 effect 中读取最新值时
  • 当存储与渲染无关的数据时(如定时器 ID)

useImperativeHandle:暴露自定义实例

useImperativeHandle 可以自定义暴露给父组件的 ref 值。

TypeScript
import { useRef, useImperativeHandle, forwardRef } from 'react';

// 子组件
const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef();

  // 自定义暴露给父组件的方法
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    clear: () => {
      inputRef.current.value = '';
    },
    getValue: () => {
      return inputRef.current.value;
    },
  }));

  return <input ref={inputRef} {...props} />;
});

// 父组件
function Form() {
  const inputRef = useRef();

  const handleClick = () => {
    // 调用子组件暴露的方法
    inputRef.current.focus();
    inputRef.current.clear();

    const value = inputRef.current.getValue();
    console.log('输入值:', value);
  };

  return (
    <>
      <CustomInput ref={inputRef} />
      <button onClick={handleClick}>聚焦并清空</button>
    </>
  );
}
谨慎使用

useImperativeHandle 应该谨慎使用,因为它破坏了组件的封装性。 大多数情况下,应该通过 props 来控制组件。

常见使用场景

1. 集成第三方库

TypeScript
function Chart({ data }) {
  const canvasRef = useRef(null);
  const chartRef = useRef(null);

  useEffect(() => {
    if (canvasRef.current) {
      // 初始化图表库
      chartRef.current = new Chart(canvasRef.current, {
        type: 'bar',
        data: data,
      });
    }

    return () => {
      // 清理图表实例
      if (chartRef.current) {
        chartRef.current.destroy();
      }
    };
  }, []);

  // 更新图表数据
  useEffect(() => {
    if (chartRef.current) {
      chartRef.current.data = data;
      chartRef.current.update();
    }
  }, [data]);

  return <canvas ref={canvasRef} />;
}

2. 检测元素是否可见

TypeScript
function LazyImage({ src, alt }) {
  const imgRef = useRef(null);
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
      },
      { threshold: 0.1 }
    );

    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <div ref={imgRef} style={{ minHeight: '200px' }}>
      {isVisible ? (
        <img src={src} alt={alt} />
      ) : (
        <div>加载中...</div>
      )}
    </div>
  );
}

3. 可变值的计数器

TypeScript
function ClickLogger() {
  const [logs, setLogs] = useState([]);
  const clickCountRef = useRef(0);

  const handleClick = () => {
    clickCountRef.current += 1;

    setLogs(prev => [
      ...prev,
      `点击 #${clickCountRef.current} at ${new Date().toLocaleTimeString()}`,
    ]);
  };

  return (
    <div>
      <button onClick={handleClick}>记录点击</button>
      <ul>
        {logs.map((log, index) => (
          <li key={index}>{log}</li>
        ))}
      </ul>
    </div>
  );
}

4. 防抖和节流

TypeScript
function useDebounce(callback, delay) {
  const timeoutRef = useRef();

  useEffect(() => {
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, []);

  const debouncedCallback = useCallback((...args) => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }

    timeoutRef.current = setTimeout(() => {
      callback(...args);
    }, delay);
  }, [callback, delay]);

  return debouncedCallback;
}

function SearchInput() {
  const [query, setQuery] = useState('');
  const debouncedSearch = useDebounce((value) => {
    console.log('搜索:', value);
  }, 500);

  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value);
    debouncedSearch(value);
  };

  return <input value={query} onChange={handleChange} />;
}

常见错误

1. 在渲染中读取 ref.current

TypeScript
// ❌ 错误:在渲染中读取 ref 会导致问题
function Counter() {
  const countRef = useRef(0);

  return <div>{countRef.current}</div>; // 不会更新
}

// ✅ 正确:使用 state
function Counter() {
  const [count, setCount] = useState(0);

  return <div>{count}</div>;
}

2. 在渲染中修改 ref.current

TypeScript
// ❌ 错误:每次渲染都会执行
function Component() {
  const ref = useRef(0);
  ref.current += 1; // 每次渲染都增加

  return <div>{ref.current}</div>;
}

// ✅ 正确:在 effect 中修改
function Component() {
  const ref = useRef(0);

  useEffect(() => {
    ref.current += 1;
  });

  return <div>{ref.current}</div>;
}

3. 忘记检查 ref.current 是否存在

TypeScript
// ❌ 错误:可能导致 null 错误
useEffect(() => {
  inputRef.current.focus(); // 如果 ref 为空会报错
}, []);

// ✅ 正确:检查是否存在
useEffect(() => {
  inputRef.current?.focus(); // 使用可选链
}, []);

4. ref 在首次渲染前是 null

TypeScript
function Component() {
  const ref = useRef(null);

  console.log(ref.current); // null(首次渲染)

  useEffect(() => {
    console.log(ref.current); // DOM 元素(渲染后)
  }, []);

  return <div ref={ref} />;
}

最佳实践

  • 1. 只在必要时使用 ref

    大多数情况下,应该使用 state 而不是 ref。ref 只用于特定场景

  • 2. 使用可选链访问 ref

    使用 ref.current?.method() 避免可能的 null 错误

  • 3. 在 useEffect 中访问 DOM

    ref 在首次渲染前是 null,应该在 effect 中访问

  • 4. 清理 ref 中的资源

    如果 ref 中存储了需要清理的资源(如定时器、订阅),记得在清理函数中清理

  • 5. 避免过度使用 useImperativeHandle

    大多数情况下应该通过 props 来控制组件,而不是直接操作 ref

下一步

现在你已经了解了 useRef,可以继续学习:

这篇文章有帮助吗?