Learning path · Level 9 · Tutorial 7

Focus Native Controls with React Refs

Use typed React refs to focus native controls when an interaction calls for it, without storing visible data outside React state.

  • Level: Intermediate
  • Duration: 13 min
  • 6 steps
Start tutorial Back to the path
Focus Native Controls with React Refs

Step 1 of 6

Separate Visible Data from DOM Handles

Separate React-owned visible message data from a DOM ref that holds an imperative native-control handle.

Visible data and element handles

Two kinds of information

In Message draft, the text a person sees and edits is interface data. The input displays message, and the preview displays that same value. Because a change to this data must update what React renders, it belongs in state.

A DOM ref has a different job. It holds an imperative handle to a native element so code can later ask that element to perform a native action. It is not a second place to store the message.

State drives output; a ref points to an element

Diagram showing one state value driving both a controlled input and a preview, while a separate reference points to the input element.

State supplies visible data to React's rendered interface. A DOM ref identifies the native control for an imperative action.

Choose the owner by the result

Different changes have different consequences

A ref is a stable container for the same mounted component instance. Its current value can persist through ordinary renders, but changing that value does not ask React to render again.

Use state when a change must affect rendered output. Use a DOM ref when code needs an imperative handle to a native element. The handle can support a native action, but it does not own the controlled value.

State and ref responsibilities

Use this decision rule for the running example.

Only a state update requests React to reconsider the rendered interface.
Thing or changeKeep it inWhy
Message shown in the inputStateIt is visible data
Text after Preview:StateIt is rendered output
Handle to the native inputDOM refIt enables a native action
Updating messageState setterIt requests a render
Changing ref.currentRefIt does not request a render

Tip

Keep React responsible for content

Do not use an element handle to manually replace React-owned input text or preview text. React still renders from state, so a later render can overwrite that manual DOM change. Update the controlled value through state instead.

Check your distinction

Classify the responsibility

Match each description to its correct category.

Tap an item and then its matching pair.

Step 2 of 6

Attach a Typed Ref to a Native Input

Attach a nullable, element-specific ref to the controlled Message input without moving visible data out of state.

Keep the controlled value in state

One input, two responsibilities

Your controlled input still keeps its visible text in message state. Add a separate ref only to identify the native <input> element.

A ref is not a second value store: it does not replace value={message} or the onChange handler. Its object remains stable for this mounted component, and changing its current value does not request a render.

For a text <input>, declare the ref with the matching DOM type and an empty initial value: HTMLInputElement | null and null.

State renders; the ref identifies

Diagram showing state driving an input and preview, while a separate ref points to the native input element.

The controlled value remains React state; the ref is a handle to the native control.

Build the typed attachment

Message draft: typed input ref

Replace src/App.tsx with this starter in your existing local React and TypeScript app. It keeps the Message field controlled while attaching a typed ref object to the native input.

tsx
import { useRef, useState } from "react";

export default function App() {
  const [message, setMessage] = useState("Hello, React!");
  const messageRef = useRef<HTMLInputElement | null>(null);

  return (
    <main>
      <h1>Message draft</h1>

      <label htmlFor="message">Message</label>
      <input
        ref={messageRef}
        id="message"
        type="text"
        value={message}
        onChange={(event) => setMessage(event.currentTarget.value)}
      />

      <p>Preview: {message}</p>
    </main>
  );
}
Expected output
Message draft
Message
Hello, React!
Preview: Hello, React!

The attached ref produces no additional visual change. The input and preview continue to render from `message` state.

Tip

Attach the object, not its current value

Pass the ref object as ref={messageRef}. Do not pass messageRef.current in JSX. React uses the object to maintain its connection to the native element.

The ref identifies this controlled input, but React state still owns its displayed value and the preview.

Match the native element

Choose the element type that matches the actual native control. The ref type describes what may eventually be available through the ref.

Use the DOM interface for the native JSX element.
Native JSX elementRef generic typeEmpty value
<input>HTMLInputElement | nullnull
<textarea>HTMLTextAreaElement | nullnull

Check the ref declaration

Complete the input ref

Complete the declaration and attachment for a native text input.

tsx
const messageRef = useRef< | >();

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

Step 3 of 6

Track When ref.current Is Available

Follow a DOM ref from its initial null value through attachment, retention, and removal, without treating rendering as a DOM-access phase.

See the attachment lifecycle

From empty reference to attached element

A ref begins with its declared empty value. For the attached input, messageRef.current is null during the initial Render. Render calculates JSX; it is not a safe place to read a DOM ref or call a native DOM method.

Afterward, during Commit, React attaches the native input and assigns that element to current. The ref container itself remains stable for the mounted component instance.

The ref lifecycle

A four-stage diagram showing an empty ref, an input attached to it, the input retained through an update, and the ref empty after removal.

Read the flow as Render, Commit, an ordinary rerender, and Removal. React connects and clears the DOM reference as it updates the committed interface.

Do not assume a requested node exists

Rerenders and removal

On an ordinary rerender, the ref container is retained. If the same input remains mounted, its current value continues to refer to that input.

At Removal, when React removes the referenced input, it clears current back to null. A ref does not keep a removed DOM node available.

