TypeScript con React: componentes tipados, hooks y eventos del DOM

React y TypeScript se han convertido en una de las combinaciones más habituales del desarrollo frontend. Tipar componentes, hooks y eventos del DOM no es complicado una vez que sabes qué tipos usar dónde. Esta guía cubre los patrones más habituales en un proyecto React TypeScript real: componentes funcionales, hooks con tipos, eventos del DOM, custom hooks y forwardRef.

Componentes funcionales: JSX.Element, ReactNode y props opcionales

El tipo de retorno de un componente funcional puede ser JSX.Element, React.ReactElement o React.ReactNode. ReactNode es el más permisivo: incluye strings, numbers, arrays y null, además de JSX:

import React from "react";

interface TarjetaProps {
  titulo: string;
  descripcion?: string;        // prop opcional
  children: React.ReactNode;   // cualquier contenido JSX
  onClick?: () => void;
}

function Tarjeta({ titulo, descripcion, children, onClick }: TarjetaProps) {
  return (
    <div onClick={onClick}>
      <h2>{titulo}</h2>
      {descripcion && <p>{descripcion}</p>}
      {children}
    </div>
  );
}

useState con tipos

useState infiere el tipo del estado a partir del valor inicial. Cuando el valor inicial puede ser null o el tipo es complejo, añade el tipo genérico explícitamente:

import { useState } from "react";

interface Usuario {
  id: number;
  nombre: string;
}

function PerfilUsuario() {
  const [usuario, setUsuario] = useState<Usuario | null>(null);
  const [cargando, setCargando] = useState(false);    // inferido: boolean
  const [contador, setContador] = useState(0);        // inferido: number

  async function cargar(id: number) {
    setCargando(true);
    const datos = await fetch(`/api/usuarios/${id}`).then(r => r.json());
    setUsuario(datos as Usuario);
    setCargando(false);
  }

  return <div>{usuario?.nombre}</div>;
}

useRef: referencias al DOM y valores mutables

useRef tiene dos usos: referencias a elementos del DOM y contenedores de valores mutables que no disparan re-renders. El tipo de genérico varía:

import { useRef, useEffect } from "react";

function CampoConFoco() {
  // Referencia al DOM: tipo del elemento, inicializado a null
  const inputRef = useRef<HTMLInputElement>(null);

  // Valor mutable: tipo del valor, no null
  const contadorRenders = useRef<number>(0);

  useEffect(() => {
    inputRef.current?.focus();
    contadorRenders.current++;
  });

  return <input ref={inputRef} type="text" />;
}

Tipar eventos del DOM

Los tipos de eventos de React están en React.ChangeEvent<T>, React.FormEvent<T>, React.MouseEvent<T>, etc., donde T es el tipo del elemento HTML:

function Formulario() {
  function handleChange(e: React.ChangeEvent<HTMLInputElement>): void {
    console.log(e.target.value);
  }

  function handleSubmit(e: React.FormEvent<HTMLFormElement>): void {
    e.preventDefault();
    // procesar formulario
  }

  function handleClick(e: React.MouseEvent<HTMLButtonElement>): void {
    console.log(`Click en (${e.clientX}, ${e.clientY})`);
  }

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

Custom hooks con tuplas tipadas

function useContador(inicial = 0): [number, () => void, () => void] {
  const [cuenta, setCuenta] = useState(inicial);
  const incrementar = () => setCuenta(c => c + 1);
  const decrementar = () => setCuenta(c => c - 1);
  return [cuenta, incrementar, decrementar];
}

// Uso: TypeScript conoce exactamente los tipos de la tupla
const [cuenta, incrementar, decrementar] = useContador(0);

forwardRef

import { forwardRef } from "react";

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  etiqueta: string;
}

const CampoTexto = forwardRef<HTMLInputElement, InputProps>(
  ({ etiqueta, ...props }, ref) => (
    <label>
      {etiqueta}
      <input ref={ref} {...props} />
    </label>
  )
);

Imagen: Pexels / Stanislav Kondratiev

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP