useRef and Forwarding Refs Part 1

Refs are an absolute must-learn for React beginners. As part of my React All The Hooks series, it’s important that we make a stop-by on a very important hook: React useRef.

One important concept to grok with React is the React virtual DOM from the real browser DOM.

If you can do this, you can craft sophisticated applications that will have slick user interfaces (UIs), because you will understand when to let React do its thing and when you need to get your hands dirty with the real DOM objects themselves.

Find the examples here: https://github.com/jasonfb/useRef_Examples

What is the useRef Hook?

Refs, or formerly called “forwarded refs” are like pointers. They will attach to either DOM elements or any Javascript object and work outside of React’s normal, native render/update cycle. You use them in special cases only when React’s prop & state paradign cannot work.

There are two ways of creating a ref: useRef and React.createRef()

To use useRef, you will import it along with your import React statement

import React, {useRef} from 'react'

You can also create a ref using React.createRef(). [TBD: is this true?]

For this reason, your choice of whether to use useRef and React.createRef() impacts how you can pass it from one prop to another. The section below “React useRef vs React.createRef()” explores this more.


Although refs are a little difficult to wrap your head around at first, I recommend you start by learning this: the difference between useRef, React.createRef() and how to use a Forwarded Ref.

Examples 1-5 on this page demonstrate those concepts and the implications of passing a ref between components.

Why and When Should You use a ref at all?

  • Manage focus, text selection, or media playback. 
  • Trigger imperative animations.
  • Interacting with 3rd party libraries.

So, in short, anything outside of the normal React render/update cycle.

“Focus” in the browser or UI context refers to when a field on your page has the cursor in it and is ready for entry.

Example 6 demonsrates manaing focus,

The concept is so familiar to most web users today we don’t even think about it.

When an input element has focus on the page, if you type a character on the keyboard the browser will interpret as an entry for that input. Most browsers implement a highlight color around the text to tell you that when you type on the keyboard will replace the text. Either this or having your cursor in the input element means the element has focus.

Example 7 demonstrates managing text selection, and Example 8 demonstrates Media Playback.

That is to say, an imperative animation is one in which you tell the code to animate at a specific point in time or in your code’s execution. As well, A declarative animation on the other hand would be one that you describe using code that doesn’t tell or instruct the browser to do something at a specific point. Rather, a declarative animation describes the animation and lets the browser implement it when it triggers. For example, a :hover animation in CSS would be a declarative animation. When you initiate an animated transition between screens in your app, you are creating an imperative animation.

This post does not have examples for Triggering imperative animations and interacting with 3rd party libraries.

Boats and Docks Metaphor

Dock with rope useRef
  • Let’s say we’re talking a boat rode from the side of a dock. React refs are the anchor line attached to the dock.
    As boats come by they will dock by the rope tying the boat, or attaching to it, temporarily as it docks
  • The boats are like DOM elements that React will render onto the page for us.
  • If we used JS to store references (even if we were able or allowed) to the real DOM elements, we aren’t guaranteed those DOM elements will stick around after React’s render/update cycle. 

What’s tricky here is that you’re going to create the ref before the DOM element. Such a ref, before React attaches it, is an unattached ref.

The useRef React hook is a function that returns a mutable ref object.

[@saransk]

In conclusion, useRef takes only an initialValue, but mostly you will initialize a ref with nothing, so you pass nothing and undefined will be used. React creates the ref but not the DOM element it is attached to.

Most importantly, remember that the ref is the anchor line but it isn’t yet attached to a dock. That’s because React needs to go through a render cycle in order to actually construct your page.

You will then pass this ref to a DOM element, like so:

<input ref={myRef} />

Anchored! Ahoy matey!

ferry rope tied to boat useRef

In my extended metaphor, consider that React might some point in the future actually swap out that DOM element with a new one. Although the React virtual DOM structure tries to make as few manipulations of the real DOM as possible, it does not guarantee that it won’t make changes to the real DOM.

For this reason, if you tried to access DOM elements from your code you’d get strange effects: An element that you access and assign to a variable might not exist or persist the next time that code runs — the DOM element itself has been fully replaced by React under the hood.

React useRef vs React.createRef()

The Legacy way of creating refs was React.createRef(). Using createRef() is still around, even in hook-based functional components, but it’s important to note some important differences.

Dorothy Parker said, “A ref is a ref is a ref,” except that only a ref created with useRef can be passed as a property.

Remember, React.createRef() was the legacy way that refs need to be created with class-based react, and refs developed special rules when you want to pass a ref between components:

useRef HookReact.createRef()
for use with Hook-based Functional ComponentsFound in both functional components and legacy React code
Can it reference a thing within the same component it was created in?Yes (see example 1)Yes (see example 2)
Can you pass the ref as to a child component through an arbitrarily named property?
e.g.
<ThingTwo myRefB={myRef}>
Yes (see example 3)Yes (see example 4)
Can you pass a ref to a child component wrapping the entire component with the ref itself?
e.g.
<ThingTwo ref={myRef}>
No, you cannot pass ref as a prop without using Forwarded ref
Example 5– not working because we didn’t use ForwardedRef
Example 6 – fixed with forwarding the ref to the child component

