Manipulate the DOM with React refs

Kent C. Dodds
InstructorKent C. Dodds

Share this video with your friends

Send Tweet

React is really good at creating and updating DOM elements, but sometimes you need to work with them yourself. A common use case for this is when you’re using a third party library that wasn’t built for or with React specifically. To do this, we need to have some value that’s associated with our component (like state) to store a reference to the DOM element, but doesn’t trigger re-renders when it’s updated (unlike state). React has something specifically for this and it’s called a ref.

You create a ref object with the useRef hook and that object’s current property is the current value of the ref. It can be anything, but if you pass that ref object to a component as a prop called ref, then React will set the current property to the DOM element it creates so you can reference it and manipulate it in your useEffect hook.

In this lesson we’ll get to see how that works with a cool library called vanilla-tilt.

Kent C. Dodds: [0:00] We have a function component called Tilt. Thanks to use some handy class names and some handier CSS, we have it styled in this fancy-looking way but we can make it do something fancy by using a library that I've included called vanilla-tilt. Vanilla-tilt takes a DOM node and makes it react to when the user mouses over that DOM node.

[0:19] The DOM node we want to give it is the DOM node that's created for this element, the tilt root. Remember this is a React element, not a DOM node and React takes that React element and renders it to the DOM.

[0:32] We need React to give us the DOM node that creates for this particular React element so we can wire up vanilla-tilt to it. To do this, we're going to use a ref prop and we need to pass a ref which is an object that has a mutable current property.

[0:47] Let's go ahead and use the React.useRef hook. From that we'll get our tiltRef and then we can copy that, paste it here to our ref. Then the tiltRef is an object that has a current property. That current property is the current value for this ref object.

[1:05] In our case, because we're passing this ref to a div with a ref prop, that current property will be the DOM node that React creates for this div. If we console.log tiltRef.current, and we save that then we should be seeing the DOM node.

[1:21] We actually see undefined. The reason we see that is because at the time that this function runs, React has not created the DOM node for this div, so the tiltRef.current is currently undefined. In fact, you can initialize that current value by passing an argument to their useRef hook.

[1:39] Here, how do we get the DOM node so we can initialize vanilla-tilt on it? We need to have some code that runs after React has updated the DOM and set our tiltRef.current property. Interacting with the DOM is a side effect. The logical place for this would be in a React.useEffect hook.

[1:55] If we move that console.log in here and we save that, then we get a refresh and now, we are getting the DOM node. From here, we can get our tiltNode from tiltRef.current and then let's go ahead and make some vanillaTiltOptions here. We can specify how we want vanilla-tilt to treat this DOM node.

[2:16] We'll say max 25, speed 400, glare true, and max glare .5. With that now, we can say VanillaTilt.init on that tiltNode and with the vanillaTiltOptions. If we save that, we get a refresh. This code runs after React has rendered to the DOM and updated the tiltRef.current property so that we can get access to the tiltNode and initialize vanilla-tilt.

[2:47] Now, if we hover, we get that really cool effect. If we unclick the show tilt checkbox, that will unmount the tilt component from the page removing the DOM node from the page. However, there's still a bunch of event handlers on that DOM node and several references to that DOM node from within the vanilla tilt library.

[3:07] That means that the DOM node itself may not exist on the page but it does still exist in memory because there're references to it in vanilla-tilt. Unfortunately, this could lead to a memory leak. If we keep on mounting and unmounting this over and over and over again, then we're going to have a bunch of DOM node sitting around in memory that really aren't needed by the user anymore.

[3:28] To combat this problem, we can return a function which will be called for every update of our tilt component. In this function, we can say tiltNode.vanillaTilt which is a property that vanilla-tilt is adding to our DOM node .destroy.

[3:48] This will remove all references of our DOM node from vanilla-tilt and remove all event handlers so that we can avoid memory leaks with our tilt component. What you can't see is that that DOM node has actually been garbage collected properly. We don't need to worry about memory leaks anymore.

[4:04] Another thing we should do here is recognize that useEffect is going to be called on every re-render of our tilt component. That's not going to lead to any bugs but it is sub-optimal because what that means is that we're called destroy with vanilla-tilt and then we'll call init with vanilla-tilt between every render of our component.

[4:21] We don't need to do that because none of the things in this function can change with re-renders of our tilt component. We're going to add a dependency array here with all the dependencies of our function and because none of the variables in our useEffect callback can change, we don't need to list any dependencies here.

[4:37] What this is effectively saying is I want to run this when the component is initially mounted to the page and then I want to run this when the component is unmounted from the page. That's another useful optimization we can make for this case.

[4:49] In review, the thing that we wanted to do is make this DOM node do some fancy stuff and we included vanilla-tilt on the page so that we could do that. We also included some CSS from the vanilla-tilt author to make our DOM nodes look really fancy.

[5:05] To get access to the DOM nodes so we can initialize vanilla-tilt on it, we're using this ref prop on the div and we pass the thing that we did back from a React useRef call. That ref has a current property that we can use to access the current value of this object.

[5:19] The reason that useRef is an object that has a current property is so that we can mutate the current property to be whatever we want without triggering a re-render of our component. We can use refs for more than just DOM nodes like we're using here. We can use it for any value that we want to keep track of and mutate over time without triggering a re-render of our component.

[5:39] After a component has been mounted, our useEffect callback is going to be called. We get the tilt node from our tilt ref current property. We create some vanilla-tilt options and pass those to the initialization for our tilt node with vanilla-tilt.

[5:54] Then we return a cleanup function so that we can remove all references of our DOM node in vanilla-tilt and remove all the event listeners that vanilla-tilt put on our DOM node.