React ForwardRef:属性 'current' 在类型 'ForwardedRef<HTMLElement>' 上不存在

React ForwardRef: Property 'current' does not exist on type 'ForwardedRef<HTMLElement>'

我正在尝试创建一个组件来跟踪垂直滚动。问题是——实际的滚动容器不容易预测(在这种特定情况下,它既不是 windowdocument 也不是 body——它是 div#__next,因为 CSS 溢出规则).

我想保持组件的灵活性和 self-contained。所以我创建了一个 ref 和 DOM 选择器作为参数。我知道这远非惯用语(至少可以说),但令人惊讶的是它似乎有效:

// Parent component
import { useRef } from "react"

const Article = (props) => {
  const scrollContainerRef = useRef<HTMLElement | null>(
    document.querySelector("#__next") // <-- the scroll container reference
  )

  return (
    <SomeContent>
      <ScrollToTop treshold={640} ref={scrollContainerRef} />
    </SomeContent>
)
// ScrollToTop
const ScrollToTop = forwardRef(
  ({ treshold }, ref) => {
    const [visible, setVisible] = useState(false)

    useEffect(() => {

      if (ref?.current) {
        ref.current.addEventListener("scroll", throttle(toggleVisible, 300))
        return () => {
          ref.current.removeEventListener("scroll", throttle(toggleVisible, 300))
        }
      }
    }, [])
// …

所以问题是什么?当前的是 Typescript。我花了几个小时试图让类型正确,但无济于事。 parent 组件是免费的红色波浪线(除非我通过 globalThis,这似乎至少在 CodeSandbox 中有效),但是每当我访问 currentScrollToTop 都在抱怨属性:

Property 'current' does not exist on type 'ForwardedRef<HTMLElement>'.

我尝试在 parent 和 child 中使用 React.MutableRefObject<HTMLElement | null /* or other T's */>,但没有用。

知道如何让类型匹配吗?或者这从一开始就是一个愚蠢的想法?

CodeSandbox demo

Refs 可能 是具有 .current 属性 的对象,但它们也可能是函数。所以你不能假设转发的 ref 有 .current 属性.

我认为在这里使用 forwardRef 是错误的。 forwardRef 的目的是允许父组件访问子组件中的元素。但是相反,父级是找到元素的那个,然后您将它传递给子级以供其使用。我会为此使用常规状态和道具:

const Article = (props) => {
  const [scrollContainer, setScrollContainer] = useState<HTMLElement | null>(() => {
    return document.querySelector("#__next");
  });

  return (
    <SomeContent>
      <ScrollToTop treshold={640} scrollContainer={scrollContainer} />
    </SomeContent>
)

interface ScrollToTopProps {
  treshold: number;
  scrollContainer: HTMLElement | null;
}

const ScrollToTop = ({ treshold, scrollContainer }: ScrollToTopProps) => {
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    if (scrollContainer) {
      const toggle = throttle(toggleVisible, 300);
      scrollContainer.addEventListener("scroll", toggle);
      return () => {
        scrollContainer.removeEventListener("scroll", toggle);
      }
    }
  }, [scrollContainer]);
  // ...
}