React Ref 指南:原理、实现与实践

发布于:2025-07-07 ⋅ 阅读:(20) ⋅ 点赞:(0)

前言

React Ref(引用)是React中一个强大而重要的概念,它为我们提供了直接访问DOM元素或组件实例的能力。虽然React推崇声明式编程和数据驱动的理念,但在某些场景下,我们仍需要直接操作DOM或访问组件实例。本文将深入探讨React Ref的工作原理、使用方法和最佳实践。

什么是React Ref?

React Ref是一个可以让我们访问DOM节点或在render方法中创建的React元素的方式。它本质上是一个对象,包含一个current属性,用于存储对真实DOM节点或组件实例的引用。

为什么需要Ref?

在React的声明式编程模型中,数据流是单向的:props向下传递,事件向上冒泡。但在以下场景中,我们需要直接访问DOM或组件:

  • 管理焦点、文本选择或媒体播放
  • 触发强制动画
  • 集成第三方DOM库
  • 测量DOM元素的尺寸
  • 访问子组件的方法

Ref的演进历史

1. String Refs(已废弃)

// 不推荐使用
class MyComponent extends React.Component {
  componentDidMount() {
    this.refs.myInput.focus();
  }
  
  render() {
    return <input ref="myInput" />;
  }
}

String Refs存在性能问题和潜在的内存泄漏风险,已在React 16.3中被废弃。

2. Callback Refs

class MyComponent extends React.Component {
  setInputRef = (element) => {
    this.inputElement = element;
  }
  
  componentDidMount() {
    if (this.inputElement) {
      this.inputElement.focus();
    }
  }
  
  render() {
    return <input ref={this.setInputRef} />;
  }
}

3. createRef(React 16.3+)

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  
  componentDidMount() {
    this.inputRef.current.focus();
  }
  
  render() {
    return <input ref={this.inputRef} />;
  }
}

4. useRef Hook(React 16.8+)

function MyComponent() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  
  return <input ref={inputRef} />;
}

深入理解useRef

useRef的基本用法

useRef返回一个可变的ref对象,其.current属性被初始化为传入的参数。

const refContainer = useRef(initialValue);

useRef的特点

  1. 持久化存储:useRef在组件的整个生命周期中保持同一个引用
  2. 不触发重新渲染:修改.current属性不会触发组件重新渲染
  3. 同步更新.current的值会同步更新,不像state那样异步

useRef vs useState

function RefVsState() {
  const [stateValue, setStateValue] = useState(0);
  const refValue = useRef(0);
  
  const updateState = () => {
    setStateValue(prev => prev + 1);
    console.log('State value:', stateValue); // 异步更新,可能显示旧值
  };
  
  const updateRef = () => {
    refValue.current += 1;
    console.log('Ref value:', refValue.current); // 同步更新,显示新值
  };
  
  return (
    <div>
      <p>State: {stateValue}</p>
      <p>Ref: {refValue.current}</p>
      <button onClick={updateState}>Update State</button>
      <button onClick={updateRef}>Update Ref</button>
    </div>
  );
}

Ref的实际应用场景

1. 访问DOM元素

function FocusInput() {
  const inputRef = useRef(null);
  
  const handleFocus = () => {
    inputRef.current.focus();
  };
  
  const handleClear = () => {
    inputRef.current.value = '';
  };
  
  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleFocus}>Focus Input</button>
      <button onClick={handleClear}>Clear Input</button>
    </div>
  );
}

2. 存储可变值

function Timer() {
  const [time, setTime] = useState(0);
  const intervalRef = useRef(null);
  
  const start = () => {
    if (intervalRef.current) return;
    
    intervalRef.current = setInterval(() => {
      setTime(prev => prev + 1);
    }, 1000);
  };
  
  const stop = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  };
  
  useEffect(() => {
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);
  
  return (
    <div>
      <p>Time: {time}</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

3. 保存上一次的值

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

function MyComponent({ count }) {
  const prevCount = usePrevious(count);
  
  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount}</p>
    </div>
  );
}

高级Ref技巧

1. forwardRef

forwardRef允许组件将ref转发到其子组件:

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

function Parent() {
  const inputRef = useRef(null);
  
  const handleFocus = () => {
    inputRef.current.focus();
  };
  
  return (
    <div>
      <FancyInput ref={inputRef} />
      <button onClick={handleFocus}>Focus Input</button>
    </div>
  );
}

2. useImperativeHandle

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

const CustomInput = React.forwardRef((props, ref) => {
  const inputRef = useRef(null);
  
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    scrollIntoView: () => {
      inputRef.current.scrollIntoView();
    },
    getValue: () => {
      return inputRef.current.value;
    }
  }));
  
  return <input ref={inputRef} {...props} />;
});

