Search code examples
reactjstypescriptreact-refreact-forwardref

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


I am trying to create a component that will track the vertical scroll. The catch is – the actual scroll container is not easily predictable (in this specific case it is neither window, document nor body – it is div#__next, due to CSS overflow rules).

I want to keep the component flexible and self-contained. So I've created a ref with DOM selector as an argument. I know it is far from idiomatic (to say the least), but it suprisingly seems to be working:

// 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))
        }
      }
    }, [])
// …

So what's the problem? the current one is Typescript. I've spent hours trying to get the types right, but to no avail. The parent component is red squigly lines free (unless I pass globalThis, which seems to work at least in CodeSandbox), but the ScrollToTop is compaining whenever I am accessing current property:

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

I've tried to use React.MutableRefObject<HTMLElement | null /* or other T's */>, both in parent and in child, but it didn't help.

Any ideas how to get the types to match? Or is this a silly idea from the beginning?

CodeSandbox demo


Solution

  • Refs might be objects with a .current property, but they might also be functions. So you can't assume that a forwarded ref has a .current property.

    I think it's a mistake to use forwardRef at all here. The purpose of forwardRef is to allow a parent component to get access to an element in a child component. But instead, the parent is the one finding the element, and then you're passing it to the child for it to use. I would use a regular state and prop for that:

    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]);
      // ...
    }