React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

typescriptreacteventsformshandlers

Typing Events

Recipe

Type React event handlers correctly for clicks, form submissions, input changes, keyboard events, and custom event callbacks. Use React's synthetic event types for full type safety.

Working Example

// Click event
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
  console.log("Button clicked at", event.clientX, event.clientY);
}
 
// Form submit event
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
  event.preventDefault();
  const formData = new FormData(event.currentTarget);
  const email = formData.get("email") as string;
  console.log("Submitted:", email);
}
 
// Input change event
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
  console.log("Value:", event.target.value);
}
 
// Keyboard event
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
  if (event.key === "Enter") {
    console.log("Enter pressed");
  }
}
// Full form component
function LoginForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
 
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log({ email, password });
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        onKeyDown={(e) => {
          if (e.key === "Escape") setEmail("");
        }}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Log In</button>
    </form>
  );
}

Deep Dive

How It Works

  • React wraps native DOM events in SyntheticEvent objects. TypeScript provides generic event types like React.MouseEvent<T> where T is the HTML element type.
  • event.target is typed as EventTarget, which is broad. Use event.currentTarget for the element the handler is attached to -- it is typed as the generic parameter (HTMLButtonElement, HTMLInputElement, etc.).
  • When writing inline handlers (onChange={(e) => ...}), TypeScript infers the event type automatically from the JSX attribute.
  • When extracting handlers to named functions, you must annotate the event parameter explicitly.

Variations

Select and textarea:

function handleSelectChange(event: React.ChangeEvent<HTMLSelectElement>) {
  console.log("Selected:", event.target.value);
}
 
function handleTextareaChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
  console.log("Text:", event.target.value);
}

Drag events:

function handleDragStart(event: React.DragEvent<HTMLDivElement>) {
  event.dataTransfer.setData("text/plain", "dragged");
}

Focus events:

function handleFocus(event: React.FocusEvent<HTMLInputElement>) {
  event.currentTarget.select();
}

Custom event callback props:

type SearchBarProps = {
  onSearch: (query: string) => void;
  onClear?: () => void;
};
 
function SearchBar({ onSearch, onClear }: SearchBarProps) {
  const [query, setQuery] = useState("");
 
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    onSearch(query);
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <button type="submit">Search</button>
      {onClear && <button type="button" onClick={onClear}>Clear</button>}
    </form>
  );
}

TypeScript Notes

  • Common event types: React.MouseEvent, React.ChangeEvent, React.FormEvent, React.KeyboardEvent, React.FocusEvent, React.DragEvent, React.TouchEvent, React.WheelEvent.
  • The generic parameter determines the type of currentTarget. For example, React.MouseEvent<HTMLAnchorElement> gives currentTarget the type HTMLAnchorElement.
  • React.SyntheticEvent is the base type for all React events. Use it when you do not care about the specific event type.

Gotchas

  • event.target vs event.currentTarget: target is the element that triggered the event (could be a child), while currentTarget is the element the handler is attached to. TypeScript types currentTarget more precisely.
  • Using event.target.value on a form submit event will error because EventTarget does not have a value property. Cast it or use currentTarget.
  • React's synthetic events are pooled in React 16 and earlier. In React 17+, event pooling is removed, so you can safely access event properties asynchronously.
  • Forgetting event.preventDefault() on form submit causes a full page reload.

Alternatives

ApproachProsCons
Inline arrow functionsTypeScript infers event type automaticallyCreates new function on each render
Named handler functionsReusable, testable, no re-creationMust manually annotate event type
React.EventHandler<E> typeConcise handler type aliasLess common, slightly harder to read
Callback prop (e.g., onSearch: (q: string) => void)Decouples parent from DOM eventsParent does not have access to raw event
useCallback wrapped handlerStable reference for memoized childrenAdditional boilerplate

FAQs

What is the difference between event.target and event.currentTarget?
  • event.target is the element that triggered the event (could be a child element).
  • event.currentTarget is the element the handler is attached to.
  • TypeScript types currentTarget more precisely based on the generic parameter.
Why does TypeScript infer event types automatically for inline handlers but not named functions?
  • Inline handlers like onChange={(e) => ...} get their type from the JSX attribute context.
  • Named functions exist outside that context, so TypeScript cannot infer the event type.
  • You must annotate the event parameter explicitly: function handleChange(e: React.ChangeEvent<HTMLInputElement>).
What are the most common React event types?
  • React.MouseEvent<T> -- clicks, mouse movement.
  • React.ChangeEvent<T> -- input, select, textarea changes.
  • React.FormEvent<T> -- form submissions.
  • React.KeyboardEvent<T> -- key presses.
  • React.FocusEvent<T> -- focus and blur.
Gotcha: Why does accessing event.target.value on a form submit event cause a TypeScript error?
  • event.target is typed as EventTarget, which does not have a value property.
  • Use event.currentTarget (typed as the form element) or cast event.target.
  • For form data, prefer new FormData(event.currentTarget).
How do you type a custom event callback prop?
type SearchBarProps = {
  onSearch: (query: string) => void;
  onClear?: () => void;
};
  • Expose a domain-specific callback (e.g., onSearch(query)) instead of the raw DOM event.
  • This decouples the parent from the child's DOM implementation.
What does the generic parameter in React.MouseEvent<HTMLButtonElement> control?
  • It determines the type of event.currentTarget.
  • React.MouseEvent<HTMLButtonElement> means currentTarget is typed as HTMLButtonElement.
  • Use the HTML element type that matches the element the handler is attached to.
Gotcha: What happens if you forget event.preventDefault() on form submit?
  • The browser performs a full page reload (default form submission behavior).
  • Your SPA state is lost and the form handler's logic may not complete.
  • Always call event.preventDefault() at the top of form submit handlers.
When should you use React.SyntheticEvent as the event type?
  • When you do not care about the specific event type (mouse, keyboard, etc.).
  • It is the base type for all React events.
  • Prefer specific types when you need access to event-specific properties (e.g., clientX, key).
How do you type a handler for a select element?
function handleSelectChange(event: React.ChangeEvent<HTMLSelectElement>) {
  console.log("Selected:", event.target.value);
}
  • Use React.ChangeEvent<HTMLSelectElement> -- not HTMLInputElement.
  • The value property is the string value of the selected option.
Is event pooling still a concern in React 17+?
  • No. React 17 removed event pooling entirely.
  • In React 16 and earlier, synthetic events were recycled, and accessing properties asynchronously required event.persist().
  • In React 17+, you can safely access event properties in async callbacks without persist().