Notice here that in the first example, we’re passing a ref through an arbitrary prop name (in this case myRefB), In the second example, we’re actually passing a prop named ref to the React child, which tells React to do something special.

useRef, React.createRef(), and Forwarded Refs

Example 1: useRef within the Same Component

import ThingOne from './components/ThingOne.js'

function App() {
return (
<div className="App">
<ThingOne />

</div>
);
}

export default App;

Consider this basic example of a ref (myRef) created in the main body of a functional component using useRef():

components/ThingOne.js

import React, {useRef} from 'react'

const ThingOne = (props) => {
  const myRef = useRef(undefined)

  const handleButton = (event) => {
    if(myRef.current) {
      if (myRef.current.value === "") {
        alert(`The ref is empty string`)
      }
      else {
        alert(`The ref is ${myRef.current.value}`)
      }
    }
  }

  return(
    <>
      <input ref={myRef} />
      <input type={"submit"} onClick={handleButton} />
    </>
  )
}

export default ThingOne


This one-component example demonstrates just what a ref from within a container does: it points to the DOM element that we pass it to, using the special ref attribute on the DOM element.

<input ref={myRef} />

In this way, myRef now becomes a special React-managed object that contains one nested attribute: a “current” property. The current property, which in this example points to an <input> element on the page, always attached to whichever DOM element React keeps current, even after React has gone through its update/render cycle (during which the DOM element might be detached and re-rendered).

In our example <input ref={myRef}> is what is referred to, or the boat in the dock-boat metaphor.

If we leave the field blank, as the if condition above states, we see this:

The ref is empty string useRef example React code

As you expect, if we enter into the field, we get the alerted text:

Hello world example showing alert message

Example 2: Using React.createRef() within the Same Component

What if I use React.createRef() here?
import React from 'react'

const ThingOne = () => {
  const myRef = React.createRef()
  const handleButton = (event) => {
    if(myRef.current) {
      if (myRef.current.value === "") {
        alert(`The ref is empty string`)
      }
      else {
        alert(`The ref is ${myRef.current.value}`)
      }
    }
  }
  return (<>
      <input ref={myRef} />
      <input type={"submit"} onClick={handleButton} />
    </>
  )
}

export default ThingOne

In fact, we get the same result because in this context you use either useRef or React.createRef()

Example 3: Passing a Ref To a Child via an Arbitrary Property Name

Now let’s reset our example code. We are going to create two components: a ThingOne and a ThingTwo. The ThingTwo will be inside ThingOne.

Ths is important to demonstrate what happens when passing refs.

The two refs will be attached to two separate <input /> elements

Everytime we make a keystroke, we want the two inputs concatenated together and the result displayed below, like so:

Rect refs showing two refs

We will do this by updating a state every time a keystroke is made

App.js

import ThingOne from './components/ThingOne.js'

function App() {
return (
<div className="App">
<ThingOne />
</div>
);
}

export default App;

components/ThingOne.js

import React, {useRef, useState} from 'react'
import ThingTwo from '../components/ThingTwo'
const ThingOne = (props) => {
  // create refs here
  const myRefA = useRef(undefined)
  const myRefB = useRef(undefined)

  const [concatText, setConcatText] = useState("")

  const concatinateThemTogether = (event ) => {// don't use event
    const new_concat_text = (myRefA.current ? myRefA.current.value : " ") +
      (myRefB.current ? myRefB.current.value : "")
    console.log("concatinateThemTogether was called; new text is ", new_concat_text)
    setConcatText(new_concat_text);
  }

  return (
    <>this is thing one
      <br />

      <input ref={myRefA}
             onChange={concatinateThemTogether}/>
      <br />
      <ThingTwo
        concatinateThemTogether={concatinateThemTogether}
        myRefB={myRefB}  />
      <br />
      the two things together are:
      <br />
      {concatText}
    </>
  )
}
export default ThingOne

Notice that in this example, I’m passing the ref through a prop that isn’t named ‘ref’ itself: myRefB (that is, named anything other than ref)

In the child component, I’ll receive that via the same prop name, myRefB

components/ThingTwo.js

import React from 'react'

const ThingTwo = (props) => {
  return (
    <>This is thing two
      <br />
      type something here:
      <br />
      <input
        onChange={props.concatinateThemTogether}
        ref={props.myRefB} />
    </>
  )
}

export default ThingTwo

Or, if you prefer to destructure your props:

import React from 'react'

const ThingTwo = ({myRefB, concatinateThemTogether}) => {
  return (
    <>This is thing two
      <br />
      type something here:
      <br />
      <input
        onChange={concatinateThemTogether}
        ref={myRefB} />
    </>
  )
}

