React useRef使用:从基础到实战应用

一、为什么需要 useRef

在 React 函数组件中管理状态主要依靠 useState,但当我们需要操作 DOM 元素或保存可变值时,state 的更新机制反而会成为阻碍。此时 useRef 的价值就体现出来了:

  1. 持久化存储:ref 对象在组件的整个生命周期中保持不变
  2. 直接访问:可以绕过 React 的渲染机制直接操作 DOM
  3. 性能优化:避免不必要的重新渲染
  4. 副作用管理:配合 useEffect 使用更灵活

与 state 的关键区别在于,修改 ref 的 current 属性不会触发组件重新渲染,这是理解 useRef 的核心要点。

二、基本使用方法

2.1 创建 ref 对象

import { useRef } from 'react';

function MyComponent() {
  const inputRef = useRef(null);
  // ...
}

2.2 访问 ref 的三种方式

  1. 字符串方式(已废弃)
// 不推荐使用
<input ref="myInput" />
  1. 回调函数方式
<input ref={(el) => { this.inputEl = el; }} />
  1. useRef Hook(推荐)
function TextInput() {
  const inputRef = useRef(null);

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

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus</button>
    </>
  );
}

三、核心使用场景

3.1 DOM 元素操作

function ImageLoader() {
  const imgRef = useRef(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    if (imgRef.current) {
      imgRef.current.onload = () => setLoading(false);
    }
  }, []);

  return (
    <div>
      {loading && <div>Loading...</div>}
      <img
        ref={imgRef}
        src="large-image.jpg"
        alt="Large content"
        style={{ display: loading ? 'none' : 'block' }}
      />
    </div>
  );
}

3.2 保存可变值

function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef();

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(intervalRef.current);
  }, []);

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

3.3 获取 previous props/state

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

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

  return (
    <div>
      <p>Current: {count}, Previous: {prevCount}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}

四、高级应用场景

4.1 与第三方库集成

function ChartWrapper() {
  const chartRef = useRef(null);
  const chartInstance = useRef(null);

  useEffect(() => {
    if (chartRef.current && !chartInstance.current) {
      chartInstance.current = new Chart(chartRef.current, {
        type: 'line',
        data: chartData,
        options: { /*...*/ }
      });
    }

    return () => {
      if (chartInstance.current) {
        chartInstance.current.destroy();
        chartInstance.current = null;
      }
    };
  }, []);

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

4.2 自定义 Hook 中的应用

function useHover() {
  const [isHovered, setIsHovered] = useState(false);
  const ref = useRef(null);

  useEffect(() => {
    const node = ref.current;

    const handleMouseEnter = () => setIsHovered(true);
    const handleMouseLeave = () => setIsHovered(false);

    if (node) {
      node.addEventListener('mouseenter', handleMouseEnter);
      node.addEventListener('mouseleave', handleMouseLeave);
    }

    return () => {
      node.removeEventListener('mouseenter', handleMouseEnter);
      node.removeEventListener('mouseleave', handleMouseLeave);
    };
  }, []);

  return [ref, isHovered];
}

// 使用示例
function HoverComponent() {
  const [hoverRef, isHovered] = useHover();
  return (
    <div ref={hoverRef}>
      {isHovered ? 'Hovering!' : 'Hover me'}
    </div>
  );
}

五、性能优化技巧

5.1 避免重复创建 ref

// 正确 ✅
const ref = useRef(new HeavyObject());

// 错误 ❌ 每次渲染都会创建新实例
const ref = useRef(null);
if (!ref.current) {
  ref.current = new HeavyObject();
}

5.2 结合 useCallback 使用

function MeasurableComponent() {
  const [width, setWidth] = useState(0);
  const measuredRef = useCallback(node => {
    if (node) {
      setWidth(node.getBoundingClientRect().width);
    }
  }, []);

  return <div ref={measuredRef}>Width: {width}px</div>;
}

5.3 批量 DOM 操作

function VirtualList() {
  const listRef = useRef(null);
  const itemRefs = useRef(new Map());

  const scrollToItem = (index) => {
    const itemNode = itemRefs.current.get(index);
    if (itemNode) {
      listRef.current.scrollTo({
        top: itemNode.offsetTop,
        behavior: 'smooth'
      });
    }
  };

  return (
    <div ref={listRef} className="list">
      {items.map((item, index) => (
        <div
          key={item.id}
          ref={node => {
            if (node) {
              itemRefs.current.set(index, node);
            } else {
              itemRefs.current.delete(index);
            }
          }}
        >
          {item.content}
        </div>
      ))}
    </div>
  );
}

六、常见问题解决方案

6.1 ref 为 null 的情况

function SafeRefComponent() {
  const ref = useRef(null);

  useEffect(() => {
    // 使用可选链操作符
    console.log(ref.current?.value);
    
    // 或者使用定时器
    const timer = setTimeout(() => {
      if (ref.current) {
        ref.current.focus();
      }
    }, 100);

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

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

6.2 转发 refs

const FancyInput = React.forwardRef((props, ref) => {
  return <input ref={ref} className="fancy" {...props} />;
});

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

  useEffect(() => {
    inputRef.current.focus();
  }, []);

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

6.3 类组件中的使用

class ClassComponent extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }

  focusInput = () => {
    this.inputRef.current.focus();
  };

  render() {
    return <input ref={this.inputRef} />;
  }
}

七、最佳实践指南

  1. 优先使用 useRef 而非 createRef
    在函数组件中始终使用 useRef,类组件中使用 createRef

  2. 避免在渲染期间修改 ref
    ref 的修改应放在事件处理或 useEffect 中

  3. 区分可变值与不可变值
    需要触发渲染的值用 state,否则用 ref

  4. 注意内存泄漏
    及时清理定时器、事件监听器等资源

  5. 配合 TypeScript 使用

    const inputRef = useRef<HTMLInputElement>(null);
    
  6. 性能敏感操作优先使用 ref
    如动画帧、大量计算等

八、实战案例:表单验证增强

function AdvancedForm() {
  const formRef = useRef(null);
  const [errors, setErrors] = useState({});

  const validateField = (name, value) => {
    // 复杂验证逻辑
    if (name === 'email' && !/\S+@\S+\.\S+/.test(value)) {
      return 'Invalid email format';
    }
    return null;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData(formRef.current);
    const newErrors = {};

    for (const [name, value] of formData.entries()) {
      const error = validateField(name, value);
      if (error) newErrors[name] = error;
    }

    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors);
    } else {
      // 提交表单
    }
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <div>
        <label>Email:</label>
        <input name="email" />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>
      {/* 其他表单字段 */}
      <button type="submit">Submit</button>
    </form>
  );
}
正文到此结束
评论插件初始化中...
Loading...