Example

A request is not an attachment

A user action can request a state change that conditionally includes an input. The handler continues before React has completed the later Render and Commit. Therefore, requesting the input does not make its DOM node available immediately inside that same handler.

Keep DOM reads and DOM manipulation out of Render. Render should describe the interface; React performs the attachment during Commit.

Check the timing

Check the timing assumption

A user action requests state that will include an input that is currently absent. Which statement is accurate during that same handler?

Apply the lifecycle

Order the ref stages

Arrange these stages from earliest to latest for an initially absent conditional input that later remains mounted for one ordinary rerender and is then removed.

  1. User action requests that the conditional input appear; `current` is still `null` in that handler.
  2. Removal detaches the input, and React clears `current` to `null`.
  3. An ordinary rerender retains the same ref container and mounted input.
  4. Commit attaches the input and assigns the element to `current`.
  5. Render calculates the requested interface; no DOM ref access occurs.

Step 4 of 6

Focus from a Deliberate User Action

Implement a null-safe native focus action from the Edit message button while preserving the controlled message and preview.

Choose the deliberate action

Focus is an action, not visible data

The input is already mounted and enabled, so the Edit message button can perform one deliberate imperative action: move focus to that input.

Put this work in the button's event handler—not while React is rendering. Here, focus means that the input becomes the active keyboard target; it does not mean its text is selected.

The input remains controlled by message. Focusing it does not require setMessage, and it does not require changing React-owned DOM text yourself.

Focus moves; rendered data stays

A button points toward a focused text input with a visible focus ring, while a preview area below remains unchanged.

The handler moves focus to the existing input. It does not change the controlled message or its preview.

Implement the interaction

Add a null-safe focus handler

Add the handler and button to the existing controlled-input example. The ref's current can still be null, so optional chaining calls the native method only when an input element is available.

Replace src/App.tsx in your existing local React and TypeScript app with this complete component.

src/App.tsx

tsx
import { useRef, useState } from "react";

export default function App() {
  const [message, setMessage] = useState("Hello, React!");
  const messageRef = useRef<HTMLInputElement | null>(null);

  function handleEditMessage() {
    messageRef.current?.focus();
  }

  return (
    <main>
      <h1>Message draft</h1>

      <label htmlFor="message">Message</label>
      <input
        ref={messageRef}
        id="message"
        type="text"
        value={message}
        onChange={(event) => setMessage(event.currentTarget.value)}
      />

      <button type="button" onClick={handleEditMessage}>
        Edit message
      </button>

      <p>Preview: {message}</p>
    </main>
  );
}
Expected output
Initial browser view:
Message draft
Message
[Hello, React!]
[Edit message]
Preview: Hello, React!

After Edit message is activated:
The input containing "Hello, React!" has visible focus and is the active keyboard target.
The input text remains "Hello, React!".
Preview: Hello, React!

This is the expected browser result. The exact appearance of the native focus indicator varies by browser and CSS, but the input should become the active keyboard target after the button is activated.

Warning

What null-safe means

?. prevents an error when current is null; it does not create an input or guarantee that focus moved. Use this handler for an existing, mounted, enabled control. Do not replace the lifecycle check with a non-null assertion.

Practice the code

Complete the focus connection

Complete the missing pieces that connect the native button to its handler and call the input's native method safely.

tsx
function handleEditMessage() {
  messageRef.current?.();
}

<button type="button" ={handleEditMessage}>
  Edit message
</button>

Check the outcome

Apply and predict

In your local src/App.tsx, complete the button-to-handler connection and the null-safe native call. Then activate Edit message once.

Write the event-handler JSX fragment, the handler line, and a prediction of what changes versus what stays the same for the message and Preview:.

Write at least 60 characters (0/60).

Step 5 of 6

Preserve Native Keyboard Behavior

Use the native button's built-in keyboard behavior while keeping focus movement deliberate and predictable.

Use the browser's built-in keyboard behavior

Keep the native interaction

A native <button> already supports keyboard activation. When Edit message has focus, pressing Enter or Space activates its onClick handler—no separate key handler is needed.

The intended interaction is:

  1. Use Tab to reach Edit message.
  2. Activate it with Enter or Space.
  3. The handler moves focus to the Message input.
  4. Type normally; the controlled value and Preview: update through state.

Afterward, Tab and Shift+Tab should keep their normal navigation behavior.

Keyboard flow

A three-step diagram shows focus moving from a native button to a text input after keyboard activation, followed by typing in the input.

The button keeps native activation; the deliberate action moves focus to the input.

Avoid competing focus behavior

Move focus only when requested

Keep the focus move limited to the deliberate button action. Do not focus the input again while the learner types or whenever React renders: that could interrupt navigation or pull focus away from where the learner placed it.

Also avoid custom activation keys and positive tabIndex values. Native controls already provide the expected keyboard model and focus order.

Example

Native button example

The button needs only its normal click handler. Enter and Space activation are built into the native button.

Use onClick, not custom key handling

Keep this handler and button in your existing src/App.tsx. Do not add an onKeyDown handler just to activate the button.

