Focusing a custom input from outside: forwardRef and useImperativeHandle

June 19, 2022 · 4 min read

Most of React is about passing data down and letting the child decide what to do. This is the one time I needed the opposite: the parent has to reach into a child and make something happen.

What I wanted

My login form has two custom Input components. I removed the disabled state from the submit button, so now the user can click it with bad data. When that happens, the first invalid field should get focus.

With a plain <input> that is easy. Put a ref on it and call ref.current.focus(). My Input is my own function component, though, and a ref on it does nothing. It has no way to hand that ref to anything.

First, a ref inside the component

To see how refs behave, I first did it the wrong way round. Inside Input I put a ref on the real input and focused it in an effect:

const inputRef = useRef();

useEffect(() => {
  inputRef.current.focus();
}, []);

The result confused me. The password field ended up focused, and the email field had a red border. The reason is the order. Email focuses itself, then password focuses itself, so email loses focus. Losing focus fires onBlur, which validates the empty email and marks it invalid. Also, focus() is plain JavaScript, not a React thing.

Exposing a function

What I actually want is for Input to own a function, and for Login to call it when it needs to. useImperativeHandle does that. You tell it which ref to attach to and return an object of whatever the outside is allowed to use:

const activate = () => {
  inputRef.current.focus();
};

useImperativeHandle(ref, () => ({
  focus: activate,
}));

The object works like a translation table. focus is the name the parent sees, and activate is the private function behind it.

Where the ref comes from

A component normally takes only props. A second argument exists, and it is the ref the parent passed in. But React only fills it in if the component is wrapped in forwardRef:

const Input = React.forwardRef((props, ref) => {
  // useImperativeHandle(ref, ...) goes here
});

The wrapped component is still a normal component, but now it accepts ref. In Login I make a ref for each field, pass it along, and call it on submit:

const emailInputRef = useRef();

<Input id="email" ref={emailInputRef} ... />

// in submitHandler
if (!emailIsValid) emailInputRef.current.focus();
PARENTLoginemailInputRefref=INPUTforwardRefreceives ref{ focus }exposed objectfocus()inputRef.currentreal <input>parent sees only the exposed box
The ref ends at the exposed object. The real input stays private to Input.

Only what you expose

The parent's ref does not point at the DOM node. It points at the object from useImperativeHandle, so focus is all it can use. It can't read the input's value or change its styles. I like that, since the child decides how much of itself is open.

I would still avoid this most of the time, because it works against how React normally flows. Focus and scroll are the cases where it earns its place.

What stuck with me: forwardRef lets the ref in, and useImperativeHandle decides what the parent gets to touch.