function Parent() {
  const customInputRef = useRef(null);
  
  const handleAction = () => {
    customInputRef.current.focus();
    console.log(customInputRef.current.getValue());
  };
  
  return (
    <div>
      <CustomInput ref={customInputRef} />
      <button onClick={handleAction}>Focus and Get Value</button>
    </div>
  );
}

3. Ref回调函数

function MeasureElement() {
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
  
  const measureRef = useCallback((node) => {
    if (node !== null) {
      setDimensions({
        width: node.getBoundingClientRect().width,
        height: node.getBoundingClientRect().height
      });
    }
  }, []);
  
  return (
    <div>
      <div ref={measureRef} style={{ padding: '20px', border: '1px solid #ccc' }}>
        Measure me!
      </div>
      <p>Width: {dimensions.width}px</p>
      <p>Height: {dimensions.height}px</p>
    </div>
  );
}

最佳实践与注意事项

1. 避免过度使用Ref

// ❌ 不推荐:过度使用ref
function BadExample() {
  const inputRef = useRef(null);
  const [value, setValue] = useState('');
  
  const handleChange = () => {
    setValue(inputRef.current.value); // 不必要的ref使用
  };
  
  return <input ref={inputRef} onChange={handleChange} />;
}

// ✅ 推荐:使用受控组件
function GoodExample() {
  const [value, setValue] = useState('');
  
  const handleChange = (e) => {
    setValue(e.target.value);
  };
  
  return <input value={value} onChange={handleChange} />;
}

2. 检查ref的有效性

function SafeRefUsage() {
  const elementRef = useRef(null);
  
  const handleClick = () => {
    // 总是检查ref是否有效
    if (elementRef.current) {
      elementRef.current.focus();
    }
  };
  
  return (
    <div>
      <input ref={elementRef} />
      <button onClick={handleClick}>Focus</button>
    </div>
  );
}

3. 清理副作用

function ComponentWithCleanup() {
  const intervalRef = useRef(null);
  
  useEffect(() => {
    intervalRef.current = setInterval(() => {
      console.log('Interval running');
    }, 1000);
    
    // 清理函数
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);
  
  return <div>Component with cleanup</div>;
}

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

// ❌ 不推荐:在渲染期间访问ref
function BadRefUsage() {
  const inputRef = useRef(null);
  
  // 渲染期间访问ref可能为null
  const inputValue = inputRef.current?.value || '';
  
  return <input ref={inputRef} placeholder={inputValue} />;
}

// ✅ 推荐:在effect或事件处理器中访问ref
function GoodRefUsage() {
  const inputRef = useRef(null);
  const [placeholder, setPlaceholder] = useState('');
  
  useEffect(() => {
    if (inputRef.current) {
      setPlaceholder(inputRef.current.value || 'Enter text');
    }
  });
  
  return <input ref={inputRef} placeholder={placeholder} />;
}

性能考虑

1. 使用useCallback优化ref回调

function OptimizedRefCallback() {
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
  
  // 使用useCallback避免不必要的重新渲染
  const measureRef = useCallback((node) => {
    if (node !== null) {
      const rect = node.getBoundingClientRect();
      setDimensions({ width: rect.width, height: rect.height });
    }
  }, []);
  
  return <div ref={measureRef}>Measured content</div>;
}

2. 避免内联ref回调

// ❌ 不推荐:内联ref回调
function InlineRefCallback() {
  const [element, setElement] = useState(null);
  
  return (
    <div ref={(node) => setElement(node)}>
      Content
    </div>
  );
}

// ✅ 推荐:使用useCallback
function OptimizedRefCallback() {
  const [element, setElement] = useState(null);
  
  const refCallback = useCallback((node) => {
    setElement(node);
  }, []);
  
  return <div ref={refCallback}>Content</div>;
}

实际项目示例

自定义Hook:useClickOutside

function useClickOutside(callback) {
  const ref = useRef(null);
  
  useEffect(() => {
    const handleClick = (event) => {
      if (ref.current && !ref.current.contains(event.target)) {
        callback();
      }
    };
    
    document.addEventListener('mousedown', handleClick);
    return () => {
      document.removeEventListener('mousedown', handleClick);
    };
  }, [callback]);
  
  return ref;
}

// 使用示例
function Dropdown() {
  const [isOpen, setIsOpen] = useState(false);
  const dropdownRef = useClickOutside(() => setIsOpen(false));
  
  return (
    <div ref={dropdownRef}>
      <button onClick={() => setIsOpen(!isOpen)}>
        Toggle Dropdown
      </button>
      {isOpen && (
        <div className="dropdown-menu">
          <p>Dropdown content</p>
        </div>
      )}
    </div>
  );
}

网站公告

今日签到

点亮在社区的每一天
去签到