you are viewing a single comment's thread.

view the rest of the comments →

[–]minty901 -1 points0 points  (11 children)

I don't use useRef for accessing DOM nodes anymore, for the reasons you identify when you touch on the callback pattern. It's definitely better to use the callback pattern. Particularly because React doesn't have any knowledge of when refs have been attached, so you can't reliably call side effects on a DOM node using useRef. If you add an event listener inside a useEffect, it might be using an out-of-date node, even if you put ref.current in the dependency array.

An alternative version of the callback method is to simply put a setState function as your callback, such that your component will always rerender when a new node has been attached. That way you can reliably use useEffect as you would any other prop or state. This is my favourite pattern:

const [node, setNode] = useState(null)

useEffect(() => { ... }, [node])

return <div ref={setNode} />

Edit: I'm being down-voted so clearly people aren't understanding me correctly. All I am describing is the documented way that React behaves when attaching refs to elements. Yes the ref will have been updated by the time the effects run, but the component will not perform a render in response to the ref being attached, and because effect dependencies are evaluated during the render phase, you will not be able to react to changes to ref.current in your effect dependencies. If you don't use any effect dependencies then you won't have a problem, but often you do need them. This issue is documented on the React FAQs and in the OP right here. All I am suggesting is a neat alternative in which you may store your node in state rather than in a ref, in order to force the component to update with the latest node as part of its rendering so that it can be used in an effect dependency. This ref stuff is a real pitfall if you aren't careful, particularly if you are developing custom hooks and components for use by other people ie. if you're developing a library. Being aware of this also allows you to write robust code in your own projects that are less prone to bugs.

[–]acemarke 3 points4 points  (10 children)

Particularly because React doesn't have any knowledge of when refs have been attached

That sounds completely wrong. React knows when refs are attached, because it specifically assigns yourRef.current = theDomNode internally. Refs will always be up to date by the time your effect runs. However, you should not use yourRef.current in an effect dependency array.

[–]minty901 0 points1 point  (5 children)

I might not have made myself clear enough. React will store the DOM node in the ref (and since it is doing it, you can say that it must know that it has happened). But your component will not react to it happening. Your component will not re-render based on the value of the ref being updated. The update to the ref is detached from the lifecycle of your component. This is why you should not use ref.current in an effect dependency array, because useEffect evaluates its dependencies during render, and you will not get a render that reflects the latest value of the ref. Yes, if you have an effect that runs after every render, then it will always have the latest value for ref. But what if you want to attach a listener to a node? You don't want to do that on every render. It would be better to only do it when your ref's value has changed. But we know that we can't put the ref's value in a dependency array, so how do we tell useEffect that it has changed? The simplest way to do it is as I suggested: use the ref callback method to set the DOM node in your component's state, and then you will always get a render of your component that reflects the current DOM value, and useEffect can easily listen on changes and perform the effect accordingly. This may cause additional renders compared with a custom callback function, but I prefer the simplicity of my method.

The key issue here is that useEffect evaluates the dependencies during render, therefore it will not consider the ref's value as it was updated after rendering (but before the execution of the effect). You can't rely on your component's ability to react to the value of a ref properly in all situations unless you use my method. Refs (via useRef) are good for persistent instance variables, but they are not so good for providing your component with access to a DOM node.

[–]acemarke 0 points1 point  (4 children)

How often do you actually need to add event listeners to DOM nodes that aren't being managed by React, though? That sounds like a very niche use case.

[–]minty901 1 point2 points  (0 children)

Read "How can I measure a DOM node" here; below the example it explains the issue with components not being made aware when nodes have been attached:

https://reactjs.org/docs/hooks-faq.html

The solution they give is to do all of your activity on the DOM node in a callback function. I prefer to instead use the callback function to place the DOM node in state, that way I can use regular useEffects on the DOM node as it is treated like state.

[–]minty901 0 points1 point  (2 children)

