You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在TypeScript中将forwardRef的ref传入自定义Hook?

解决useIntersection Hook与forwardRef配合的TypeScript类型问题

我实现了一个useIntersection Hook用于监听元素交叉状态,但在和forwardRef配合使用时遇到TypeScript类型错误。

原Hook代码:

import { MutableRefObject, useEffect } from 'react';

export default function useIntersection<T extends HTMLElement, U extends HTMLElement>(ref: MutableRefObject<T>, nodeCallback: () => U) {
  useEffect(() => {
    function callback([entry]) {
      ref.current.style.opacity = entry.isIntersecting ? '0' : '';
    }
    const node = nodeCallback();
    const observer = new IntersectionObserver(callback, {
      root: node
    });
    observer.observe(ref.current);
    return () => observer.unobserve(ref.current);
  }, [ref.current]);
}

配合forwardRef使用的组件代码:

import { forwardRef } from 'react';

const DownArrow = require('@site/static/img/down-arrow.svg').default;
import styles from './index.module.css';
import useIntersection from '@site/src/hooks/useIntersection';

export default forwardRef<HTMLDivElement, {}>(function MoreIcon({}, ref): JSX.Element {
  useIntersection(ref, () => document.querySelector('footer'));
  return (
    <div className={styles.arrow} ref={ref}>
      <DownArrow />
    </div>
  );
});

遇到的错误:

Argument of type 'ForwardedRef' is not assignable to parameter of type 'MutableRefObject'.
Type '(instance: HTMLDivElement) => void' is not assignable to type 'MutableRefObject'.


核心问题

forwardRef传递的ref参数类型是ForwardedRef<T>,它包含两种可能的类型:

  • MutableRefObject<T | null>:对象类型的ref
  • (instance: T | null) => void:回调类型的ref

而原Hook只接受MutableRefObject<T>,自然会出现类型不兼容的错误。

修改后的Hook代码

我们需要让Hook兼容两种ref类型,同时处理元素可能为null的边界情况:

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

export default function useIntersection<T extends HTMLElement, U extends HTMLElement>(
  ref: Ref<T>,
  nodeCallback: () => U | null
) {
  // 内部ref统一存储目标元素,适配两种ref类型
  const targetRef = useRef<T | null>(null);

  useEffect(() => {
    // 处理传入的ref:如果是回调类型,绑定到内部ref;如果是对象类型,直接取current
    if (typeof ref === 'function') {
      ref((element) => {
        targetRef.current = element;
      });
    } else {
      targetRef.current = ref?.current ?? null;
    }

    const targetElement = targetRef.current;
    if (!targetElement) return;

    const rootNode = nodeCallback();
    const observer = new IntersectionObserver(([entry]) => {
      targetElement.style.opacity = entry.isIntersecting ? '0' : '';
    }, { root: rootNode });

    observer.observe(targetElement);

    // 清理函数:取消监听,若为回调ref则清空
    return () => {
      observer.unobserve(targetElement);
      if (typeof ref === 'function') {
        ref(null);
      }
    };
  }, [ref, nodeCallback]);
}

修改后的组件使用

组件中只需确保nodeCallback的返回值类型正确(处理document.querySelector可能返回null的情况):

import { forwardRef } from 'react';

const DownArrow = require('@site/static/img/down-arrow.svg').default;
import styles from './index.module.css';
import useIntersection from '@site/src/hooks/useIntersection';

export default forwardRef<HTMLDivElement, {}>(function MoreIcon({}, ref) {
  useIntersection(ref, () => document.querySelector('footer') as HTMLElement | null);
  return (
    <div className={styles.arrow} ref={ref}>
      <DownArrow />
    </div>
  );
});

关键优化点

  1. 类型兼容:改用Ref<T>作为参数类型,覆盖对象ref和回调ref两种场景
  2. 边界处理:判断目标元素是否存在,避免调用observe时传入null
  3. 依赖优化:useEffect依赖数组使用ref而非ref.current,因为ref本身是稳定引用
  4. 内存清理:在卸载时取消监听,同时清理回调ref的引用

内容的提问来源于stack exchange,提问作者jcubic

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 12:03:45