Four ways to use a fallback ref with React.forwardRef

Published on in JavaScript and React

A component wrapped in React.forwardRef normally expects to receive a ref from its parent component. What if you want the component to have a ref even if it doesn't receive one from its parent?

Table of contents

The problem

React.forwardRef allows passing a ref to a child component:

function Parent() {
  const ref = useRef(null)

  useEffect(() => {
    console.log(ref.current)
  }, [])

  return <Child ref={ref} />
}

const Child = React.forwardRef((props, forwardedRef) => {
  useEffect(() => {
    console.log(forwardedRef?.current)
  }, [forwardedRef])

  return <div ref={forwardedRef}>...</div>
})

When rendering <Parent />, both useEffects log the <div> element from the Child component:

<Parent />
// Logs `<div>` twice

But when rendering <Child /> on its own, the Child's useEffect logs undefined because no ref is passed/forwarded:

<Child />
// Logs `undefined` once 😿

More specifically: forwardedRef is null, so forwardedRef?.current is undefined.

What if you want Child to have a ref to its <div> element even when no ref is forwarded to Child? You can create a new ref in Child, but you can't attach both refs to the <div>:

const Child = React.forwardRef((props, forwardedRef) => {
  const ref = useRef(null)

  return (
    <div
      // 🛑 JSX elements cannot have multiple attributes with the same name
      ref={forwardedRef}
      ref={ref}
    >
      ...
    </div>
  )
})

Desired outcome

When rendering <Parent />, both useEffects should still log the <div> element from the Child component:

<Parent />
// Logs `<div>` twice

When rendering <Child> on its own, its useEffect should log the <div> element:

<Child />
// Logs `<div>` once

Four solutions

useRef + ||

Create a fallback ref with useRef and use it if forwardedRef is null:

const Child = React.forwardRef((props, forwardedRef) => {
  const fallbackRef = useRef(null)
  const ref = forwardedRef || fallbackRef

  useEffect(() => {
    console.log(ref.current)
  }, [ref])

  return <div ref={ref}>...</div>
})

Or as a custom hook:

const Child = React.forwardRef((props, forwardedRef) => {
  const ref = useFallbackRef(forwardedRef)

  useEffect(() => {
    console.log(ref.current)
  }, [ref])

  return <div ref={ref}>...</div>
})

function useFallbackRef(forwardedRef) {
  const fallbackRef = useRef(null)
  return forwardedRef || fallbackRef
}

Pros:

  • Very simple and clear. Maybe the simplest solution.

Cons:

  • The fallback ref is created even when it's not needed, but it's probably not a big deal.

I found this solution via brunoscopelliti/use-forward-ref on GitHub, which was the only npm package found via searching "react fallback ref" on npmjs.com.

Lazy useState + conditional createRef

Create a fallback ref with React.createRef if forwardedRef is null.

To avoid creating a new ref every time the Child component is re-rendered, wrap the conditional ref creation in useState and use lazy initial state:

const Child = React.forwardRef((props, forwardedRef) => {
  const [ref] = useState(() => forwardedRef || React.createRef())

  useEffect(() => {
    console.log(ref.current)
  }, [ref])

  return <div ref={ref}>...</div>
})

Or as a custom hook:

const Child = React.forwardRef((props, forwardedRef) => {
  const ref = useFallbackRef(forwardedRef)

  useEffect(() => {
    console.log(ref.current)
  }, [ref])

  return <div ref={ref}>...</div>
})

function useFallbackRef(forwardedRef) {
  return useState(() => forwardedRef || React.createRef())[0]
}

