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:
packages/react/src/ReactHooks.js— Defines the publicuseRefhook that forwards to the reconciler dispatcher.packages/react-reconciler/src/ReactFiberHooks.js— Contains the internalmountRefandupdateRefimplementations that ensure ref stability across renders.packages/react-reconciler/src/__tests__/useRef-test.internal.js— Test suite demonstrating usage patterns includinguseRef(null)anduseRef(undefined).packages/eslint-plugin-react-hooks/__tests__/ReactCompilerRuleTypescript-test.ts— Shows TypeScript-typed refs in real-world patterns (React.useRef<ScrollView>(null)).
Summary
- Create typed refs using
useRef<HTMLElementType>(null)to ensure compile-time safety when accessing DOM properties. - Attach refs to JSX elements via the
refprop, then interact withref.currentinsideuseEffector event handlers. - Guard against null using optional chaining (
?.) or explicit checks, since the ref isnulluntil the component mounts. - Forward refs using
forwardRefwhen building reusable components, and useuseImperativeHandleto expose specific methods to parent components. - Reference the implementation in
ReactHooks.jsandReactFiberHooks.jsto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →