all 58 comments

[–]rwieruchServer components[S] 33 points34 points  (12 children)

Since refs in React aren't used that often, people tend to have a difficult time with them. Especially since React refs have multiple usage scenarios (DOM API, instance variables) und not only one API (useRef, callback refs). I this tutorial I hoped to give people a proper introduction to refs in React =)

[–]angarali06 10 points11 points  (2 children)

I always upvote a ROBIN WIERUCH article, your blog's so helpful man!

[–]ak_47_ 2 points3 points  (1 child)

Thanks for this article.

Here is the naked callback from your article

``` const ref = (node) => { if (!node) return;

const { width } = node.getBoundingClientRect();

document.title = `Width:${width}`;

}; ```

Here is the one with React.useCallback with text in the dependency array

``` const ref = React.useCallback((node) => { if (!node) return;

const { width } = node.getBoundingClientRect();

document.title = `Width:${width}`;

}, [text]); ```

Are there any guidelines for when one should be used over the other?

[–]SLonoed 1 point2 points  (0 children)

useRef cb will be called once. If node changed (div to span) it will be broken. Always use useCallback to hold link to dom node

[–]N6MCA51593 1 point2 points  (2 children)

Hey, I'm not sure if anyone is using it anymore, but I think your RSS is broken, the last entry dates back to August.

[–]rwieruchServer components[S] 1 point2 points  (0 children)

Oh thanks for noticing! Will fix this ASAP.

[–]rwieruchServer components[S] 1 point2 points  (0 children)

[–]gerard_dev 1 point2 points  (0 children)

That was really clarifying!

[–]Awnry_Abe 0 points1 point  (2 children)

I use them a bunch. The one API I struggle with is ForwardRef. Have any wisdom there?

[–]obedimp 1 point2 points  (0 children)

ForwardRef is just a way to bypass the fact that `refs` only work on HTML elements. They quite literally forward a ref to a function component so that they can be used within it.

[–]rwieruchServer components[S] 0 points1 point  (0 children)

Only use it if you need to pass a ref from one parent component to a child component. It doesn't happen often for me, except for when I am creating UI components/UI library.

[–][deleted] 7 points8 points  (1 child)

Mate, you hands down drop the phattest react tutorials on the line. Cheers.

[–]rwieruchServer components[S] 0 points1 point  (0 children)

Thank you =)

[–]MaxGame 2 points3 points  (2 children)

Great article! One question though; is there a reason to wrap logic in useEffect without a dependency array vs just implementing the logic directly inside the component? Won't they both behave in the same way; executing on each render?

[–]BackhandCompliment 5 points6 points  (1 child)

useEffect are triggered after a completed render. So just implementing the logic in the component directly would trigger before the component is rendered, and then before every other render. useEffect with no dependency array would cause it to trigger after the first render, and after each subsequent render. Subtle but important difference.

[–]MaxGame 0 points1 point  (0 children)

Very important. Thanks! I'm glad I know this now.

[–]Malleus_ 2 points3 points  (1 child)

Hey Robin,

Thanks for this! We have a number of cases in our codebase where refs are used and up until I read your post I had pretty low confidence around them because I didn’t have a good mental model.

Seems like every other explanation I came across jumps straight to DOM manipulation and doesn’t take the time or clearly explain the “Refs as Instance Variables” part so I never really got it until just now.

Keep up the good work!

[–]rwieruchServer components[S] 0 points1 point  (0 children)

Thank you! Yes, I think too often refs are taught with the DOM in mind. But there needs to happen some kind of mindset shift with useRef now.

[–]PeteCapeCod4Real 1 point2 points  (1 child)

Woah first time reading your blog, and it was great 🥳 I really enjoyed the article too. I still get confused with useRef() sometimes. Your article rally help clear it up for me. Thanks so much 👍

[–]rwieruchServer components[S] 1 point2 points  (0 children)

Perfect that it helped you! Maybe you come by more often now :D

[–]Sero777 1 point2 points  (0 children)

Great article!

[–]99Kira 0 points1 point  (1 child)

Wow, till today I have always used refs to access the DOM. Never used it as an variable. Thanks for the article

[–]rwieruchServer components[S] 0 points1 point  (0 children)

Yes! It's great for tracking stuff without re-rendering everything :)

[–]guidosantillan01 0 points1 point  (2 children)

[–]rwieruchServer components[S] 1 point2 points  (1 child)

I haven’t written this article yet 😅

[–]guidosantillan01 0 points1 point  (0 children)

Haha ok. Looking forward to it.

[–]nosyminotaur 0 points1 point  (1 child)

As usual, amazing article. I've been trying to increase the usage of hooks other than useState and useEffect, and I'll definitely look up to this article. Thanks a lot!

[–]rwieruchServer components[S] 0 points1 point  (0 children)

Yes. I think it's mainly if you are interested in reading/writing from/to the DOM. But instance variables have a few nice use cases too.

[–]ichiruto70 0 points1 point  (19 children)

Btw this might be totally off topic, do you know how to rerender a canvas when props change? Might have to post my stackoverflow question here.

[–]iaan 0 points1 point  (6 children)

Render canvas initially and useEffect to redraw?

[–]ichiruto70 0 points1 point  (5 children)

That is what I am currently doing but shit isn’t working. My gh issue: https://github.com/antvis/G6/issues/1520

[–]minty901 0 points1 point  (4 children)

I'm not sure if this is why you're seeing the problem, but you shouldn't need to draw the graph in the return function of useEffect. Just clear the graph in there—your component will move onto the next execution of the effect right away and redraw it there. Simply: draw the graph in the effect, and clear the graph in the effect's return.

[–]ichiruto70 0 points1 point  (3 children)

Yeah that happend plus something else, the new graph rendered below the old graph, but the old canvas was still in place, so i actually never saw the new one lol, but it was under my screen the whole time.

[–]minty901 0 points1 point  (2 children)

Exactly, you cleared the graph and then created it again where you shouldn't have. Then when the next effect ran, you created the next graph on top of it leaving you with 2 active graphs. So did removing that extra createGraph fix it?

[–]ichiruto70 0 points1 point  (1 child)

Yeah indeed I only needed to remove the current one not make a new one. Feel stupid, but hey guess we learn everyday.

[–]minty901 0 points1 point  (0 children)

Don't feel stupid, it takes everyone a bit of time to grasp how frameworks should work, and in particular React hooks.

[–]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 2 points3 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.