Pros:

  • Simple and reads like English: use forwardedRef if it exists (i.e. if it's not null), otherwise create a new ref.
  • The fallback ref is created only when necessary. (Probably doesn't matter though.)

Cons:

  • It's mildly confusing to mix hooks and the non-hook createRef function.
  • Simple because uses only the useState hook, though you do need to know about and understand lazy initial state.

I came up with this solution myself. 🤙

useImperativeHandle

React's useImperativeHandle hook allows customizing the value of a forwarded ref.

The docs for the hook are very terse (emphasis added and example code slightly modified):

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle customizes the instance value that is exposed to parent components when using ref. As always, imperative code using refs should be avoided in most cases. useImperativeHandle should be used with forwardRef:

const FancyInput = React.forwardRef((props, forwardedRef) => {
  const ref = useRef()

  useImperativeHandle(forwardedRef, () => ({
    focus: () => ref.current.focus(),
  }))

  return <input ref={ref} />
})

In this example, a parent component that renders <FancyInput ref={inputRef} /> would be able to call inputRef.current.focus().

In the quoted example, inputRef.current has only the focus method.

To expose all methods and properties of the DOM element to the parent ref, expose ref.current as-is:

useImperativeHandle(forwardedRef, () => ref.current)

Then:

  • If a ref is forwarded, forwardedRef.current and ref.current will point to the same DOM element.
  • If a ref is not forwarded, forwardedRef will still be null, but ref will act as a fallback ref.

Put together:

const Child = React.forwardRef((props, forwardedRef) => {
  const ref = useRef(null)
  useImperativeHandle(forwardedRef, () => ref.current)

  useEffect(() => {
    console.log(ref.current)
  }, []) // `ref` is not needed here

  // Notice we are using `ref` here, not `forwardedRef`
  return <div ref={ref}>...</div>
})

Or as a custom hook:

const Child = React.forwardRef((props, forwardedRef) => {
  const ref = useFallbackRef(forwardedRef)

  useEffect(() => {
    console.log(ref.current)
  }, [ref]) // `ref` is needed again here

  return <div ref={ref}>...</div>
})

function useFallbackRef(forwardedRef) {
  const ref = useRef(null)
  useImperativeHandle(forwardedRef, () => ref.current)
  return ref
}

Pros:

  • The useImperativeHandle hook is designed for customizing the value of the forwarded ref, so using it like this feels correct.
  • ESLint knows that the ref variable is stable, so it doesn't need to be included in the useEffect's dependency array, making the code clearer. (Though ESLint won't know that ref is stable when using the custom hook.)

Cons:

  • The useImperativeHandle hook is exotic and probably only rarely used. If you have never heard of it before, you can't know how it works at a first glance. (Compared to useState, which is maybe the most basic hook and should be familiar to any React dev.)
  • The fallback ref is created even when it's not needed, but it's probably not a big deal.

I found this solution via Jake Trent's blog post Fallback Ref in React. It introduced me to the useImperativeHandle hook and has good musings about why the hook is lame.

Merge refs

Use a utility like react-merge-refs to merge the two refs:

import mergeRefs from 'react-merge-refs'

const Child = React.forwardRef((props, forwardedRef) => {
  const ref = useRef()

  useEffect(() => {
    console.log(ref.current)
  }, [])

  return <div ref={mergeRefs([forwardedRef, ref])}>...</div>
})

Under the hood mergeRefs returns a callback ref:

function mergeRefs(refs) {
  return (value) => {
    refs.forEach((ref) => {
      if (typeof ref === 'function') {
        ref(value)
      } else if (ref != null) {
        ref.current = value
      }
    })
  }
}

If forwardedRef is null, mergeRefs simply skips it.

Pros:

  • More versatile because supports merging more than two refs.

Cons:

  • Requires a 3rd party library unless you re-implement the utility yourself. And if you are going to re-implement it yourself, there are simpler solutions (unless you need to merge more than two refs).

The use-callback-ref library provides a similar mergeRefs function as well as a useMergeRefs hook.

Which solution to use?

If you need to merge more than two refs, use a utility like react-merge-refs or use-callback-ref.

Otherwise I would prefer the first or the second solution. Both are simple and straightforward. No point in installing a dependency if you just want to create a fallback ref.

Initially the useImperativeHandle solution felt the most correct to me because the hook is designed for customizing the value of the forwarded ref. However, the solution is more difficult to understand because the hook is so exotic and rarely used.

One more point to consider: what if, during the lifecycle of Child (the component wrapped in React.forwardRef), forwardedRef is sometimes null and sometimes a ref object? I haven't tested how the different solutions would react to that. This might or might not be relevant in your case.

Invalid solutions

Knowing what doesn't work can be very useful as well.

Conditional useRef

Creating a fallback ref with useRef if forwardedRef is null might work:

const Child = React.forwardRef((props, forwardedRef) => {
  // 🛑 Not allowed!
  // Calling hooks conditionally breaks the rules of hooks.
  const ref = forwardedRef || useRef()

  useEffect(() => {
    console.log(ref.current)
  }, [ref])

  return <div ref={ref}>...</div>
})

...but it might also not work because calling hooks conditionally breaks the rules of hooks.

Conditional createRef

React.createRef is not a hook, so it can be called conditionally:

const Child = React.forwardRef((props, forwardedRef) => {
  // 🛑 A new ref is created on every re-render if `forwardedRef` is `null`
  const ref = forwardedRef || React.createRef()

  useEffect(() => {
    console.log(ref.current)
  }, [ref])

  return <div ref={ref}>...</div>
})

However, if forwardedRef is null, a new ref is created every time the Child component is re-rendered.

Furthermore, ref is not stable, so it needs to be included in the useEffect's dependency array. Because of this, the useEffect will run on every re-render.

Omitting ref from the dependency array and ignoring the "exhaustive-deps" ESLint rule is not a proper solution. Ignoring the rule is confusing and opens the door for further problems.

useRef + conditional createRef

To make the ref from the previous invalid solution stable, we can wrap it in another useRef and immediately grab its current value (i.e. forwardedRef or the newly-created ref):

const Child = React.forwardRef((props, forwardedRef) => {
  // 🛑 A new ref is created and discarded on every re-render if `forwardedRef` is `null`
  const ref = useRef(forwardedRef || React.createRef()).current

  useEffect(() => {
    console.log(ref.current)
  }, [ref]) // `ref` must be included here but is stable

  return <div ref={ref}>...</div>
})

However, if forwardedRef is null, a new ref is still created every time the Child component is re-rendered; it's just discarded immediately.

Plus the code looks ugly and unclear.

Further resources

This blog post contains these links: