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

2

7

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

Saransarangi answered 12/1, 2022 at 14:20 Comment(2)
Here is an excellent cheatsheet on React and TypeScript for Refs. There are some differences to your code lite React.createRef<HTMLDivElement> instead of<HTMLElement>. Don't know what element type your scrollContainerRef has. react-typescript-cheatsheet.netlify.app/docs/basic/…Ecg
@bödvar Thank you. I am aware of the React/TypeScript cheat sheet. The scrollContainerRef is shown to be React.MutableRefObject<HTMLElement> when hovering over. But if I try to use that in the child component, I got errors.Saransarangi
M
0

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

Materialist answered 12/1, 2022 at 14:35 Comment(4)
Thank you! I'll give it a try. I probably want to use ref to add flexibility (use whatever parent send), but it probably doesn't matter.Saransarangi
Thank you. It works like a charm without any TS complaints.Saransarangi
Technically you answered the posters question, but you didn't explain how to fix the type error, which means your answer is helpful to nobody else but the poster.Catnap
@JohnMiller thank you for the feedback. Feel free to edit the answer to improve it.Materialist
D
4

When using forwardRef the type of the ref argument is:

type ForwardedRef<T> = ((instance: T | null) => void) | MutableRefObject<T | null> | null;

Meaning the ref can be (1) a function, (2) a mutable object with current property or (3) null.

If you are sure your instance is MutableRefObject just use casting.

const MyInput = forwardRef<HTMLInputElement, MyProps>((props, ref) => {
  ...
  const inputRef = ref as MutableRefObject<HTMLInputElement>;
  const inputText = ref.current.value;
  ...

  return <Input ref={ref} ...>;
});

P.S Please note that using as isn't type safe, so if you aren't sure what is the type of ref, some type checking is required.

Darby answered 13/8, 2023 at 19:47 Comment(0)
M
0

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

Materialist answered 12/1, 2022 at 14:35 Comment(4)
Thank you! I'll give it a try. I probably want to use ref to add flexibility (use whatever parent send), but it probably doesn't matter.Saransarangi
Thank you. It works like a charm without any TS complaints.Saransarangi
Technically you answered the posters question, but you didn't explain how to fix the type error, which means your answer is helpful to nobody else but the poster.Catnap
@JohnMiller thank you for the feedback. Feel free to edit the answer to improve it.Materialist

© 2022 - 2024 — McMap. All rights reserved.