Refs, and when an input should control itself

February 14, 2022 · 4 min read

I had a small form with a username field, and I wired it the way I had just learned: state, an onChange handler, and value fed back in. It worked. Then I looked at what it was doing. The component re-rendered on every keystroke, and I only ever needed the text once, when someone pressed submit.

Reaching the input directly

A ref is a way to hold on to a real DOM element from my own code. It starts with useRef, which only works inside a component, like any hook. Then I attach it with the special ref prop, the same way key is a special prop:

const nameInputRef = useRef();

<input type="text" id="username" ref={nameInputRef} />

What confused me first is what the ref actually holds. It is always an object with a single current field. That field is empty until the component renders. After that, React puts the real DOM node for the input in it. So nameInputRef.current is the same element I would get from document.querySelector.

Reading it on submit

Every input element has a value property, so the handler just reads it:

const formSubmitHandler = (e) => {
  e.preventDefault();
  const username = nameInputRef.current.value;
  props.onSaveNewUser(username);
  nameInputRef.current.value = "";
};

The state, the change handler, and the value and onChange props are all gone. Nothing runs while the user types.

The last line is the one I'm unsure about. Clearing the field by writing to the DOM works, and for a single input I can live with it. But it skips React completely. The rule I keep in mind is that React should own the DOM, and my code should mostly look at it.

CONTROLLEDkeystrokeonChangesetStatere-renderinputvalue={state}value fed back into the inputREFtype freelyno state, no rendersubmitref.current.valueread once
A controlled input goes around the loop on every keystroke. A ref input is only looked at when you ask.

Ref or state

The difference that matters is what happens when the value changes. Setting state schedules a re-render. Changing ref.current does nothing visible, and I can do it as often as I like. Its value also survives re-renders, so it is a place to keep something the screen doesn't depend on.

Controlled and uncontrolled

These names finally made sense once I tied them to who owns the value. In my expenses app the year filter is a select that gets props.selected from its parent and reports changes through props.onChangeFilter. The select holds nothing of its own, so it is controlled.

<select value={props.selected} onChange={dropdownChangeHandler}>

My ref input is the opposite. The browser keeps whatever the user types, and I only fetch it when I need it. I never feed a value back, so it is uncontrolled. That also explains why ref.current.value = "" felt off: it is the plain DOM API, with React just handing me the node. The quick test is two-way binding. If I pass value in and listen for changes, React controls the input.

What stuck with me: state when the screen has to react to the value, a ref when I only need to read it once.