Ref与DOM操作 #

一、Ref概述 #

1.1 什么是Ref #

Ref(Reference)提供了一种访问 DOM 节点或 React 组件实例的方式。

1.2 Ref使用场景 #

场景 说明
焦点管理 自动聚焦输入框
文本选择 选择输入框内容
媒体播放 控制视频/音频
动画触发 触发CSS动画
第三方库 集成非React库
测量DOM 获取元素尺寸位置

1.3 何时使用Ref #

javascript
// ✅ 适合使用Ref的场景
- 焦点管理、文本选择
- 触发动画
- 集成第三方DOM库
- 计时器ID存储

// ❌ 不应该使用Ref的场景
- 表单验证(应该用state)
- 条件渲染(应该用state)
- 列表渲染(应该用state)

二、创建Ref #

2.1 useRef Hook #

javascript
import { useRef } from 'react';

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

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

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

2.2 createRef(类组件) #

javascript
import { Component, createRef } from 'react';

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

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

  render() {
    return (
      <div>
        <input ref={this.inputRef} type="text" />
        <button onClick={this.focusInput}>聚焦</button>
      </div>
    );
  }
}

2.3 回调Ref #

javascript
function TextInput() {
  let inputElement = null;

  const setInputRef = (element) => {
    inputElement = element;
  };

  const focusInput = () => {
    if (inputElement) {
      inputElement.focus();
    }
  };

  return (
    <div>
      <input ref={setInputRef} type="text" />
      <button onClick={focusInput}>聚焦</button>
    </div>
  );
}

2.4 useRef vs createRef #

特性 useRef createRef
适用组件 函数组件 类组件
持久性 组件生命周期内不变 每次渲染创建新实例
推荐程度 推荐 类组件使用

三、访问DOM #

3.1 基本DOM操作 #

javascript
function InputDemo() {
  const inputRef = useRef(null);

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

  const clear = () => {
    inputRef.current.value = '';
  };

  const select = () => {
    inputRef.current.select();
  };

  return (
    <div>
      <input ref={inputRef} defaultValue="Hello" />
      <button onClick={focus}>聚焦</button>
      <button onClick={clear}>清空</button>
      <button onClick={select}>选择</button>
    </div>
  );
}

3.2 获取元素尺寸 #

javascript
function MeasureDemo() {
  const boxRef = useRef(null);
  const [size, setSize] = useState({ width: 0, height: 0 });

  const measure = () => {
    if (boxRef.current) {
      const { offsetWidth, offsetHeight } = boxRef.current;
      setSize({ width: offsetWidth, height: offsetHeight });
    }
  };

  return (
    <div>
      <div
        ref={boxRef}
        style={{ width: '200px', height: '100px', background: '#f0f0f0' }}
      >
        Box
      </div>
      <button onClick={measure}>测量</button>
      <p>宽度: {size.width}, 高度: {size.height}</p>
    </div>
  );
}

3.3 滚动控制 #

javascript
function ScrollDemo() {
  const containerRef = useRef(null);

  const scrollToTop = () => {
    containerRef.current.scrollTop = 0;
  };

  const scrollToBottom = () => {
    containerRef.current.scrollTop = containerRef.current.scrollHeight;
  };

  return (
    <div>
      <div
        ref={containerRef}
        style={{ height: '200px', overflow: 'auto', border: '1px solid #ccc' }}
      >
        {Array.from({ length: 50 }, (_, i) => (
          <div key={i}>Item {i + 1}</div>
        ))}
      </div>
      <button onClick={scrollToTop}>滚动到顶部</button>
      <button onClick={scrollToBottom}>滚动到底部</button>
    </div>
  );
}

3.4 媒体控制 #

javascript
function VideoPlayer({ src }) {
  const videoRef = useRef(null);

  const play = () => {
    videoRef.current.play();
  };

  const pause = () => {
    videoRef.current.pause();
  };

  const setVolume = (volume) => {
    videoRef.current.volume = volume;
  };

  return (
    <div>
      <video ref={videoRef} src={src} width="400" />
      <div>
        <button onClick={play}>播放</button>
        <button onClick={pause}>暂停</button>
        <input
          type="range"
          min="0"
          max="1"
          step="0.1"
          onChange={(e) => setVolume(parseFloat(e.target.value))}
        />
      </div>
    </div>
  );
}

四、useImperativeHandle #

4.1 基本用法 #

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

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