export default ThingTwo

Either way, the functionality is unaffected by the fact that we passed a ref created with useRef() through a property. We still can concatenate two strings together.

Concatenate two string useRef React Example

What did we learn here?

• You can create a ref with useRef() and pass it through a property as long as that property

Example 4: Passing a Ref Created with React.createRef() Through an Arbitrary Prop

components/ThingOne.js

import React, {useState} from 'react'
import ThingTwo from '../components/ThingTwo'
const ThingOne = (props) => {
  // create refs here.
  const myRefA = React.createRef()
  const myRefB = React.createRef()

  const [concatText, setConcatText] = useState("")

  const concatinateThemTogether = (event ) => {// don't use event
    const new_concat_text = (myRefA.current ? myRefA.current.value : " ") +
      (myRefB.current ? myRefB.current.value : "")
    console.log("concatinateThemTogether was called; new text is ", new_concat_text)
    setConcatText(new_concat_text);
  }

  return (
    <>this is thing one
      <br />

      <input ref={myRefA}
             onChange={concatinateThemTogether}/>
      <br />
      <ThingTwo
        concatinateThemTogether={concatinateThemTogether}
        myRefB={myRefB}  />
      <br />
      the two things together are:
      <br />
      {concatText}
    </>
  )
}
export default ThingOne

Works:

React.createRef() works fine when you pass it as an arbitrary prop name

What did we learn here?

• A ref created in one component created with React.createRef(), when passed to another component, will continue to reference fine when as long as you pass it an arbitrary prop name (not ref).

Example 5 — Forwarding Refs The Wrong Way

You cannot use ref as a prop to wrap a child component, but you can use Forwarded Refs.

Take for example the following non-working code:

components/ThingOne.js

import React, {useRef, useState} from 'react'
import ThingTwo from '../components/ThingTwo'
const ThingOne = (props) => {
  // create refs here.
  const myRefA = useRef(undefined)
  const myRefB = useRef(undefined)

  const [concatText, setConcatText] = useState("")

  const concatinateThemTogether = (event ) => {// don't use event
    const new_concat_text = (myRefA.current ? myRefA.current.value : " ") +
      (myRefB.current ? myRefB.current.value : "")
    console.log("concatinateThemTogether was called; new text is ", new_concat_text)
    setConcatText(new_concat_text);
  }

  return (
    <>this is thing one
      <br />

      <input ref={myRefA}
             onChange={concatinateThemTogether}/>
      <br />
      <ThingTwo
        concatinateThemTogether={concatinateThemTogether}
        ref={myRefB}  />
      <br />
      the two things together are:
      <br />
      {concatText}
    </>
  )
}
export default ThingOne

This also, does not work:

components/ThingTwo.js

import React from 'react'

const ThingTwo = ({ref, concatinateThemTogether}) => {
  return (
    <>This is thing two
      <br />
      type something here:
      <br />
      <input
        onChange={concatinateThemTogether}
        ref={ref} />
    </>
  )
}

export default ThingTwo

If you try this, you will get two warnings from React in your developer console:

ref is not a prop error message when trying to use React ref as a property

Notice that the first thing it tells you is:

index.js:1 Warning: ThingTwo: ref is not a prop. Trying to access it will result in undefined being returned. If you need to access the same value within the child component, you should pass it as a different prop.

It says “if you need to access the same value within the child component, you should pass it as a different prop,” which is precisely what we covered in Example 3 and Example 4. above.

Finally, it says,

index.js:1 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Example 6: Forwarded Ref the Right Way

What did we learn?

You cannot use ref as a prop to wrap a child component, but you can use Forwarded Refs.

Phew! That forwarded refs stuff gets tricky fast

But if you get the above concepts working with Refs actually becomes easier. In the next examples, I show more realistic code examples. It’s still, intentionally, prototype code, but here we move onto the important work of managing focus, text selection, and playback.

Refs to Manage Focus

Please Join us Tues, Aug 3 at the Brooklyn Javascripters Meetup (online).

Refs to Manage Text Selection

Please Join us Tues, Aug 3 at the Brooklyn Javascripters Meetup (online).

Please Join us Tues, Aug 3 at the Brooklyn Javascripters Meetup (online).

Refs to Manage Video Playback

Refs to reach outside of state & props and interact with 3rd party libraries

Since the useRef React hook returns a JavaScript object, it is not limited to storing DOM nodes. We can use it to store any variables that we want to be persisted across re-renders.

This can be extremely valuable if we want to have something that holds a state-like persistence but doesn’t actually live within React’s component state life cycle, which I cover in the useState lesson.

Avoiding Side Effects

Updating a ref value is considered a side effect. This is the reason why you want to update your ref value in event handlers and effects and not during rendering (unless you are working on lazy initialization). React docs warn us that not following this rule could lead to unexpected behaviors.

But the React docs recommend modifying refs in event handlers or effects.