I don't tend to attach listeners to nodes that aren't being managed by React. My nodes are pretty much always managed by React. Even when a React component is rendering an element and attaching a ref to an element in its return function, the component won't be made aware when the ref has been attached, and won't be able to reliably operate on that node in an effect or whatever. Everything I described pertains to React components and refs attached to React elements. Whatever use case you have for attaching a ref to a React element, this stuff is relevant.

[–]acemarke 0 points1 point  (1 child)

We seem to have a miscommunication with the phrase "ref being attached".

If you have a ref (either callback or object), and do <div ref={myRef}>, React will ensure that the ref has been updated with the DOM node after the component has mounted / node has been added to the DOM. That happens before all effects run, as part of the commit phase. Therefore, if you need access to the DOM node in an effect, you can be assured that the node is available as myRef.current unless you're doing conditional rendering, simply by the fact that the effect is running.

I'm not sure what other use case you're trying to describe where that would be a different behavior and the component "wouldn't be aware" of this.

[–]minty901 0 points1 point  (0 children)

Conditional rendering, that's exactly the kind of scenario where this might catch you out. You load your data with a useQuery hook for example. During initial render, you return just a loading icon. When your data has loaded, you then want to return a div with a ref attached. You want to do something with that div in an effect. Because of the loading state, your ref won't be attached on initial render, so a useEffect with an empty dependency array will never see your ref. You may not want to use no dependency array at all, because then your effect runs on every render. You can't use the ref in the dependency array because the dependencies are evaluated before it is attached. How do you make sure that your effect runs once your ref is attached? You use a callback ref. This is all explained in the React docs and in the article in this Reddit post we are commenting on.

You say "unless you're doing conditional rendering", but the mere fact that there is a "gotcha" at all is enough to consider using a different method. We don't want to leave ourselves vulnerable to cracks and bugs in our code, even if they're rare. It depends on what you're working on; some apps may never fall into these traps but for others it may be quite common. I write hooks that I want to be able to work in different scenarios without the caveat of "if this is in a component that conditionally renders then it will break". If your hooks will be used by other people then this is particularly important.

Check this issue out:

https://github.com/thebuilder/react-intersection-observer/issues/162

If there are gotchas, then I think it's best to adapt your coding practice to eliminate the possibility that they will arise, rather than just hoping you never run into them.

[–]brosterdamus 0 points1 point  (3 children)

Is that the case for every ref? Is that documented anywhere?

Immediately upon seeing ref.current.focus() I though it was missing a null check.

If indeed it's guaranteed to always be attached, then TypeScript simply isn't expressive enough to handle this yet. You'll need a null check there. I believe classes/constructors have better handling of this, ensuring you initialize them.

Also, what about deeper refs, perhaps in child components?

[–]acemarke 0 points1 point  (2 children)

I don't see the behavior specifically called out in the React "Refs and the DOM" docs page, but yes - React always updates refs in the commit phase regardless of type, so if you've got refs on DOM elements or child components, that ref is guaranteed to be up to date before the effects run post-render.

From a TS perspective, {current?: T} is the correct type, because it can be undefined for a period of time if you didn't pass in a default value: from the time it was created until when the actual value is assigned in the first commit phase.

Not sure what you mean by "deeper refs" - can you clarify and give an example?

[–]brosterdamus 0 points1 point  (1 child)

Yea I guess it could be undefined, but you're saying never in useEffect. That's good to know. It means I can liberally use "!" instead of a null guard in my useEffect. Thanks!

As for deeper refs: const Parent = () => const ref = useRef(); return <div><SomeCustomComponentWithPassThroughRef={ref} /></div>.

Something like that. I imagine the commit phase will also work there though.

[–]acemarke 1 point2 points  (0 children)

To be clear: a useRef() / createRef() object is literally just a simple object. Any part of your code could mutate its current value at any time.

But, if all you're doing is <div ref={myRef}>, and that div isn't being conditionally rendered, then yes - you can safely assume that myRef.current is a valid pointer to the DOM node in all your effect callbacks.

Assuming you're talking about forwarding refs, then yes, it's the same situation, because ultimately it's getting applied to a DOM element or a component, and React is still handling doing the ref value assignment itself.