tsx
function handleEditMessage() {
  messageRef.current?.focus();
}

<button type="button" onClick={handleEditMessage}>
  Edit message
</button>
Expected output
With Edit message focused:
Enter or Space activates handleEditMessage.
Focus moves to the Message input.
The existing message and Preview: do not change until the user types.

Interaction result to check in the browser: first place focus on Edit message with Tab, then activate it with Enter or Space.

Tip

Make the result understandable

A visible focus indicator and the input's associated Message label tell keyboard users both where focus moved and what they can edit.

Check the interaction choice

Choose the accessible behavior

Which implementation best preserves native keyboard interaction for Edit message?

Step 6 of 6

Apply and Verify the Complete Interaction

Complete and locally verify an accessible, typed focus interaction while keeping visible message data in state.

Put the pieces together

Integrate the running example

In your existing local React + TypeScript project, replace src/App.tsx with this complete Message draft example. The input value remains React state; the ref is used only for the deliberate focus action.

Save the file and view the running app in your local browser.

Complete App.tsx

src/App.tsx

tsx
import { useRef, useState } from "react";

export default function App() {
  const [message, setMessage] = useState("Hello, React!");
  const messageRef = useRef<HTMLInputElement | null>(null);

  function handleEditMessage() {
    messageRef.current?.focus();
  }

  return (
    <main>
      <h1>Message draft</h1>

      <label htmlFor="message">Message</label>
      <input
        ref={messageRef}
        id="message"
        type="text"
        value={message}
        onChange={(event) => setMessage(event.currentTarget.value)}
      />

      <button type="button" onClick={handleEditMessage}>
        Edit message
      </button>

      <p>Preview: {message}</p>
    </main>
  );
}
Expected output
Initial browser rendering:
Message
Input value: Hello, React!
Edit message
Preview: Hello, React!

After activating Edit message:
Visible focus: Message input
Input value: Hello, React!
Preview: Hello, React!

After typing a changed message:
Input value: the typed message
Preview: the same typed message

This is the expected browser result, not console output. Your browser's focus indicator styling may differ.

Check the rendered behavior

Verify it with the keyboard

Check the interaction in your local browser:

  1. Confirm that the input initially contains the message and the preview shows the same value.
  2. Use Tab to reach Edit message. Activate it with Enter, then repeat using Space.
  3. After each activation, look for the visible focus indicator on Message. Focus makes it the active keyboard target; it does not select its text.
  4. Type a change. The preview should change with the controlled value.
  5. Use Tab and Shift+Tab afterward. Navigation should continue through the native controls normally.

The button handler moves focus only. It does not replace the current message or write visible content outside React state.

Expected keyboard flow

Diagram of Tab reaching a native button, Enter or Space activating it, focus moving to a message input, and typing updating a preview while normal Tab navigation continues.

Native button activation moves focus to the existing input. Typing afterward updates the state-driven preview.

Final review

The completed interaction has separate responsibilities. State owns message, so React renders the input value and Preview: from one visible source of truth. messageRef identifies the already mounted native input without owning its value. The button handler performs one deliberate focus action, while native controls retain their built-in keyboard behavior.

Summary

Remember the distinction

  • message controls the visible input text and the preview.
  • messageRef identifies the native input without owning its value.
  • Edit message performs one deliberate native focus action.
  • Native Tab, Shift+Tab, Enter, and Space behavior remains available.

Apply the implementation

Complete the integrated focus code

Complete the typed ref declaration, the handler's native method call, and the input attachment.

tsx
import { useRef, useState } from "react";

export default function App() {
  const [message, setMessage] = useState("Hello, React!");
  const messageRef = useRef<>(null);

  function handleEditMessage() {
    messageRef.current?.();
  }

  return (
    <main>
      <h1>Message draft</h1>

      <label htmlFor="message">Message</label>
      <input
        ref={}
        id="message"
        type="text"
        value={message}
        onChange={(event) => setMessage(event.currentTarget.value)}
      />

      <button type="button" onClick={handleEditMessage}>
        Edit message
      </button>

      <p>Preview: {message}</p>
    </main>
  );
}

Confirm the complete interaction

Report your local verification

Run your completed app locally and perform two keyboard passes: activate Edit message once with Enter and once with Space. Then type a changed draft and navigate with Tab and Shift+Tab.

Report what you observed for all of these points: visible focus after each activation, whether the existing message text changed on activation, whether the preview changed while typing, and how Tab and Shift+Tab behaved afterward.

Write at least 220 characters (0/220).

Complete

Congratulations! You completed: Focus Native Controls with React Refs

Complete. You integrated a typed DOM ref for a deliberate native focus action while keeping visible message data in React state.

Download the App now to have access to + 5000 free courses, exercises, certificates and lots of content without paying anything!

  • 100% free online courses from start to finish

    Thousands of online courses in video, ebooks and audiobooks.

  • More than 60 thousand free exercises

    To test your knowledge during online courses

  • Valid free Digital Certificate with QR Code

    Generated directly from your cell phone's photo gallery and sent to your email

Cursa app on the ebook screen, the video course screen and the course exercises screen, plus the course completion certificate