React 的 ForwardedRef 和 RefObject 有什么区别?

Yeh*_*ira 17 typescript reactjs react-typescript

在我正在开发的 React 代码库中,我有一个自定义钩子,它接受 RefObject 作为参数,以及与此类钩子一起使用的随附提供程序:

export const ScrollUtilsProvider = React.forwardRef<HTMLDivElement, ScrollUtilsProviderProps>(
  (props, ref) => {
    const scrollUtils = useScrollUtils(ref) // issue happens on this line
    return <div ref={ref}><ScrollUtilsContext.Provider value={scrollUtils}>{props.children}</ScrollUtilsContext.Provider></div>
  },
)

export const useScrollUtils = <T extends Element>(ref: RefObject<T>) => {
  return {
    // some cool functions w/ the passed ref
  }
}
Run Code Online (Sandbox Code Playgroud)

我收到的错误消息:

Argument of type 'ForwardedRef<HTMLDivElement>' is not assignable to parameter of type 'RefObject<HTMLDivElement>'.
  Type 'null' is not assignable to type 'RefObject<HTMLDivElement>'.
Run Code Online (Sandbox Code Playgroud)

深入研究这两种类型,我意识到它们确实不同:

Argument of type 'ForwardedRef<HTMLDivElement>' is not assignable to parameter of type 'RefObject<HTMLDivElement>'.
  Type 'null' is not assignable to type 'RefObject<HTMLDivElement>'.
Run Code Online (Sandbox Code Playgroud)

我的问题是:

  • 为什么这些类型如此不同?
  • 有没有办法将 ForwardedRef 用作 RefObject?
  • 如果没有,是否有什么东西让 ForwardedRefs 如此特别?

rpa*_*tel 29

引用对象

interface RefObject<T> {
    readonly current: T | null;
}
Run Code Online (Sandbox Code Playgroud)

RefObject是方法的返回类型React.createRef

调用此方法时,它会返回一个对象,其唯一字段.current设置为null。不久之后,当 arender将 ref 传递给组件时,React 将设置.current为对该组件的引用。该组件通常是 DOM 元素(如果传递给 HTML 元素)或类组件的实例(如果传递给自定义类组件)。

请注意,RefObject与 非常相似MutableRefObject<T | null>,但.currentreadonly。此类型规范仅用于指示该.current属性由 React 内部管理,不应由 React 应用程序中的代码修改。

可变引用对象

interface MutableRefObject<T> {
    current: T;
}
Run Code Online (Sandbox Code Playgroud)

MutableRefObject是方法的返回类型React.useRef。在内部,React.useRef创建 a MutableRefObject,将其存储到功能组件的状态,然后返回该对象。

请注意,当对象存储到 React 组件的状态时,修改其属性不会触发重新渲染(因为 Javascript 对象是引用类型)。这种情况允许您在没有实例的功能组件中模仿类实例变量。换句话说,您可以将其视为React.useRef一种将变量与功能组件关联起来而不影响组件渲染的方法。

React.useRef下面是使用实例变量的类组件和用于实现相同目的的函数组件的示例:

class ClassTimer extends React.Component {
    interval: NodeJS.Timer | null = null;

    componentDidMount() {
        this.interval = setInterval(() => { /* ... */ });
    }

    componentWillUnmount() {
        if (!this.interval) return;
        clearInterval(this.interval);
    }

    /* ... */
}

function FunctionalTimer() {
    const intervalRef = React.useRef<NodeJS.Timer>(null);

    React.useEffect(() => {
        intervalRef.current = setInterval(() => { /* ... */ });
        return () => {
            if (!intervalRef.current) return;
            clearInterval(intervalRef.current);
        };
    }, []);

    /* ... */
}
Run Code Online (Sandbox Code Playgroud)

转发引用

type ForwardedRef<T> = 
    | ((instance: T | null) => void)
    | MutableRefObject<T | null> 
    | null;
Run Code Online (Sandbox Code Playgroud)

ForwardedRef是 React 使用 传递给功能组件的 ref 类型React.forwardRef

这里的主要思想是父组件可以将 ref 传递给子组件。例如,MyForm可以将 ref 转发到MyTextInput,允许前者访问后者.value呈现的HTMLInputElement

分解联合类型:

  • MutableRefObject<T | null>- 转发的参考是使用 创建的React.useRef

  • ((instance: T | null) => void)- 转发的 ref 是回调 ref

  • null- 没有转发任何裁判。

在子组件中使用 ForwardedRef

当子组件接收到 时ForwardedRef,通常会将 ref 暴露给父组件。但是,有时子组件可能需要使用 ref 本身。在这种情况下,您可以使用挂钩来协调ForwardedRef上面列出的每种类型。

这是本文中的一个钩子(针对 Typescript 进行了调整),可以帮助实现这一目标:

function useForwardedRef<T>(ref: React.ForwardedRef<T>) {
    const innerRef = React.useRef<T>(null);

    React.useEffect(() => {
        if (!ref) return;
        if (typeof ref === 'function') {
            ref(innerRef.current);
        } else {
            ref.current = innerRef.current;
        }
    });

    return innerRef;
}
Run Code Online (Sandbox Code Playgroud)

这个钩子背后的想法是组件可以创建自己的引用,无论父组件是否转发引用,它都可以使用该引用。该钩子有助于确保任何转发的引用的.current属性与内部的属性保持同步。

该钩子的返回类型是MutableRefObject<T>,它应该与RefObject<T>代码片段中的参数兼容useScrollUtils,例如:

const MyComponent = React.forwardRef<HTMLDivElement>(
    function MyComponent(_props, ref) {
        const innerRef = useForwardedRef(ref);

        useScrollUtils(innerRef);

        return <div ref={innerRef}></div>;
    }
);
Run Code Online (Sandbox Code Playgroud)