Initializing focus state in React: you might be doing it wrong

Have you ever written a component with internal state like hasFocus or isFocused? I have. And now I’ve written enough to notice that my team’s, and many popular and well-maintained open source components, have an annoying bug!

This is pretty common if you’ve built custom form components with some higher-level features before, like a <TextField> perhaps. It probably looked something like this:

function Input(props) {
  const [hasFocus, setFocus] = useState(false);

  return (
    <input
      {...props}
      onFocus={() => setFocus(true)}
      onBlur={() => setFocus(false)}
    />
  );
}

This is useful whenever you want to conditionally render or style elements depending on whether the input is focused. Here’s a more realistic example where we actually use it:

function TextField({ id, label, ...rest }) {
  const [hasFocus, setFocus] = useState(false);

  return (
    <div
      style={{
        display: "inline-block",
        borderRadius: 5,
        padding: 10,
        background: hasFocus ? "rgba(164, 233, 255, 0.5)" : "transparent"
      }}
    >
      <label htmlFor={id}>{label}</label>
      {hasFocus ? " 👇" : null}
      <br />
      <input
        {...rest}
        id={id}
        onFocus={() => setFocus(true)}
        onBlur={() => setFocus(false)}
      />
    </div>
  );
}

Here’s precisely that component in action:


In the real world, this is practically necessary for building a fancier <input> replacement that allows you to render floating labels and “adornments” (like in Material UI) that appear to be visually inside the <input> box, but are actually adjacent to it. Inline input prefixes, icons, and show/hide password toggle buttons are good examples. This lets you achieve the visual style and conceptual grouping you want, without using messy absolute positioning or allowing the user’s text input to disappear behind other elements.

Why not use the CSS :focus pseudo-class?

Unfortunately, trying to use :focus limits what you can do: you can style the input or siblings that come after the input… but that’s it. In terms of conditional rendering, you can hide or show elements, but not mount or unmount them from the DOM. And crucially, you can’t style the parent element. Well, you could try to use :focus-within, but support is more limited, and what if you also have other focusable elements within your form field (like a dropdown button)? The limitations of CSS make this impractical.

Anyway, back to our focus state…

So we just showed a demo of tracking focus state in React above. We’re done, right? Not quite. Unfortunately, there’s one case we didn’t consider: the initial state!

Maybe your current React application is entirely rendered client-side, and the <input> doesn’t even exist yet before your React code starts running. But if you were to start using server-side rendering (SSR), or publish your component to be consumed by other people using server-side rendering, they might find that it’s possible to get the focus state out of sync! How?

The reason is that with server-side rendering, the <input> may already be on the page before our JavaScript code has even been downloaded and before React hydrates it and starts listening for events. And during this time, it’s still possible for users to focus the <input>. In fact, this was a common issue with focus-handling JavaScript code long before React arrived on the scene. You may have experienced this frustration before if you tried to navigate a JavaScript-heavy site too quickly: you focused the search bar or some other input, but the site doesn’t seem to recognize it!

When this happens and the element starts off focused before React hydrates, it’s too late for React to see the focus event and call our onFocus handler. But the element is focused, so our initial hasFocus state of false is wrong!

To simulate this, I’ve delayed the hydration of the <TextField> below until after you’ve focused it for the first time. Try it:


Not hydrated

Notice how when you focus it for the first time, it doesn’t get the same treatment as our first example above, even though it’s the same component! You need to move focus away from the element, then back again for React to fire the onFocus handler we supplied and thus update the hasFocus state.

So what’s the solution? This is actually a familiar situation in any event listener or subscription setup, and the fix is the same: we should not only subscribe to the event, but also check the value at initialization time, so that the state reflects any events that occurred before we started listening. So, when our component first mounts, we need to check whether it was already focused.

To check whether an element is focused in JavaScript, you can check whether it is the document’s activeElement. Like so:

node === document.activeElement

If you’re doing something more complex and want to track whether an entire region contains focus, you could also check whether the activeElement is your node or a descendant of it:

node.contains(document.activeElement)

Doing it this way actually more closely reflects the meaning of React’s onFocus and onBlur handlers, because unlike the standard browser focus and blur events, React will bubble them up through the component tree (which actually comes in pretty handy).

There’s one remaining quirk, though. It’s possible to move focus away from an HTML document altogether (to another window, or other parts of the browser chrome, for instance). And when this happens while an element is focused, it’s blurred but confusingly remains the document.activeElement! So in order get the correct initial state all the time, we also need to check whether the document itself has focus. There’s a convenient hasFocus() method to determine this:

document.hasFocus()

Putting it all together

Let’s go back to our very simple <Input> example and drop in the new code:

function Input(props) {
  const ref = useRef();
  const [hasFocus, setFocus] = useState(false);

  useEffect(() => {
    if (document.hasFocus() && ref.current.contains(document.activeElement)) {
      setFocus(true);
    }
  }, []);

  return (
    <input
      {...props}
      ref={ref}
      onFocus={() => setFocus(true)}
      onBlur={() => setFocus(false)}
    />
  );
}

And here’s a demo of our fancy <TextField> component using the fixed code. Notice how the very first focus shows the correct state, even though React doesn’t kick in until later:


Not hydrated

Stay focused, friends. Did I find a bug in your component? Let me know.