React’s Virtual DOM arguably revolutionized web development. In short: React will keep a copy of the DOM in its own memory space. When you make updates, for example, several updates to component state, the React will manipulate the Virtual DOM first, performing all of the mutations on it in succession. Once all of your logic has run, React knows how to compare the Virtual DOM to the real DOM, and then it makes small, isolated updates one-by-one, only changing the least amount necessary.
Why does it do this? Well, for one thing, when the browser performs DOM operations it is famously slow at doing so, that is non-performant when many updates are happening. If each mutation of the DOM was done in the browser, you couldn’t achieve the fast effects you can with React.
You’ll note then that the objects you code in React, then, don’t expose direct access to the DOM elements. This is a good thing, because it means that to change the DOM you must always go through the React re-render cycle when you want to change your DOM.
Most of the time, this can get you most of what you need doing. But occasionally, if you need to access DOM elements to inspect them, you’ll want to use a feature of React called React Refs.
Other cases of using Refs is to manage text highlighting, managing focus, and media playback. Refs are also used for some advanced purposes too, like managing interactions with 3rd parties and multistep form state.
Today I will explore three patterns for using Refs in React: 1) A basic Ref implementation, 2) A forwarded Ref, that is a ref that has been sent to another component, and 3) A forwarded Ref used within a Styled Components setup.
Remember, your Ref will simply point to the DOM elements. What’s important here to keep in mind is that you 1) first need to create your ref, 2) then assign it to a component using ref=
, and 3) let React maintain the Ref for you during the rendering.
Example #1 — Basic Usage
First, you need to create a Ref object. You do this before your component is rendered using React.createRef()
First let’s start with the most basic React component. (To see this, edit your App.js file and remove the default content and add <ThingOne />
)
// src/thing_one.js
import React from 'react' class ThingOne extends React.Component{ constructor(props) { super(props) this.ref1 = React.createRef() } render() { return ( <div> </div> ) } } export default ThingOne
Notice that in the constructor, we create the Ref using this.ref1 = React.createRef()
. At this point, if you examine your object, you will see something interestingly empty:
Notice that your variable now holds an object with a single key, current
, that points to nothing (null
).
Now, let’s add an input checkbox and also tell React to use this ref (ref1
) for that input checkbox.
import React from 'react' class ThingOne extends React.Component{ constructor(props) { super(props) this.ref1 = React.createRef() } render() { return ( <div> this is an experiment into forwarded refs <br /> Please check this box: <input type={"checkbox"} ref={this.ref1} /> </div> ) } } export default ThingOne
We use this Ref (ref1
) in the render method, when we call
<input type={"checkbox"} ref={this.ref1} />
Here, we’re telling React when it renders the dom, a reference will be stored back onto the value in the current
slot in our Ref.
If you examine the Ref after React has rendered, you get something different:
Now, the Ref’s current value is a reference to the DOM element. That means that it can be treated like a “native DOM element” because it is one.
Finally, in our little baby example, we’ll attach an onClick
handler to the boxChecked
handler.
import React from 'react' class ThingOne extends React.Component{ constructor(props) { super(props) this.ref1 = React.createRef() // if you don't bind your method then boxChecked will be called // without `this` in scope this.boxChecked = this.boxChecked.bind(this) } boxChecked(event) { const dom_elem = this.ref1.current const is_checked = dom_elem.checked alert("This box is now " + (is_checked ? 'CHECKED' : 'UNCHECKED')) } render() { return ( <div> this is an experiment into forwarded refs <br /> Please check this box: <input type={"checkbox"} ref={this.ref1} onClick={this.boxChecked} /> </div> ) } } export default ThingOne
Be sure to bind it to the this
object (because, err.. javascript) in the constructor, and Bob’s your uncle… once clicked, this.ref1.current
now returns {current: input}
as you see above. As you can see from the code in boxChecked, you can pull the DOM element out from this.ref1.current
and then examine it using native HTML properties (.clicked
is implemented in the browser and is a native DOM element property.)
You can see the full code for this example here.
Example 2 — With Ref Forwarding
Now it’s time to go down the rabbit hole a little deeper. And with ref forwarding, we really are traveling down a rabbit hole.
Let’s stay we want to nest ThingTwo
inside of ThingOne
, but be able to look at the ref for ThingTwo
from code inside of ThingOne
.
Let’s say we try something like this
// src/thing_one.js
import React from 'react'
import ThingTwo from './thing_two'
class ThingOne extends React.Component{
constructor(props) {
super(props)
this.ref1 = React.createRef()
// if you don't bind your method then boxChecked will be called
// without `this` in scope
this.boxChecked = this.boxChecked.bind(this)
}
boxChecked(event) {
const dom_elem = this.ref1.current
alert("ThingTwo is " + dom_elem.getBoundingClientRect())
}
render() {
return (
<div style={{position: 'relative', border: 'solid 1px green'}}>
this is an experiment into forwarded refs
<br />
Please <button onClick={this.boxChecked} type="button" > click here </button>
<ThingTwo ref={this.ref1}/>
</div>
)
}
}
export default ThingOne
// src/thing_two.js
import React from 'react'
class ThingTwo extends React.Component{
render() {
return (
<div style={{position: 'relative', top: 0, left: 0, border: 'solid 1px red'}}>
This is thing Two
</div>
)
}
}
export default ThingTwo
Warning: The above code doesn’t actually work!
It makes sense that we might think we can pass a ref as a prop from one component to another. However, because of how React passes props around, this doesn’t actually work. Moreso, it actually produces no error message or indication to the developer that this is wrong. But here’s how you know it’s wrong: If you examine the ref once the UI is rendered, you get this wrong result:
If you see the name of the Component, something is wrong. it won’t work and your ref will not be usable. What’s wrong here is that you need to forward your ref from one component to another.
Interestingly, the code in thing_one.js doesn’t change — you’re still going to pass the ref down to ThingTwo via the props.
However, for ThingTwo, you need to wrap ThingTwo’s entire implementation in React.forwardRef
. This utility method will take a function and will call that function with two arguments: the props passed and the ref passed, but it will allow you to pipe the ref into ThingTwo like so:
import React from 'react' class ThingTwo extends React.Component{ render() { const {selfRef} = this.props return ( <div ref={selfRef} style={{position: 'relative', top: 0, left: 0, border: 'solid 1px red'}}> This is thing Two </div> ) } } export default React.forwardRef((props, ref) => <ThingTwo {...props} selfRef={ref} />)
Many examples online use a pattern like ref={ref}
but I personally find this confusing for demonstration. What is happening is that one variable name exists in the ForwadedRef constructor, and a distinct variable also named ref
then also exists inside your component, they just happen to both be called ‘ref.’ To eliminate the confusion, I named the ref that ThingTwo receives selfRef
, indicating that this variable is only ever used as the reference to DOM element that maps to the component (to ‘itself’). You can pick any variable names you want, but this example demonstrates that the variable occupies a distinct namespace in ThingTwo
.
Now, if you examine your ref after rendering, it looks like so:
Note that in some cases your DOM element object looks slightly different. For example, if React renders multiple of the same component, it will keep letter-based class names on your objects (it does this as its internal mapping to the DOM).
Here’s an example from a different app (you aren’t seeing the code for this). In this case, my component has a built-in class for logo-container
and the strange-looking characters that make up the other classes are correct.
The example code for example #2 can be found here.
Example 3 — With Ref Forwarding in a Styled Component
Forwarding refs is slightly tricky. Once you get the hang of it, it makes sense. Always remember that the receiving component must be wrapped in the forwarder. The final pattern to show you here is how a ForwardedRef can be used with Styled Components.
This example is just like #2, except that ThingTwo uses a StyledThingTwo to display itself
import React from 'react' import styled from 'styled-components' const StyledThingTwo = styled.div` position: relative; top: 0; left: 0; border: solid 1px red; ` class ThingTwo extends React.Component{ render() { const {selfRef} = this.props return ( <StyledThingTwo ref={selfRef} > This is thing Two </StyledThingTwo> ) } } export default React.forwardRef((props, ref) => <ThingTwo {...props} selfRef={ref} />)
You will note that because StyledThingTwo
is a functional styled component, it doesn’t need its own forwarded ref wrapper. Only the ThingTwo
needs to be wrapped in the forwardRef call.