How to Implement a React Ref to Access DOM Elements in TypeScript

Use the useRef hook with a generic type argument (e.g., useRef<HTMLInputElement>(null)) to create a mutable reference that persists across renders without causing re-renders, providing type-safe access to DOM nodes.

The useRef hook is the standard mechanism for accessing DOM elements directly in React function components. When working with TypeScript, properly typing your react ref ensures compile-time safety while interacting with native browser APIs. This guide examines the implementation details from the facebook/react repository to demonstrate type-safe patterns for DOM manipulation.

Understanding the useRef Hook Architecture

React’s useRef hook creates a mutable object whose .current property can hold a reference to a DOM element. According to the source code in packages/react/src/ReactHooks.js, the public useRef hook simply forwards to the reconciler’s dispatcher (dispatcher.useRef(initialValue)).

The internal implementation resides in packages/react-reconciler/src/ReactFiberHooks.js, which contains the mountRef and updateRef functions. These ensure the ref object is created once during the component’s first render, guaranteeing a stable identity (===) for the entire component lifecycle.

Creating a Typed React Ref for DOM Elements

To properly type a ref for a DOM element, pass the element interface as a generic to useRef and initialize with null.

import React, { useRef, useEffect } from 'react';

export function FocusableInput() {
  // Create a ref typed to HTMLInputElement
  const inputRef = useRef<HTMLInputElement>(null);

  // Access the DOM node after mounting
  useEffect(() => {
    // Optional chaining guards against null (e.g., during SSR)
    inputRef.current?.focus();
  }, []);

  // Attach the ref to the JSX element
  return <input type="text" ref={inputRef} placeholder="Auto-focused" />;
}

The generic <HTMLInputElement> tells TypeScript that inputRef.current will be an HTMLInputElement (or null) once the element attaches. This provides autocomplete and type checking for DOM methods like .focus().

Accessing DOM Nodes with React Refs in Event Handlers

Refs are not limited to effects; you can access them directly in event handlers for immediate DOM manipulation.

import React, { useRef } from 'react';

export function ToggleVisibility() {
  const divRef = useRef<HTMLDivElement>(null);

  const toggle = () => {
    if (divRef.current) {
      const el = divRef.current;
      el.style.display = el.style.display === 'none' ? 'block' : 'none';
    }
  };

  return (
    <>
      <button onClick={toggle}>Toggle Visibility</button>
      <div ref={divRef}>Content that can be hidden</div>
    </>
  );
}

This pattern avoids React’s state mechanism for purely presentational changes, preventing unnecessary re-renders while maintaining type safety through the HTMLDivElement generic.

Forwarding React Refs in Custom Components

When building reusable components, use forwardRef to pass the ref through to a DOM element. Combine it with useImperativeHandle to expose a limited API to parent components.

import React, { forwardRef, useImperativeHandle, useRef } from 'react';

type Props = { label: string };

export type FancyInputHandle = {
  focus: () => void;
  clear: () => void;
};

export const FancyInput = forwardRef<FancyInputHandle, Props>((props, ref) => {
  const localRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focus: () => localRef.current?.focus(),
    clear: () => {
      if (localRef.current) {
        localRef.current.value = '';
      }
    },
  }));

  return <input ref={localRef} placeholder={props.label} />;
});

The parent component can then use the ref with full type safety:

const parentRef = useRef<FancyInputHandle>(null);
<FancyInput ref={parentRef} label="Name" />;
parentRef.current?.focus(); // TypeScript knows this method exists

Key Implementation Files in the React Source Code

The following files from the facebook/react repository define and test the ref behavior described above:

Summary

  • Create typed refs using useRef<HTMLElementType>(null) to ensure compile-time safety when accessing DOM properties.
  • Attach refs to JSX elements via the ref prop, then interact with ref.current inside useEffect or event handlers.
  • Guard against null using optional chaining (?.) or explicit checks, since the ref is null until the component mounts.
  • Forward refs using forwardRef when building reusable components, and use useImperativeHandle to expose specific methods to parent components.
  • Reference the implementation in ReactHooks.js and ReactFiberHooks.js to understand how React guarantees ref stability across renders.

Frequently Asked Questions

Why must I initialize useRef with null in TypeScript?

TypeScript requires the initial value to match the generic type. Since the DOM element does not exist until after the first render, null is the only valid initial state. Declaring useRef<HTMLInputElement>(null) tells TypeScript that current will be HTMLInputElement | null, forcing you to handle the null case safely.

What is the difference between useRef and createRef?

useRef is a hook designed for function components; it returns the same mutable object on every render. createRef is typically used in class components and creates a new ref object on every render, which breaks identity comparisons. According to the React source in ReactHooks.js, useRef maintains stability through the fiber architecture, while createRef lacks this optimization.

How do I type a ref for a custom component instead of a DOM element?

Use React.RefObject<YourComponentType> or React.MutableRefObject<YourComponentType> depending on whether the ref holds a mutable value. When forwarding refs to custom components, type the forwardRef generic as forwardRef<RefType, PropsType>. If exposing imperative methods, define an interface for the handle and use useImperativeHandle as shown in the FancyInput example above.

Is it safe to access ref.current during render?

No. React does not guarantee the DOM node exists until after the component mounts. Accessing ref.current during the render phase (before useEffect runs) may return null or stale values. Always interact with refs inside useEffect, useLayoutEffect, or event handlers to ensure the DOM node is attached and ready for manipulation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →