返回首页

说说 React 中 ref 的用法?应用场景?

问题解析

面试官考察点:

  • 是否理解 ref 的概念和用途
  • 是否掌握 ref 的创建和使用方式
  • 是否了解 ref 转发
  • 对 ref 使用场景的理解

核心概念

什么是 Ref

Ref(reference)提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素

在 React 中,数据通常通过 props 自上而下传递,但某些情况下需要在组件间直接交互(如获取焦点、触发动画等),这时可以使用 ref。

Ref 的本质

  • 对于 HTML 元素:ref 指向底层的 DOM 节点
  • 对于类组件:ref 指向组件的实例
  • 对于函数组件:默认没有实例,需要使用 forwardRef 或 useImperativeHandle

详细解答

1. 创建 Ref 的三种方式

方式一:createRef(类组件推荐)

import React, { Component } from 'react';

class MyComponent extends Component {
  constructor(props) {
    super(props);
    // 创建 ref
    this.myRef = React.createRef();
  }

  componentDidMount() {
    // 访问 ref
    console.log(this.myRef.current);
    this.myRef.current.focus();
  }

  render() {
    // 绑定 ref
    return <input ref={this.myRef} />;
  }
}

方式二:useRef(函数组件推荐)

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

function MyComponent() {
  const myRef = useRef(null);

  useEffect(() => {
    // 访问 DOM 节点
    myRef.current.focus();
  }, []);

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

方式三:回调 Ref(灵活但已较少使用)

class MyComponent extends Component {
  componentDidMount() {
    // 直接访问
    this.inputElement.focus();
  }

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

2. Ref 的常见使用场景

管理焦点、文本选择或媒体播放

function TextInputWithFocusButton() {
  const inputEl = useRef(null);

  const onButtonClick = () => {
    // 让 input 获得焦点
    inputEl.current.focus();
  };

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

获取表单值(非受控组件)

function UncontrolledForm() {
  const inputRef = useRef(null);
  const fileInputRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    // 直接获取 DOM 值
    console.log('Input value:', inputRef.current.value);
    console.log('File:', fileInputRef.current.files[0]);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={inputRef} type="text" defaultValue="Initial" />
      <input ref={fileInputRef} type="file" />
      <button type="submit">Submit</button>
    </form>
  );
}

触发强制动画

function AnimatedBox() {
  const boxRef = useRef(null);

  const animate = () => {
    const box = boxRef.current;
    box.style.transition = 'transform 0.5s';
    box.style.transform = 'translateX(100px)';

    setTimeout(() => {
      box.style.transform = 'translateX(0)';
    }, 500);
  };

  return (
    <>
      <div ref={boxRef} style={{ width: 50, height: 50, background: 'red' }} />
      <button onClick={animate}>Animate</button>
    </>
  );
}

集成第三方 DOM 库

function ChartComponent({ data }) {
  const chartRef = useRef(null);
  const chartInstance = useRef(null);

  useEffect(() => {
    // 初始化第三方图表库
    chartInstance.current = new Chart(chartRef.current, {
      type: 'line',
      data: data
    });

    return () => {
      // 清理
      chartInstance.current.destroy();
    };
  }, []);

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

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

3. Ref 转发(forwardRef)

import React, { forwardRef } from 'react';

// 函数组件默认不能接受 ref,需要使用 forwardRef
const FancyInput = forwardRef((props, ref) => {
  return <input ref={ref} className="fancy-input" {...props} />;
});

// 使用
function Parent() {
  const inputRef = useRef(null);

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

  return (
    <>
      <FancyInput ref={inputRef} placeholder="Enter text" />
      <button onClick={focusInput}>Focus Input</button>
    </>
  );
}

4. useImperativeHandle 自定义暴露内容

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

const FancyInput = 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 Parent() {
  const fancyInputRef = useRef();

  const handleClick = () => {
    fancyInputRef.current.focus();
    console.log(fancyInputRef.current.getValue());
    // fancyInputRef.current.clear();
  };

  return (
    <>
      <FancyInput ref={fancyInputRef} />
      <button onClick={handleClick}>Interact</button>
    </>
  );
}

5. 在类组件中使用 Ref

// 父组件获取子组件实例
class Parent extends React.Component {
  childRef = React.createRef();

  handleClick = () => {
    // 调用子组件方法
    this.childRef.current.customMethod();
  };

  render() {
    return (
      <>
        <ChildComponent ref={this.childRef} />
        <button onClick={this.handleClick}>Call Child Method</button>
      </>
    );
  }
}

class ChildComponent extends React.Component {
  customMethod = () => {
    console.log('Method called from parent');
  };

  render() {
    return <div>Child Component</div>;
  }
}

深入理解

Ref 的更新时机

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

  console.log('During render:', ref.current); // null

  useEffect(() => {
    console.log('In useEffect:', ref.current); // DOM node
  });

  useLayoutEffect(() => {
    console.log('In useLayoutEffect:', ref.current); // DOM node
  });

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

Ref 作为可变容器

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

  // 保存最新的 count 值,不触发重渲染
  countRef.current = count;

  const startTimer = () => {
    timerRef.current = setInterval(() => {
      // 可以访问到最新的 count
      console.log(countRef.current);
    }, 1000);
  };

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

  useEffect(() => {
    return () => stopTimer();
  }, []);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

最佳实践

1. 不要过度使用 Ref

// 不推荐:用 ref 控制组件状态
function BadExample() {
  const inputRef = useRef();

  const getValue = () => {
    return inputRef.current.value; // 非受控模式
  };
}

// 推荐:使用受控组件
function GoodExample() {
  const [value, setValue] = useState('');

  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  );
}

2. 清理工作

function ResizableComponent() {
  const elementRef = useRef(null);
  const observerRef = useRef(null);

  useEffect(() => {
    observerRef.current = new ResizeObserver((entries) => {
      // 处理尺寸变化
    });

    observerRef.current.observe(elementRef.current);

    return () => {
      // 清理
      observerRef.current.disconnect();
    };
  }, []);

  return <div ref={elementRef}>Resizable content</div>;
}

3. 避免在渲染期间访问 ref

function BadExample() {
  const myRef = useRef(null);

  // 错误:在渲染期间读取 ref
  const value = myRef.current?.value;

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

function GoodExample() {
  const myRef = useRef(null);
  const [value, setValue] = useState('');

  useEffect(() => {
    // 正确:在 effect 中读取 ref
    setValue(myRef.current?.value);
  }, []);

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

应用场景总结

场景 示例
管理焦点 自动聚焦输入框
文本选择 高亮选中文本
媒体控制 播放/暂停视频
动画触发 强制触发动画
第三方集成 集成图表、地图库
测量 DOM 获取元素尺寸位置
保存可变值 保存定时器 ID、上一次的值

面试要点

  1. ref 的用途:访问 DOM、获取组件实例、保存可变值
  2. 创建方式:createRef、useRef、回调 ref
  3. ref 转发:forwardRef 让函数组件接受 ref
  4. useImperativeHandle:自定义暴露给父组件的内容
  5. 注意事项:不要滥用 ref,优先使用 props 和 state

常见追问:

  • ref 和 state 有什么区别?
  • 为什么函数组件需要使用 forwardRef?
  • useImperativeHandle 的作用是什么?
  • ref 在渲染期间可以访问吗?