React Refs with TypeScript: Cannot read property 'current' of undefined

J. Hesters picture J. Hesters · Nov 10, 2018 · Viewed 12k times · Source

I'm building a React application using TypeScript.

I want to create button, that scrolls to a header of a child component on my main page.

I've created a ref in the child component, following this stack overflow answer and (tried to) use forward refs to access it on my parent component.

export class Parent extends Component {
  private testTitleRef!: RefObject<HTMLHeadingElement>;

  scrollToTestTitleRef = () => {
    if (this.testTitleRef.current !== null) {
      window.scrollTo({
        behavior: "smooth",
        top: this.testTitleRef.current.offsetTop
      });
    }
  };

  render() {
    return <Child ref={this.testTitleRef} />
  }
}

interface Props {
  ref: RefObject<HTMLHeadingElement>;
}

export class Child extends Component<Props> {
  render() {
    return <h1 ref={this.props.ref}>Header<h1 />
  }
}

Unfortunately when I trigger scrollToTestTitleRef I get the error:

Cannot read property 'current' of undefined

Meaning that the ref is undefined. Why is that? What am I doing wrong?

EDIT: Estus helped me to create the ref. But when I trigger the scrollToTestTitleRef() event, it doesn't scroll. When I console.log this.testTitleRef.current I get the output:

{"props":{},"context":{},"refs":{},"updater":{},"jss":{"id":1,"version":"9.8.7","plugins":{"hooks":{"onCreateRule":[null,null,null,null,null,null,null,null,null,null,null,null],"onProcessRule":[null,null,null],"onProcessStyle":[null,null,null,null,null,null],"onProcessSheet":[],"onChangeValue":[null,null,null],"onUpdate":[null]}},"options":{"plugins":[{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}]}},"sheetsManager":{},"unsubscribeId":null,"stylesCreatorSaved":{"options":{"index":-99999999945},"themingEnabled":false},"sheetOptions":{},"theme":{},"_reactInternalInstance":{},"__reactInternalMemoizedUnmaskedChildContext":{"store":{},"storeSubscription":null},"state":null}

Note: I deleted the keys of cacheClasses, _reactInternalFiber and __reactInternalMemoizedMaskedChildContext, because they contained cyclic dependencies.

So current doesn't seem to have a key of offsetTop. Does this maybe have something to do with the fact that in my real application the child component is wrapped inside material-ui's withStyle and React-Redux' connect?

Answer

Estus Flask picture Estus Flask · Nov 10, 2018

! non-null assertion operator suppresses the actual problem. There is no way in JavaScript/TypeScript how testTitleRef property could be assigned from being used as <Child ref={this.titleRef} />, so it stays undefined (there's also inconsistency with testTitleRef and titleRef).

It should be something like:

  private testTitleRef: React.createRef<HTMLHeadingElement>();

  scrollToTestTitleRef = () => {
      if (!this.testTitleRef.current) return;

      window.scrollTo({
        behavior: "smooth",
        top: this.testTitleRef.current.getBoundingClientRect().top + window.scrollY
      });
  };
  render() {
    return <Child scrollRef={this.testTitleRef} />
  }

and

export class Child extends Component<Props> {
  render() {
    return <h1 ref={this.props.scrollRef}>Header<h1 />
  }
}