Skip to content
React rc typescript 4 min read

Typing Events & Refs

Event handlers and refs are where React meets the DOM, and they are also where loose typing causes the most pain. Without precise types you lose autocomplete on event.target, you cannot tell which element a ref points at, and null checks get forgotten. This page shows how to type handlers with React’s synthetic event types, how to give useRef a concrete element type, and how to forward refs through your own components.

Typing event handlers

React wraps every native DOM event in a synthetic event — a cross-browser wrapper exposed through generic types like React.ChangeEvent<T>, React.MouseEvent<T>, and React.FormEvent<T>. The generic parameter T is the element the handler is attached to, which is what makes event.target and event.currentTarget strongly typed.

import { ChangeEvent, FormEvent, MouseEvent } from "react";

function SignupForm() {
  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    // event.target is typed as HTMLInputElement, so .value is a string
    console.log(event.target.value);
  };

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    console.log("submitted");
  };

  const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
    console.log(event.currentTarget.disabled);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
      <button type="submit" onClick={handleClick}>
        Send
      </button>
    </form>
  );
}

You rarely have to write these types when the handler is defined inline, because React infers them from the JSX attribute. Annotations matter most when the handler is a standalone function, as above.

Prefer event.currentTarget over event.target inside handlers. currentTarget is always the element the listener is bound to (and is correctly typed as T), whereas target is whatever was actually clicked and may be a child element.

Common event types

React typeFired byTypical element generic
ChangeEvent<T>onChange on inputs, selects, textareasHTMLInputElement, HTMLSelectElement
FormEvent<T>onSubmit, onResetHTMLFormElement
MouseEvent<T>onClick, onMouseEnterHTMLButtonElement, HTMLDivElement
KeyboardEvent<T>onKeyDown, onKeyUpHTMLInputElement
FocusEvent<T>onFocus, onBlurHTMLInputElement

To type an entire handler in one shot — including its return type — use the React.ChangeEventHandler<T> family instead of typing the parameter:

import { ChangeEventHandler } from "react";

const onSearch: ChangeEventHandler<HTMLInputElement> = (event) => {
  console.log(event.target.value);
};

Typing element refs

useRef is generic, and the type you pass determines what ref.current holds. For a DOM ref, pass the element interface and initialize with null, because the element does not exist until React attaches it after the first render.

import { useRef, useEffect } from "react";

function SearchBox() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // inputRef.current is HTMLInputElement | null — guard before use
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} placeholder="Search..." />;
}

The crucial detail is the null argument. useRef<HTMLInputElement>(null) produces a read-only RefObject whose current is HTMLInputElement | null — exactly what a DOM ref needs. If you instead write useRef<number>(0) for a mutable value, current becomes a writable number you can reassign freely.

Output:

// After mount, the input gains focus automatically.

Always optional-chain (ref.current?.method()) or null-check DOM refs. TypeScript includes null in the type precisely because the element may not be mounted yet.

Forwarding refs

When a parent needs a ref to a DOM node inside a child component, the child must forward it. In React 19 a ref can be accepted as a normal prop, but the widely supported pattern uses forwardRef, which takes two generics: the element type and the props type.

import { forwardRef, ComponentProps, useRef } from "react";

type TextInputProps = ComponentProps<"input"> & { label: string };

const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
  ({ label, ...rest }, ref) => (
    <label>
      {label}
      <input ref={ref} {...rest} />
    </label>
  )
);

function Form() {
  const ref = useRef<HTMLInputElement>(null);
  return (
    <form>
      <TextInput ref={ref} label="Email" name="email" />
      <button type="button" onClick={() => ref.current?.focus()}>
        Focus
      </button>
    </form>
  );
}

The first generic (HTMLInputElement) is what the parent’s ref will point to; the second (TextInputProps) describes the component’s props. With React 19 you can drop forwardRef entirely and declare ref: Ref<HTMLInputElement> as a regular prop, but forwardRef remains the safe choice for libraries and older runtimes.

Best Practices

  • Type standalone handlers with React.ChangeEvent<T> / MouseEvent<T> / FormEvent<T>; let inline handlers infer.
  • Set the element generic (<HTMLInputElement>) so event.target / currentTarget are fully typed.
  • Prefer event.currentTarget over event.target for the bound element’s type and identity.
  • Initialize DOM refs with useRef<T>(null) and always optional-chain before touching current.
  • Use the EventHandler aliases when you want to type a handler variable in one annotation.
  • Pass both generics to forwardRef<ElementType, PropsType>, or accept ref as a prop in React 19.
  • Extend ComponentProps<"input"> on forwarding wrappers so native attributes pass straight through.
Last updated June 14, 2026
Was this helpful?