Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

Using a Forwarded Ref Internally

February 09, 2022
reacttypescript


Sometimes we need to do things imperatively in React - for example, setting focus to an input element.

A useRef hook allows us to access HTML elements and invoke their methods imperatively. There is also forwardRef for forwarding the ref from a reusable component.

What if we want to use the ref internally within a reusable component and also forward the ref? This post covers how to do this.

A reusable Input component

We have a reusable Input component that contains an input element with a tooltip:

type Props = React.ComponentPropsWithoutRef<"input"> & {
  tooltip?: React.ReactNode;
};
export const Input = (props: Props) => {
  const internalRef = React.useRef<HTMLInputElement>(null);
  const [
    popperElement,
    setPopperElement
  ] = React.useState<HTMLDivElement | null>(null);
  const { styles, attributes } = usePopper(internalRef.current, popperElement);
  const [showTooltip, setShowTooltip] = React.useState(false);

  return (
    <>
      <input
        ref={internalRef}
        className="input"
        {...props}
        onMouseEnter={() => setShowTooltip(true)}
        onMouseLeave={() => setShowTooltip(false)}
      />
      <div
        ref={setPopperElement}
        className="tooltip"
        style={{ ...styles.popper, display: showTooltip ? "block" : "none" }}
        {...attributes}
      >
        {props.tooltip}
      </div>
    </>
  );
};

react-popper is used for the tooltip. react-popper requires a reference to the input element - we use a variable called internalRef that uses useRef for this.

So, our reusable Input component uses the input elements ref internally.

Adding forwardRef

At the moment, consumers of the Input component haven’t got a ref to the input element to do things like setting focus to it.

We need to add forwardRef to Input to give consumers a ref to the input element:

export const Input = React.forwardRef<  HTMLInputElement,  Props>((props, ref) => {  const internalRef = React.useRef<HTMLInputElement>(null);
  ...
  return (
    <>
      <input
        ref={internalRef}
        ...
      />
      ...
    </>
  );
});

The problem with this is the forwarded ref, ref, isn’t wired up to the input element ref, internalRef.

useImperativeHandle

There is a useImperativeHandle hook in React that we can use to wire the refs up:

const internalRef = React.useRef<HTMLInputElement>(null);

// 💥 Type 'HTMLInputElement | null' is not assignable to type 'HTMLInputElement'
React.useImperativeHandle(ref, () => internalRef.current);

useImperativeHandle takes in the forwarded ref and a function that returns the resolved ref.

The above gives a type error though. This is because the ref isn’t expected to have a null value. 😞

To resolve the type errors, we can use the generic parameters for useImperativeHandle:

React.useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(
    ref,
    () => internalRef.current
  );

A consumer of the Input component can now use its ref to set focus to the input element:

const inputRef = React.useRef<HTMLInputElement>(null);

React.useEffect(() => {
  if (inputRef.current) {
    inputRef.current.focus();
  }
}, []);
return (
  <Input
    ref={inputRef}
    type="text"
    tooltip="Enter something interesting"
  />
);

Nice! 😊

The code from this post is available in Codesandbox in the link below:

🏃 Play with the code

Did you find this post useful?

Let me know by sharing it on Twitter.
Click here to share this post on Twitter

If you to learn more about using TypeScript with React, you may find my course useful:

Using TypeScript with React

Using TypeScript with React
Find out more