const FancyInput = forwardRef(function FancyInput(props, ref) {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    clear: () => {
      inputRef.current.value = '';
    }
  }));

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

function App() {
  const inputRef = useRef();

  return (
    <div>
      <FancyInput ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>聚焦</button>
      <button onClick={() => inputRef.current.clear()}>清空</button>
    </div>
  );
}

4.2 配合useCallback优化 #

javascript
const FancyInput = forwardRef(function FancyInput({ defaultValue }, ref) {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    getValue: () => {
      return inputRef.current.value;
    },
    setValue: (value) => {
      inputRef.current.value = value;
    },
    reset: () => {
      inputRef.current.value = defaultValue || '';
    }
  }), [defaultValue]);

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

五、forwardRef #

5.1 转发Ref #

javascript
import { forwardRef } from 'react';

const Button = forwardRef(function Button({ children, ...props }, ref) {
  return (
    <button ref={ref} {...props}>
      {children}
    </button>
  );
});

function App() {
  const buttonRef = useRef();

  return (
    <div>
      <Button ref={buttonRef} onClick={() => console.log('clicked')}>
        Click me
      </Button>
      <button onClick={() => buttonRef.current.focus()}>
        聚焦按钮
      </button>
    </div>
  );
}

5.2 高阶组件转发Ref #

javascript
function withLogging(Component) {
  const WithLogging = forwardRef(function WithLogging(props, ref) {
    useEffect(() => {
      console.log('Component mounted/updated');
    });

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

  WithLogging.displayName = `withLogging(${Component.displayName || Component.name})`;
  
  return WithLogging;
}

六、Ref存储可变值 #

6.1 存储任意值 #

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

  const start = () => {
    if (intervalRef.current) return;
    
    intervalRef.current = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);
  };

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

  return (
    <div>
      <p>{seconds}秒</p>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
    </div>
  );
}

6.2 获取前一次值 #

javascript
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>
  );
}

6.3 解决闭包问题 #

javascript
function Stopwatch() {
  const [time, setTime] = useState(0);
  const timeRef = useRef(time);
  timeRef.current = time;

  useEffect(() => {
    const timer = setInterval(() => {
      console.log('当前时间:', timeRef.current);
    }, 1000);

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

  return (
    <div>
      <p>{time}</p>
      <button onClick={() => setTime(t => t + 1)}>增加</button>
    </div>
  );
}

七、最佳实践 #

7.1 避免过度使用Ref #

javascript
// ❌ 不应该用Ref管理状态
function Form() {
  const nameRef = useRef();
  const emailRef = useRef();

  const handleSubmit = () => {
    console.log(nameRef.current.value);
  };
}

// ✅ 应该使用State
function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const handleSubmit = () => {
    console.log(name);
  };
}

7.2 Ref初始化时机 #

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

  useEffect(() => {
    // ✅ 在useEffect中访问ref.current
    if (ref.current) {
      ref.current.focus();
    }
  }, []);

  // ❌ 不要在渲染期间访问ref.current
  if (ref.current) {
    // 这会导致问题
  }

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

7.3 清理Ref #

javascript
function Component() {
  const timerRef = useRef(null);

  useEffect(() => {
    return () => {
      // 组件卸载时清理
      if (timerRef.current) {
        clearInterval(timerRef.current);
      }
    };
  }, []);

  return <div>...</div>;
}

八、常见问题 #

8.1 Ref为null #

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

  const handleClick = () => {
    // ✅ 检查ref.current是否存在
    if (ref.current) {
      ref.current.focus();
    }
  };

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

8.2 函数组件没有实例 #

javascript
// ❌ 函数组件不能直接获取ref
function MyComponent() {
  return <div>Content</div>;
}

function App() {
  const ref = useRef();
  return <MyComponent ref={ref} />; // 警告
}

// ✅ 使用forwardRef
const MyComponent = forwardRef((props, ref) => {
  return <div ref={ref}>Content</div>;
});

九、总结 #

要点 说明
useRef 函数组件创建Ref
createRef 类组件创建Ref
forwardRef 转发Ref给子组件
useImperativeHandle 自定义暴露的实例值

核心原则:

  • Ref 用于访问 DOM 和存储可变值
  • 不要用 Ref 替代 State
  • 访问 ref.current 前检查是否存在
  • 及时清理 Ref 中的资源
最后更新:2026-03-26