網站搜尋

reactjs - 在 React.forwardRef 中使用 ref.current


Codesandbox 在這裡

我正在嘗試使用父組件的 ref 來監聽附加在子組件上的 ref 的某些事件,該 ref 是使用 React.forwardRef 附加到子組件上的。然而,當我在子組件中引用 ref.current 時,出現了 lint 警告,內容如下:

屬性 'current' 在類型 'Ref' 上不存在。屬性 'current' 在類型 '(instance: HTMLDivElement) => void' 上不存在。

我應該如何在一個 React.forwardRef 元件中引用一個 ref?謝謝。

index.tsx:

import * as React from "react";
import ReactDOM from "react-dom";

const Component = React.forwardRef<HTMLDivElement>((props, ref) => {
  React.useEffect(() => {
    const node = ref.current;
    const listen = (): void => console.log("foo");

    if (node) {
      node.addEventListener("mouseover", listen);
    }
    return () => {
      node.removeEventListener("mouseover", listen);
    };
  }, [ref]);

  return <div ref={ref}>Hello World</div>;
});

export default Component;

const App: React.FC = () => {
  const sampleRef = React.useRef<HTMLDivElement>(null);

  return <Component ref={sampleRef} />;
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

答案#1

Refs 不一定是具有 current 屬性的物件。它們也可以是函數。因此,類型錯誤指出你可能會被傳遞後者之一。你需要編寫你的程式碼,使其能夠處理這兩種變化。

這可能有點棘手,但還是可行的。我們的 effect 不能依附在傳入的函數上,因為那個函數可能做任何事情,而且在設計時並沒有考慮到我們的 useEffect。所以我們需要創建我們自己的 ref,我會稱它為 myRef。

此時現在有兩個 ref:一個是傳入的,另一個是我們創建的本地 ref。要同時填充它們,我們需要自己使用 ref 的函數形式,在該函數中我們可以將 div 元素分配給這兩個 ref:

const Component = React.forwardRef<HTMLDivElement>((props, ref) => {
  const myRef = useRef<HTMLDivElement | null>(null);
  React.useEffect(() => {
    const node = myRef.current;
    const listen = (): void => console.log("foo");

    if (node) {
      node.addEventListener("mouseover", listen);
      return () => {
        node.removeEventListener("mouseover", listen);
      };
    }
  }, [ref]);

  return (
    <div ref={(node) => {
      myRef.current = node;
      if (typeof ref === 'function') {
        ref(node);
      } else if (ref) {
        ref.current = node;
      }
    }}>Hello World</div>
  );
});

答案#2

這個討論串中的所有回答都做得過頭了。你只需要使用 useImperativeHandle 鉤子來合併 refs 就可以了。

type Props = { id: string };

export const Component = forwardRef<HTMLInputElement, Props>((props, forwardedRef) => {
  const ref = useRef<HTMLInputElement>(null);

  useImperativeHandle(forwardedRef, () => ref.current as HTMLInputElement);

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

Component.displayName = 'Component';

答案#3

我四處搜索過,因為這是一個適合使用另一個 hook 的好候選,例如 useForwardRef。這是提案,https://github.com/facebook/react/issues/24722

我自己也試了,對這個用途效果非常好。

const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(
  (props, ref) => {
   const inputRef = useForwardRef<HTMLInputElement>(ref);
   const onLabelClick = () => {
    inputRef.current?.focus();
  };

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

當然,這本質上是最初答案中的相同程式碼,只是寫成一個 hook。

const useForwardRef = <T,>(
  ref: ForwardedRef<T>,
  initialValue: any = null
) => {
  const targetRef = useRef<T>(initialValue);

  useEffect(() => {
    if (!ref) return;

    if (typeof ref === 'function') {
      ref(targetRef.current);
    } else {
      ref.current = targetRef.current;
    }
  }, [ref]);

  return targetRef;
};

注意:這個 hook 的名稱是有爭議的,它可以被命名為 useCopyRef、useBorrowRef 等等。這裡為了簡單起見,因為它是為了 forwardRef 的目的而創建的,我們將它命名為 useForwardRef,但實際上它與 forward 沒有任何關係。

答案#4

詳細說明 @Nicholas 的回答:

import React, { MutableRefObject, Ref, useEffect } from "react";
import { TextInput } from "react-native";

type CustomTextInputProps = {};

export const CustomTextInput = React.forwardRef<
  TextInput,
  CustomTextInputProps
>((props, ref) => {
  const localRef = React.useRef<TextInput | null>(null);

  useEffect(() => {
    // using the local ref
    localRef.current?.focus();
  }, []);

  return <TextInput {...props} ref={assignRefs(localRef, ref)} />;
});

const assignRefs = <T extends unknown>(...refs: Ref<T | null>[]) => {
  return (node: T | null) => {
    refs.forEach((r) => {
      if (typeof r === "function") {
        r(node);
      } else if (r) {
        (r as MutableRefObject<T | null>).current = node;
      }
    });
  };
};