React Refs with TypeScript: Cannot read property 'current' of undefined
!
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 />
}
}
Comments
-
J. Hesters almost 2 years
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 Iconsole.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'swithStyle
and React-Redux'connect
? -
J. Hesters over 5 yearsThank you! The inconsistency was just because I jotted down that examply quickly for this question. I created the ref your way, and it doesn't crash anymore. It still doesn't work. I added more info to the question. Could you please help me further?
-
Estus Flask over 5 yearsI didn't pay attention to Child, but yes, a ref is class instance, not DOM element. You could do
el = ReactDOM.findDOMNode(this.testTitleRef.current)
but actually this may not be a good idea to do this withref
because this wouldn't work at all with function component (they have no refs), also a component may render to multiple DOM elements or no elements at all. I'd suggest to refactor the code to make components decide themselves which element should expose as scroll box, e.g.<Child scrollRef={this.testTitleRef} />
and assignthis.props.scrollRef.current
toh1
ref inside Child. -
Estus Flask over 5 yearsBtw, see stackoverflow.com/a/51828976/3731501 , it already suggests something like this, notice that it passes
refProp
and notref
to a child. -
J. Hesters over 5 yearsThank you for your help! You are helping me tremendously. I switched
ref
torefProp
and set the ref toh1
now it scrolls, but it scrolls to the top of the document, and not to the header of the child. If I logthis.props.scrollRef.current
it correctly gives out the<h1 />
. Do you have any idea what could cause this? -
Estus Flask over 5 yearsCan you provide a demo to debug?
-
J. Hesters over 5 yearsHardly, because the real application has hundres of lines of code. If I strip something it might be what causes the bug. If I console.log
this.props.scrollRef.current.offsetTop
it gives me 32px what is the height of the heading and it's margin, but not the total distance between the element and the top of the window. -
J. Hesters over 5 yearsLet us continue this discussion in chat.
-
Estus Flask over 5 yearsYes, I guess that's the problem here, and you need absolute Y offset. E.g. stackoverflow.com/questions/442404/…
-
J. Hesters over 5 yearsThank you 😊 Here is the code that I ended up using:
this.testTitleRef.current.parentElement!.getBoundingClientRect().top
. Would you mind adding this to your answer so I can accept it? -
Estus Flask over 5 yearsSure. But
parentElement
looks fishy. Why is it needed here? I wouldn't expect it to work ok in the code you posted, it would refer to Parent's container instead of Child's<h1>
. -
J. Hesters over 5 yearsTo be honest that was just try and error until the scroll worked correctly 😅
-
Estus Flask over 5 yearsThis depends on the layout but I'd expect
parentElement
to be harmful here.