Skip to content

Fundamentos de React JS con TypeScript

Objetivos

  • ¿Qué es React?
  • Comenzar un proyecto con React y TypeScript
  • JSX
  • Componentes
  • Props
  • Hooks, useState, useEffect

WARNING

Extensiones VSCode

React y Themes

TypeScript

¿Qué es React?

React es una biblioteca de JavaScript para construir interfaces de usuario. Permite crear componentes reutilizables que pueden manejar su propio estado y se actualizan de manera eficiente cuando los datos cambian.

Es mantenida por Facebook y una comunidad de desarrolladores.

¿Que es JSX?

JSX es una extensión de sintaxis para JavaScript que permite escribir HTML dentro de JavaScript.

tsx
function VideoList({ videos, emptyHeading }) {
  const count = videos.length;
  let heading = emptyHeading;
  if (count > 0) {
    const noun = count > 1 ? "Videos" : "Video";
    heading = count + " " + noun;
  }
  return (
    <section>
      <h2>{heading}</h2>
      {videos.map((video) => (
        <Video
          key={video.id}
          video={video}
        />
      ))}
    </section>
  );
}

Aunque JSX (o TSX) solo aparece en el return, la razón por la que necesitas usar la extensión .jsx o .tsx es porque ese bloque JSX no es JavaScript válido por sí solo: es una extensión de sintaxis que requiere que el compilador (como Babel o TypeScript) lo reconozca y lo transforme a JavaScript real.

Ejemplo:

Este JSX:

tsx
return <h1>Hola</h1>;

Es transformado por herramientas como Babel a algo como:

js
return React.createElement("h1", null, "Hola");

¿Por qué importa la extensión .jsx o .tsx?

Los compiladores como Babel, TypeScript, Vite, Webpack, etc., usan la extensión para saber:

  • Si deben parsear JSX y convertirlo a React.createElement(...).
  • Si deben aplicar transformaciones especiales.

Si usas .js o .ts, el compilador podría no reconocer JSX (dependiendo de la configuración) y lanzar un error como:

SyntaxError: Unexpected token '<'

Instalación de React y TypeScript

  1. React + Vite + TypeScript
  2. React Router v7 + Vite (antes Remix)
  3. Next.js (con su App Router moderno y SWC)

🧩 Comparativa: Vite vs React Router v7 vs Next.js

CaracterísticaVite + React + TSReact Router v7 + ViteNext.js (App Router)
⚙️ Tooling baseVite + EsbuildVite + EsbuildWebpack (o Turbopack), SWC
🔁 EnrutamientoManual (usualmente con React Router)Automático y basado en archivos (routes/)Automático basado en carpetas (app/)
🧠 AprendizajeMás simple, ideal para comenzarIntermedio: estructura y loadersComplejo: muchas convenciones y APIs
🚀 RenderizadoSolo ClienteClient-Side + Data LoadersSSR, SSG, ISR, Client-Side
🗂️ Páginas por archivos❌ (manual)✅ (como Remix)✅ (App Router y Pages Router)
📦 Backend/API Routes❌ (solo client-side, o conectar backend externo)✅ (puedes crear endpoints en /api)
📄 Form Handling (actions/loaders)❌ (tú manejas todo)✅ Muy similar a Remix✅ con actions, server actions (experimental)
🔒 Protección de rutas (auth)ManualMás estructurada con loadersIntegrada fácilmente con middlewares
🧪 TestingCompatible con Vitest / JestCompatibleCompatible
🎨 CSS / StylingLibre (Tailwind, CSS, etc.)LibreLibre
🧩 Plugins y EcosistemaFlexibleMás nuevo, pequeño ecosistemaEcosistema enorme y consolidado
🧠 Mental model (arquitectura)React puro“React con Routing + Loader”Fullstack Framework (SSR + API + routing)
🧱 Ideal para…Aprender React, apps SPA simplesApps SPA modernas con mejor estructuraApps empresariales, SSR, SEO, etc.

📝 Conclusiones rápidas:

  • 🔰 ¿Eres principiante en React?Vite + React + TS es tu mejor opción.
  • 🔄 ¿Te gustaba Remix y quieres estructura moderna en Vite?React Router v7 + Vite.
  • 🧩 ¿Quieres construir una app completa con SEO, SSR y backend integrado?Next.js.

✅ Si estás recién comenzando con React: empieza con Vite.

¿Por qué?

  • Vite es más simple y directo.
  • No necesitas entender rutas, SSR, o estructuras complejas.
  • Puedes concentrarte en aprender React puro: componentes, props, estado, hooks, etc.
  • Inicia rápido, se configura en segundos y es liviano.

¿Cuándo pasar a Next.js?

  • Cuando necesites características avanzadas como SSR, rutas dinámicas, API routes, o optimización de imágenes. Aunque también puedes hacer estas cosas con Vite, Next.js las hace más fácil y estructurada.
  • Cuando quieras construir aplicaciones más complejas y necesites un framework que te ayude con la estructura y las mejores prácticas.
  • SEO y rendimiento: Next.js maneja mejor el SEO y la carga inicial de la página, lo que es crucial para aplicaciones más grandes. Aunque Vite con React Router V.7 también lo puede hacer.

🧩 ¿Qué son TanStack y SWC?

  • TanStack es una colección moderna de bibliotecas poderosas para el ecosistema de React (aunque muchas también funcionan en otros frameworks). Incluye herramientas como:

    • @tanstack/react-query: manejo de data fetching y caching (ideal para reemplazar useEffect + fetch).
    • @tanstack/router: un nuevo router que es reactivo, estático, y basado en archivos (como Next.js, pero más flexible).
    • Otras como Table, Virtual, Form, Charts, etc.

TanStack Router es considerado por muchos como el sucesor conceptual de Remix, y es más potente y flexible que React Router para ciertos casos, pero también más avanzado.

  • SWC (Speedy Web Compiler) es un compilador moderno escrito en Rust, utilizado por default en Next.js y algunas otras herramientas. Reemplaza a Babel y es más rápido y más eficiente. Se encarga de:

    • Transpilar JSX y TypeScript.
    • Optimizar el código en tiempo de compilación.
    • Apoyar características modernas como server actions en Next.js.

Inicio Rápido 80% de los conceptos de React

Qué aprenderás

  • Cómo crear y anidar componentes
  • Cómo añadir marcado y estilos
  • Cómo mostrar datos
  • Cómo renderizar condicionales y listas
  • Cómo responder a eventos y actualizar la pantalla
  • Cómo compartir datos entre componentes

Instalación de Vite

Node.js V.18

Asegúrate de tener Node.js V.18 o superior instalado en tu máquina. Puedes verificar la versión con:

sh
node -v
sh
npm create vite@latest .

¿Que es eslint?

ESLint es una herramienta de análisis estático de código para identificar y reportar patrones problemáticos en el código JavaScript. Ayuda a mantener un código limpio y consistente, y puede configurarse para seguir las mejores prácticas de codificación.

main.tsx

En simples palabras, el archivo main.tsx es el punto de entrada principal de tu aplicación React: se encarga de mostrar tu componente <App /> dentro del HTML, específicamente en el elemento con id="root". Usa createRoot() para arrancar React en modo moderno y envuelve todo en <StrictMode>, que ayuda a detectar errores comunes durante el desarrollo.

🔤 Convenciones comunes para nombrar archivos de componentes:

Las convenciones para nombrar archivos de componentes suelen depender del estilo del proyecto o del equipo. Aquí te dejo las más comunes y sus nombres:

  1. PascalCase (también llamada UpperCamelCase):

    • Ejemplo: UserCard.tsx, MyComponent.vue
    • Uso común: En proyectos de React, Vue (con TypeScript) y librerías donde cada componente es único y reutilizable.
    • 📛 Se llama PascalCase porque cada palabra comienza con mayúscula.
  2. kebab-case:

    • Ejemplo: user-card.vue, my-component.ts
    • Uso común: Muy común en Vue.js (especialmente en templates), también en configuraciones, nombres de rutas, etc.
    • 📛 Se llama kebab-case porque parece una brocheta de palabras separadas por guiones.
  3. camelCase:

    • Ejemplo: userCard.js
    • Uso común: No es muy común para archivos, pero se usa mucho para nombres de variables y funciones.
  4. snake_case:

    • Ejemplo: user_card.js
    • Uso común: Más habitual en proyectos Python o bases de datos, poco común en archivos de componentes web.

Fragment

Un Fragment es una forma de agrupar múltiples elementos sin añadir un nodo extra al DOM. Es útil cuando necesitas devolver varios elementos desde un componente sin envolverlos en un contenedor adicional.

tsx
const App = () => {
  return (
    <>
      <h1>Hola App</h1>
      <MyButton />
    </>
  );
};
export default App;

function MyButton() {
  return <button>Click me</button>;
}

ClassName

tsx
import "./button.css";

const MyButton = () => {
  return <button className="btn">Click me export</button>;
};
export default MyButton;

src\components\button.css

css
.btn {
  padding: 10px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}
.btn:hover {
  background-color: #0056b3;
}

Mostrar datos

JSX te permite insertar variables y expresiones de JavaScript directamente dentro del marcado. Para mostrar datos, simplemente usa llaves {} para envolver la expresión.

Cuando quieras utilizarlo en las propiedades de un elemento, debes reemplazar las "" por llaves {}.

tsx
import MyButton from "./components/MyButton";

const App = () => {
  const user = {
    username: "Ignacio",
    imageUrl: "https://i.pravatar.cc/150?img=3",
    imageSize: 90,
  };

  // Desestructuring de objetos
  const { username, imageUrl, imageSize } = user;

  return (
    <>
      <div>
        <h1>Hola: {username}</h1>
        <img
          src={imageUrl}
          alt={`imagen-${username}`}
          style={{
            width: imageSize,
            height: imageSize,
            borderRadius: "50%",
          }}
        />
      </div>
      <MyButton />
      <br />
      <p>Lorem, ipsum dolor.</p>
    </>
  );
};
export default App;

En JSX, cuando usas la prop style, debes pasarle un objeto de JavaScript donde las propiedades de CSS se escriben en camelCase (no con guiones).

Por ejemplo:

jsx
<img
  src={imageUrl}
  alt={`imagen-${username}`}
  width={imageSize}
  style={{
    borderRadius: 100, // border-radius → borderRadius
    backgroundColor: "red", // background-color → backgroundColor
    marginTop: 10, // margin-top → marginTop
  }}
/>

¿Puedes poner cualquier propiedad de CSS?

Casi todas las propiedades de CSS estándar pueden usarse, pero con estas reglas:

  • Propiedad en camelCase:
    font-sizefontSize,
    background-colorbackgroundColor
  • Valores:
    • Si la propiedad espera un valor con unidades (ej: "10px"), puedes ponerlo como string:
      marginLeft: "10px"
    • Si es solo un número y CSS acepta solo números (como opacity, zIndex, etc.), puedes pasar un número directamente:
      opacity: 0.5
    • Para la mayoría de tamaños, si pasas solo un número, React lo interpreta como píxeles:
      marginTop: 10 equivale a "10px"

Ejemplo completo:

jsx
<div
  style={{
    backgroundColor: "blue",
    color: "white",
    padding: "20px",
    borderRadius: 10,
    border: "2px solid red",
    fontWeight: "bold",
  }}
>
  Hola mundo!
</div>

Renderizado condicional

En React, puedes renderizar contenido condicionalmente usando operadores lógicos (if else) o ternarios. Aquí hay dos formas comunes de hacerlo:

tsx
import MyButton from "./components/MyButton";

const App = () => {
  const user = {
    username: "Ignacio",
    imageUrl: "https://i.pravatar.cc/150?img=3",
    imageSize: 90,
    loggedIn: false,
  };

  // Desestructuring de objetos
  const { username, imageUrl, imageSize, loggedIn } = user;

  if (!loggedIn) {
    return <h1>Por favor, inicia sesión</h1>;
  }

  return (
    <>
      <div>
        <h1>Hola: {username}</h1>
        <img
          src={imageUrl}
          alt={`imagen-${username}`}
          style={{
            width: imageSize,
            height: imageSize,
            borderRadius: "50%",
          }}
        />
      </div>
      <MyButton />
      <br />
      <p>Lorem, ipsum dolor.</p>
    </>
  );
};
export default App;

Operador ternario y short-circuit operator (&&)

tsx
import MyButton from "./components/MyButton";

const App = () => {
  const user = {
    username: "Ignacio",
    email: "ignacio@mail.com",
    imageUrl: "https://i.pravatar.cc/150?img=3",
    imageSize: 90,
    isLoggedIn: true,
  };

  const { username, email, imageSize, imageUrl, isLoggedIn } = user;

  if (!isLoggedIn) {
    return (
      <>
        <h2>Usuario no existe</h2>
        <p>Por favor inicia sesión</p>
      </>
    );
  }

  return (
    <>
      {/* <h1>
        Hola!:{" "}
        {username ? username : <button>Agregar nombre de usuario</button>}
      </h1> */}
      {username && <h1>Hola!: {username}</h1>}
      <h2>{email}</h2>
      <img
        src={imageUrl}
        alt={`imagen-${username}`}
        width={imageSize}
        style={{
          borderRadius: 100,
        }}
      />
      <MyButton />
    </>
  );
};
export default App;

Valores falsy

En JavaScript, los valores falsy son aquellos que se consideran falsos cuando se evalúan en un contexto booleano. Estos valores son:

ValorTipoDescripción
falseBooleanEl booleano falso literal
0NumberCero numérico
-0NumberCero negativo
0nBigIntCero en BigInt
""StringCadena vacía
nullObjectAusencia de valor
undefinedUndefinedVariable sin definir
NaNNumberResultado inválido de una operación numérica

Listas

Nota que <li> tiene un atributo key (llave). Para cada elemento en una lista, debes pasar una cadena o un número que identifique ese elemento de forma única entre sus hermanos. Usualmente, una llave debe provenir de tus datos, como un ID de una base de datos. React dependerá de tus llaves para entender qué ha ocurrido si luego insertas, eliminas o reordenas los elementos.

tsx
const App = () => {
  const products = [
    { title: "Col", id: 1 },
    { title: "Ajo", id: 2 },
    { title: "Manzana", id: 3 },
  ];

  return (
    <>
      <ul>
        <li>{products[0].title}</li>
        <li>{products[1].title}</li>
        <li>{products[2].title}</li>
      </ul>
    </>
  );
};
export default App;
tsx
const Products = () => {
  const products = [
    { title: "Col", id: 1 },
    { title: "Ajo", id: 2 },
    { title: "Manzana", id: 3 },
  ];

  return (
    <div>
      <h2>Products</h2>
      <ul>
        {products.map((product) => (
          <li key={product.id}>{product.title}</li>
        ))}
      </ul>
    </div>
  );
};
export default Products;

Props

Las props (abreviatura de "properties") son una forma de pasar datos y funciones a los componentes en React. Son similares a los atributos de HTML, pero más poderosas porque pueden ser cualquier tipo de dato: cadenas, números, objetos, funciones, etc.

tsx
import ProductItem from "./components/ProductItem";

const App = () => {
  const products = [
    { title: "Col", id: 1 },
    { title: "Ajo", id: 2 },
    { title: "Manzana", id: 3 },
  ];

  return (
    <>
      <ul>
        {products.map((item) => (
          <ProductItem
            key={item.id}
            title={item.title}
            id={item.id}
          />
        ))}
      </ul>
    </>
  );
};
export default App;
tsx
// interface Props {
//   title: string;
//   id: number;
// }

type Props = {
  title: string;
  id: number;
};

const ProductItem = ({ id, title }: Props) => {
  // console.log(props);
  // const { title, id } = props;

  return (
    <li>
      {id} - {title}
    </li>
  );
};
export default ProductItem;

interface vs type

Tanto interface como type pueden usarse para definir la forma de los props en TypeScript. Por ejemplo, tu código funcionaría igual así:

typescript
type Props = {
  title: string;
  id: number;
};

¿Cuál es mejor?

  • Para props de componentes, ambos funcionan bien y la elección es más de estilo o preferencia.
  • interface es más usada para objetos y clases, y se puede extender fácilmente.
  • type es más flexible (puede unir tipos, alias para primitivos, uniones, etc).

Diferencias clave (tabla comparativa):

Característicainterfacetype
Extensión (herencia)Sí, con extendsSí, con & (intersección de tipos)
Declaración múltipleSí, se fusionanNo, da error si se redeclara
Uniones (A | B)No
Alias a primitivosNo
Implementación en clasesNo directamente
Usado para objetos
Usado para utilidades avanzadasNoSí (Mapped types, Conditional types)

Resumen breve:

  • Usa interface si solo defines la forma de un objeto o clase y posiblemente quieras extenderlo.
  • Usa type para alias de tipos más complejos (uniones, primitivos, utilidades avanzadas).
  • Para props simples, ambos son válidos.

Eventos

Puedes responder a eventos del DOM, como clics, cambios de entrada, etc., usando props especiales en JSX. Por ejemplo, para manejar un clic en un botón, puedes usar la prop onClick.

No necesitas pasarle los paréntesis a la función del evento. Si lo haces, se ejecutará inmediatamente al renderizar el componente, en lugar de esperar al evento. Por lo tanto solo le pasas la referencia a la función, sin paréntesis.

tsx
import "./button.css";

const MyButton = () => {
  // Función controladora
  const handleClick = () => {
    alert("me diste click");
  };

  return (
    <button
      className="btn"
      // Referencia a la función sin paréntesis
      onClick={handleClick}
    >
      Click me export
    </button>
  );
};
export default MyButton;

¿Qué pasa si necesitas pasarle un argumento a la función del evento?

tsx
import "./button.css";
const MyButton = () => {
  const handleClick = (message: string) => {
    console.log(message);
  };

  return (
    <button
      className="btn"
      // Usamos una función anónima para pasar el argumento
      onClick={() => handleClick("Button clicked with argument!")}
    >
      Click me export
    </button>
  );
};
export default MyButton;

En este caso, usamos una función anónima para envolver la llamada a handleClick, permitiéndonos pasarle el argumento "Button clicked with argument!" sin ejecutarla inmediatamente.

1. ¿Por qué se usa "handle" en el nombre de la función controladora?

En inglés, "handle" significa manejar o gestionar.
En React (y en programación en general), es una convención nombrar funciones que responden a eventos como handleClick, handleSubmit, etc.
Esto deja claro que la función maneja lo que ocurre cuando sucede ese evento.

Ejemplos:

  • handleClick — maneja cuando se hace clic.
  • handleChange — maneja cuando algo cambia.
  • handleSubmit — maneja cuando se envía un formulario.

2. ¿Qué es onClick? ¿Es una característica de JSX?

Sí, onClick es una prop especial en JSX que se usa para asignar una función que se ejecutará cuando el usuario haga clic en el elemento.

  • En HTML puro existe el atributo onclick, pero en React usamos onClick (camelCase).
  • Es parte de los synthetic events de React, que funcionan de manera similar a los eventos nativos de DOM pero con compatibilidad cross-browser.

3. ¿Qué otros eventos existen y se usan frecuentemente?

Hay muchos eventos en JSX/React, la mayoría similares a los de HTML pero en camelCase. Aquí algunos de los más usados:

Evento JSX¿Cuándo ocurre?Ejemplo de función
onClickAl hacer clichandleClick
onChangeAl cambiar valor (input, select, etc)handleChange
onSubmitAl enviar formulariohandleSubmit
onMouseEnterAl pasar el mouse encimahandleMouseEnter
onMouseLeaveAl sacar el mouse del áreahandleMouseLeave
onFocusAl enfocar un inputhandleFocus
onBlurAl perder el foco un inputhandleBlur
onKeyDownAl presionar una teclahandleKeyDown
onKeyUpAl soltar una teclahandleKeyUp
onDoubleClickAl hacer doble clichandleDoubleClick

useState

El hook useState es una función que te permite añadir estado a tus componentes funcionales en React.

Te devuelve un par: el valor actual del estado y una función para actualizarlo.

¿Qué es un estado en React?

El estado es un objeto que representa la información que puede cambiar en tu componente. Cuando el estado cambia, React vuelve a renderizar el componente para reflejar esos cambios en la interfaz de usuario.

Destructuring

El destructuring de array es una sintaxis de JavaScript que te permite extraer valores de un array y asignarlos a variables individuales.

ts
const fruitsArray = ["🍎", "🍐", "🍉"];

// console.log(fruitsArray[0]);
// console.log(fruitsArray[1]);
// console.log(fruitsArray[2]);

// const fruta1 = fruitsArray[0];
// const fruta2 = fruitsArray[1];
// const fruta3 = fruitsArray[2];

const [fruta1, fruta2, fruta3] = fruitsArray;

console.log(fruta1);
console.log(fruta2);
console.log(fruta3);

Lo mismo pasa con los objetos:

ts
const user = {
  name: "Ignacio",
  age: 30,
};

// console.log(user.name);
// console.log(user.age);
const { name, age } = user;
console.log(name);
console.log(age);

Ejemplo de useState

tsx
import { useState } from "react";

// destructuring de array
// const fruitsArray = ["🍐", "🍎", "🍉"]
// const [pera, manzana, sandia]  = fruitsArray

const CounterButton = () => {
  // const counter = useState(0)[0]
  // const setCounter = useState(0)[1]

  const [counter, setCounter] = useState(0);

  const handleIncrementCounter = () => {
    const newValue = counter + 1;
    setCounter(newValue);
  };

  return <button onClick={handleIncrementCounter}>{counter}</button>;
};
export default CounterButton;

Si renderizas el mismo componente varias veces, cada uno obtendrá su propio estado. Intenta hacer clic independientemente en cada botón:

tsx
import CounterButton from "./components/CounterButton";

const App = () => {
  return (
    <>
      <h1>Mis contadores</h1>
      <CounterButton />
      <CounterButton />
    </>
  );
};
export default App;

Nota que cada botón “recuerda” su propio estado count y que no afecta a otros botones.

El uso de los Hooks

Los hooks son funciones especiales que te permiten "engancharte" a las características de React, como el estado y el ciclo de vida, desde componentes funcionales. Los hooks deben ser llamados en el nivel superior de tu componente, no dentro de bucles, condiciones o funciones anidadas.

Compartir datos entre componentes

Los Props (Propiedades) son la forma en que los componentes de React pueden recibir datos de sus padres. Puedes pensar en ellos como los atributos de un elemento HTML, pero en React, son más poderosos porque pueden ser cualquier tipo de dato: cadenas, números, objetos, funciones, etc.

tsx
import CounterButton from "./components/CounterButton";
import Title from "./components/Title";

const App = () => {
  const user = {
    name: "Juan",
    age: 30,
    email: "juan@test.com",
  };

  return (
    <>
      <Title
        message="Mis queridos contadores"
        user={user}
      />
      <CounterButton />
      <CounterButton />
    </>
  );
};
export default App;
tsx
interface Props {
  message: string;
  user: {
    name: string;
    age: number;
    email: string;
  };
}

const Title = (props: Props) => {
  console.log(props);

  return (
    <h1>
      {props.message} de: {props.user.email}
    </h1>
  );
};
export default Title;

Con destructuring:

tsx
interface Props {
  message: string;
  user: {
    name: string;
    age: number;
    email: string;
  };
}

const Title = ({ message, user }: Props) => {
  return (
    <h1>
      {message} de: {user.email}
    </h1>
  );
};
export default Title;

Volviendo a nuestro ejemplo de CounterButton

Esto se llama “levantar el estado”. Al mover el estado hacia arriba, lo compartimos entre componentes.

tsx
import { useState } from "react";
import CounterButton from "./components/CounterButton";
import Title from "./components/Title";

const App = () => {
  const user = {
    name: "Juan",
    age: 30,
    email: "juan@test.com",
  };

  const [counter, setCounter] = useState(0);

  const handleIncrementCounter = () => {
    setCounter(counter + 1);
  };

  return (
    <>
      <Title
        message="Mis queridos contadores"
        user={user}
      />
      <CounterButton
        counter={counter}
        handleIncrementCounter={handleIncrementCounter}
      />
      <CounterButton
        counter={counter}
        handleIncrementCounter={handleIncrementCounter}
      />
    </>
  );
};
export default App;
tsx
import "./button.css";

interface Props {
  counter: number;
  handleIncrementCounter: () => void;
}

const CounterButton = ({ counter, handleIncrementCounter }: Props) => {
  return (
    <button
      className="btn"
      onClick={handleIncrementCounter}
    >
      {counter}
    </button>
  );
};
export default CounterButton;

Formularios

Formularios no controlados vs controlados

En React, puedes manejar formularios de dos maneras: no controlados y controlados.

  • Formularios no controlados: El valor del formulario se maneja directamente a través del DOM. No necesitas usar el estado de React para controlar el valor del input.
  • Formularios controlados: El valor del formulario se maneja a través del estado de React. El valor del input se almacena en el estado del componente y se actualiza a través de eventos como onChange.

Formulario no controlado

  • useRef: Es un hook que te permite acceder directamente a un elemento del DOM sin necesidad de usar el estado de React. Es útil para formularios no controlados, donde no necesitas que React vuelva a renderizar el componente cada vez que cambia el valor del input.
tsx
import { useRef, type FormEvent } from "react";

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

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    console.log(inputRef.current?.value); // Optional Chaining Operator
  };

  return (
    <div>
      <h1>App</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          ref={inputRef}
        />
        <button type="submit">Agregar</button>
      </form>
    </div>
  );
};
export default App;

FormData

El objeto FormData es una forma de construir un conjunto de pares clave/valor representando los campos de un formulario y sus valores.

Nota sobre checkboxes:

  • Si el checkbox está marcado, formData.get("acepta") será "on" (o el valor del atributo value si se lo das).
  • Si no está marcado, será null.
tsx
import { useRef, type FormEvent } from "react";

const App = () => {
  const formRef = useRef<HTMLFormElement>(null);

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();

    if (!formRef.current) return;

    // Create the FormData object from the form
    const formData = new FormData(formRef.current);

    // Get values using get()
    const username = formData.get("username");
    const color = formData.get("color");
    const accept = !!formData.get("accept"); // Convert to boolean

    console.log({
      username,
      color,
      accept,
    });
  };

  return (
    <div>
      <h1>App</h1>
      <form
        onSubmit={handleSubmit}
        ref={formRef}
      >
        <input
          type="text"
          name="username"
          placeholder="Your username"
        />

        <br />

        <select
          name="color"
          defaultValue=""
        >
          <option
            value=""
            disabled
          >
            Choose a color
          </option>
          <option value="red">Red</option>
          <option value="blue">Blue</option>
          <option value="green">Green</option>
        </select>

        <br />

        <label>
          <input
            type="checkbox"
            name="accept"
          />
          I accept the terms
        </label>

        <button type="submit">Submit</button>
      </form>
    </div>
  );
};
export default App;

Formularios controlados

En React, los formularios controlados son aquellos en los que el valor de los campos del formulario es controlado por el estado del componente. Esto significa que el valor del campo se almacena en el estado y se actualiza a través de eventos como onChange.

Ejemplo:

tsx
import { useState } from "react";

const Form = () => {
  const [text, setText] = useState("");

  return (
    <div>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <p>{text}</p>
    </div>
  );
};
export default Form;

OnChange es un evento que se dispara cada vez que el valor de un campo de formulario cambia. Puedes usarlo para actualizar el estado del componente con el nuevo valor del campo.

value El atributo value controla el contenido del input. En este caso, su valor siempre es igual al estado text. Así, el input depende del estado de React.

onSubmit

El atributo onSubmit en el <form> le dice a React qué función ejecutar cuando el usuario envía el formulario (por ejemplo, al hacer click en el botón "Agregar" o presionar Enter). En este caso, llama a la función handleSubmit.

tsx
import { useState, type FormEvent } from "react";

const App = () => {
  const [text, setText] = useState("");

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    console.log(e);
  };

  return (
    <div>
      <h1>App</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
        />
        <button type="submit">Agregar</button>
      </form>
      <h2>{text}</h2>
    </div>
  );
};
export default App;

De no controlado a controlado

tsx
import { useState, type FormEvent } from "react";

const App = () => {
  const [username, setUsername] = useState("");
  const [color, setColor] = useState("");
  const [accept, setAccept] = useState(false);

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();

    console.log({ username, color, accept });
  };

  return (
    <div>
      <h1>App</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          placeholder="Your username"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
        />

        <br />

        <select
          value={color}
          onChange={(e) => setColor(e.target.value)}
        >
          <option
            value=""
            disabled
          >
            Choose a color
          </option>
          <option value="red">Red</option>
          <option value="blue">Blue</option>
          <option value="green">Green</option>
        </select>

        <br />

        <label>
          <input
            type="checkbox"
            checked={accept}
            onChange={(e) => setAccept(e.target.checked)}
          />
          I accept the terms
        </label>

        <button type="submit">Submit</button>
      </form>
    </div>
  );
};
export default App;

Validaciones con Bootstrap

sh
npm i bootstrap@5.3.7

src\index.css

css
@import "bootstrap/dist/css/bootstrap.min.css";

Código:

tsx
import { useState, type FormEvent } from "react";

const App = () => {
  const [username, setUsername] = useState("");
  const [color, setColor] = useState("");
  const [accept, setAccept] = useState(false);

  // Estado para saber si el usuario ya interactuó con cada campo
  const [touched, setTouched] = useState({
    username: false,
    color: false,
    accept: false,
  });

  // Función para marcar como campo "tocado"
  // (field: keyof typeof touched) -> forma automática
  type Field = "username" | "color" | "accept";
  const handleBlur = (field: Field) => {
    setTouched((prev) => ({
      ...prev,
      [field]: true,
    }));
  };

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();

    // Marca todos como tocados al intentar enviar
    setTouched({
      username: true,
      color: true,
      accept: true,
    });

    // procesar el formulario
    if (username.length >= 3 && color !== "" && accept) {
      alert("Formulario enviado correctamente");
    }
  };

  return (
    <div className="container mx-auto">
      <h1>App</h1>
      <form onSubmit={handleSubmit}>
        <div className="mb-3">
          <input
            type="text"
            placeholder="Your username"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
            onBlur={() => handleBlur("username")}
            className={`
            form-control
            ${
              touched.username &&
              (username.length >= 3 ? "is-valid" : "is-invalid")
            }
            `}
          />

          <div className="invalid-feedback">
            El nombre de usuario tiene que tener mínimo 3 carácteres
          </div>
        </div>

        <div className="mb-3">
          <select
            value={color}
            onChange={(e) => setColor(e.target.value)}
            onBlur={() => handleBlur("color")}
            className={`form-select ${
              touched.color && (color !== "" ? "is-valid" : "is-invalid")
            }`}
          >
            <option
              value=""
              disabled
            >
              Choose a color
            </option>
            <option value="red">Red</option>
            <option value="blue">Blue</option>
            <option value="green">Green</option>
          </select>

          <div className="invalid-feedback">Seleccionar un color</div>
        </div>

        <div className="mb-3 form-check">
          <input
            type="checkbox"
            checked={accept}
            onChange={(e) => setAccept(e.target.checked)}
            onBlur={() => handleBlur("accept")}
            className={`form-check-input ${
              touched.accept && (accept ? "is-valid" : "is-invalid")
            }`}
          />
          <label className="form-check-label">I accept the terms</label>

          <div className="invalid-feedback">Accepta los términos</div>
        </div>

        <button
          type="submit"
          className="btn btn-primary"
        >
          Submit
        </button>
      </form>
    </div>
  );
};
export default App;

Algunas notas:

  • onBlur es un evento en React (y en HTML) que se activa cuando un elemento pierde el foco, es decir, cuando el usuario deja de interactuar con ese campo (por ejemplo, al hacer clic fuera de un input)

keyof typeof

  • typeof touched obtiene el tipo del objeto touched (por ejemplo: { username: boolean, color: boolean, accept: boolean }).
  • keyof typeof touched obtiene los nombres de las claves (keys) de ese objeto como un tipo.
    → Es decir: "username" | "color" | "accept"

¿Para qué sirve?
Eso significa que el parámetro field solo puede ser "username", "color" o "accept".
Así, la función solo acepta esos valores, evitando errores y ayudando con el autocompletado en TypeScript.

En TypeScript, typeof sirve para obtener el tipo de una variable u objeto en tiempo de compilación, no su valor. No devuelve un objeto, sino el tipo (como si fuera una interfaz generada automáticamente a partir de esa variable).

Ejemplo:

typescript
const persona = {
  nombre: "Juan",
  edad: 30,
};

// Así obtienes el tipo de persona:
type PersonaType = typeof persona;

Esto es equivalente a haber escrito manualmente:

typescript
type PersonaType = {
  nombre: string;
  edad: number;
};

useEffect

El hook useEffect te permite realizar efectos secundarios en componentes funcionales. Es una forma de manejar tareas que no están directamente relacionadas con el renderizado, como:

  • Llamadas a APIs
  • Suscripciones a eventos
  • Manipulación del DOM

¿Qué es un efecto secundario?

Un efecto secundario es cualquier acción que ocurre como resultado de un renderizado, pero que no afecta directamente al renderizado en sí. Por ejemplo, si haces una llamada a una API para obtener datos y luego actualizas el estado con esos datos, eso es un efecto secundario. El renderizado en sí no cambia, pero el estado del componente sí.

¿Cómo funciona useEffect?

El hook useEffect toma dos argumentos:

  1. Una función que contiene el código del efecto secundario.
  2. Un array de dependencias (opcional) que le dice a React cuándo debe ejecutar el efecto.

Si el array de dependencias está vacío, el efecto se ejecutará solo una vez, después del primer renderizado del componente. Si el array contiene variables, el efecto se ejecutará cada vez que esas variables cambien.

Ejemplo básico de useEffect

tsx
import { useState, useEffect } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("El componente se ha montado o actualizado");
    document.title = `Count: ${count}`;

    // Cleanup function (opcional)
    return () => {
      console.log("El componente se va a desmontar o actualizar");
    };
  }, [count]); // El efecto se ejecuta cada vez que 'count' cambia

  return (
    <div>
      <h1>Contador: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Incrementar</button>
    </div>
  );
};
export default App;

¿Por qué veo dos logs?

Si ves que se repiten tus logs, es porque está activado strict mode en React. En producción no deberías verlo.

más info stric mode

js
createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <App />
  </StrictMode>
);

¿Qué es el array de dependencias?

El array de dependencias es una lista de variables que el efecto depende. Si alguna de estas variables cambia, el efecto se volverá a ejecutar. Si el array está vacío, el efecto solo se ejecutará una vez, después del primer renderizado del componente.

Efectos de limpieza

A veces, es necesario limpiar los efectos secundarios antes de que el componente se desmonte o antes de que el efecto se vuelva a ejecutar. Para esto, puedes devolver una función de limpieza desde el efecto.

src\components\Modal.tsx

tsx
import { useEffect } from "react";

const Modal = () => {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log("Hola cada segundo!");
    }, 1000);

    return () => {
      clearInterval(timer);
      console.log("Temporizador detenido");
    };
  }, []);

  return <div className="alert alert-danger">Modal</div>;
};
export default Modal;
tsx
import { useState } from "react";
import Modal from "./components/Modal";

const App = () => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div className="container mx-auto">
      <h1>useEffect</h1>
      <button
        className={`btn btn-${!isOpen ? "success" : "warning"}`}
        onClick={() => setIsOpen(!isOpen)}
      >
        {!isOpen ? "Abrir" : "Cerrar"} modal
      </button>

      <div className="my-5">{isOpen && <Modal />}</div>
    </div>
  );
};

export default App;

Llamadas a APIs con useEffect

Para hacer llamadas a APIs, puedes usar fetch dentro de useEffect. Aquí tienes un ejemplo de cómo hacerlo:

tsx
import { useEffect, useState } from "react";

// Definimos la interfaz para el tipo de dato que esperamos recibir de la API
interface Todo {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

const App = () => {
  const [data, setData] = useState<null | Todo>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<null | string>(null);

  // useEffect se ejecuta al montar el componente (arreglo vacío: [])
  useEffect(() => {
    // Llamamos a la API usando fetch
    fetch("https://jsonplaceholder.typicode.com/todos/1")
      .then((res) => {
        // Si la respuesta no es exitosa, lanzamos un error
        if (!res.ok) throw new Error("Error al consumir la api");
        // Convertimos la respuesta a JSON
        return res.json();
      })
      .then((data: Todo) => {
        // Guardamos los datos recibidos en el estado
        setData(data);
      })
      .catch((e: unknown) => {
        // Si ocurre un error, comprobamos si es una instancia de Error
        if (e instanceof Error) {
          // Guardamos el mensaje del error
          setError(e.message);
        } else {
          // Si el error es desconocido, mostramos un mensaje genérico
          console.log(e);
          setError("Error desconocido");
        }
      })
      .finally(() => {
        // Cuando termina todo (éxito o error), dejamos de mostrar el loader
        setLoading(false);
      });
  }, []); // El arreglo vacío hace que esto solo se ejecute una vez al montar el componente

  if (loading) return <p>Loading...</p>;
  if (error) return <p>{error}</p>;

  return (
    <div className="container mx-auto">
      <h1>Fetch y useEffect</h1>
      {/* Mostramos los datos en formato JSON para verlos fácilmente */}
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};

export default App;

res.ok

El método res.ok en la respuesta de fetch devuelve false cuando la respuesta HTTP tiene un código de estado fuera del rango 200–299.

Aunque res.ok sea false, fetch NO lanza un error automáticamente. Por eso, muchos desarrolladores chequean manualmente res.ok y lanzan su propio error si es necesario.

unknown en TypeScript

En TypeScript, unknown es un tipo de dato que significa “no sabemos qué es todavía”.

  • Es similar a any (que puede ser cualquier cosa), pero más seguro.
  • Si una variable es unknown, TypeScript no te deja usarla directamente. Debes comprobar primero qué tipo de dato es antes de trabajar con ella.

Ejemplo con async await

useEffect no puede recibir una función async directamente porque espera que su función de callback devuelva nada (void) o una función de limpieza (() => void), no una promesa.

Porque React puede usar el valor de retorno del callback para saber si necesita ejecutar una función de limpieza cuando el componente se desmonta o se actualiza. Si el callback devuelve una promesa, React no sabría si es una función de limpieza o no, y esto rompería su ciclo de vida interno.

tsx
import { useEffect, useState } from "react";

interface Todo {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

const App = () => {
  const [data, setData] = useState<null | Todo>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<null | string>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
        if (!res.ok) throw new Error("Error al consumir la api");
        const result = (await res.json()) as Todo;

        setData(result);
      } catch (error: unknown) {
        if (error instanceof Error) {
          setError(error.message);
        } else {
          setError("Error desconocido");
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>{error}</p>;

  return (
    <div className="container mx-auto">
      <h1>Fetch y useEffect</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};
export default App;

Custom Hooks

Los Custom Hooks son funciones de JavaScript que te permiten reutilizar lógica de estado y efectos en componentes funcionales de React. Son una forma de extraer lógica compleja y compartirla entre diferentes componentes sin duplicar código.

Puedes crear un Custom Hook que encapsule la lógica de una llamada a una API, por ejemplo:

tsx
import { useEffect, useState } from "react";

export const useFetch = <T,>(url: string) => {
  const [data, setData] = useState<null | T>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<null | string>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const res = await fetch(url);
        if (!res.ok) throw new Error("Error al consumir la api");
        const result = (await res.json()) as T;

        setData(result);
      } catch (error: unknown) {
        if (error instanceof Error) {
          setError(error.message);
        } else {
          setError("Error desconocido");
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
};
tsx
import { useFetch } from "./hooks/use-fetch";

interface Todo {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

const App = () => {
  const { data, loading, error } = useFetch<Todo>(
    "https://jsonplaceholder.typicode.com/todos/1"
  );

  if (loading || !data) return <p>Loading...</p>;
  if (error) return <p>{error}</p>;

  return (
    <div className="container mx-auto">
      <h1>Fetch y useEffect</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};
export default App;

Tipo genérico

El uso de <T> en useFetch permite que el hook sea genérico y pueda manejar diferentes tipos de datos. Así, puedes usarlo para cualquier tipo de respuesta de API, no solo para Todo.

Un tipo genérico es una especie de "parámetro" de tipo. Permite que funciones, clases o interfaces trabajen con cualquier tipo de dato, en vez de uno fijo.

Ventajas de los Custom Hooks

  • Reutilización de lógica: Puedes usar el mismo Custom Hook en diferentes componentes sin duplicar código.
  • Organización: Ayudan a mantener el código más limpio y organizado, separando la lógica de negocio de la presentación.
  • Abstracción: Permiten encapsular lógica compleja en una función que puede ser fácilmente entendida y utilizada por otros desarrolladores.

librerías especializadas

Si quieres hacer llamados HTTP en React sin tener que escribir toda la lógica manualmente (fetch, manejo de loading, error, reintentos, caching, etc.), existen varias librerías especializadas que te facilitan la vida y son ampliamente usadas en la industria.

Aquí tienes las principales:

1. React Query

  • URL: https://tanstack.com/query/latest

  • Ventajas:

    • Manejo automático de loading, error y datos.
    • Caching de peticiones.
    • Reintentos, refetch, sincronización en segundo plano.
    • Integración con paginación, infinitas consultas y mutaciones.
  • Uso típico:

    typescript
    import { useQuery } from "@tanstack/react-query";
    
    const { data, isLoading, error } = useQuery({
      queryKey: ["todos"],
      queryFn: () => fetch("/api/todos").then((res) => res.json()),
    });

2. SWR

  • URL: https://swr.vercel.app/

  • Ventajas:

    • Simple y ligero.
    • Revalidación automática (Stale While Revalidate).
    • Caching y sincronización automática.
  • Uso típico:

    typescript
    import useSWR from "swr";
    
    const fetcher = (url) => fetch(url).then((res) => res.json());
    const { data, error, isLoading } = useSWR("/api/todos", fetcher);

3. Axios (con hooks)

  • URL: https://axios-http.com/

  • Ventajas:

    • Cliente HTTP muy popular (no hook por defecto, pero puedes usar hooks de la comunidad como useAxios).
    • Mejor manejo de interceptores y configuración global.
  • Ejemplo usando hook de la comunidad (axios-hooks):

    typescript
    import useAxios from "axios-hooks";
    
    const [{ data, loading, error }] = useAxios("/api/todos");

4. RTK Query

  • URL: https://redux-toolkit.js.org/rtk-query/overview
  • Ventajas:
    • Integración con Redux Toolkit.
    • Generación automática de hooks, caché y manejo de estado centralizado.
  • Uso típico:
    typescript
    // Definición de endpoint en la slice
    const api = createApi({ ... });
    // En componente
    const { data, error, isLoading } = useGetTodosQuery();

5. Apollo Client (si usas GraphQL)

  • URL: https://www.apollographql.com/docs/react/

  • Ventajas:

    • Consulta y cacheo de datos GraphQL.
    • Manejo automático de loading, error y actualización de caché.
  • Uso típico:

    typescript
    import { useQuery } from "@apollo/client";
    
    const { data, loading, error } = useQuery(MY_GRAPHQL_QUERY);

Preguntas comunes en entrevistas de trabajo


1. ¿Qué es useEffect y para qué se utiliza?

Respuesta:
useEffect es un hook de React que permite ejecutar efectos secundarios (side effects) en componentes funcionales, como llamadas a APIs, suscripciones o manipulación del DOM.


2. ¿Cuándo se ejecuta el código dentro de useEffect?

Respuesta:
Por defecto, se ejecuta después de que el componente se monta y después de cada renderizado si las dependencias cambian.


3. ¿Qué es el array de dependencias en useEffect?

Respuesta:
Es el segundo argumento de useEffect y le indica a React cuándo debe volver a ejecutar el efecto. Si está vacío ([]), el efecto solo se ejecuta una vez al montar el componente.


4. ¿Qué sucede si no se proporciona el array de dependencias?

Respuesta:
El efecto se ejecuta después de cada renderizado, lo que puede causar bucles infinitos si dentro del efecto se actualiza el estado.


5. ¿Cómo se limpia un efecto en useEffect?

Respuesta:
Devolviendo una función de limpieza (cleanup function) dentro del useEffect, que React ejecutará antes de desmontar el componente o antes de ejecutar el efecto de nuevo.


6. ¿Se puede usar una función async directamente como callback de useEffect?

Respuesta:
No, porque useEffect espera que el callback devuelva una función de limpieza o nada, pero una función async devuelve una promesa.


7. ¿Qué tipos de efectos secundarios se suelen manejar con useEffect?

Respuesta:
Llamadas a APIs, suscripciones a eventos, manipulación manual del DOM, temporizadores, y limpieza de recursos.


8. ¿Qué ocurre si se actualiza el estado dentro de useEffect sin un array de dependencias?

Respuesta:
Puede causar un bucle infinito de renderizados, ya que cada actualización de estado provoca una nueva ejecución del efecto.


9. ¿Qué diferencia hay entre useEffect y useLayoutEffect?

Respuesta:
useEffect se ejecuta después de que el DOM se haya pintado, mientras que useLayoutEffect se ejecuta sincrónicamente después de que React haya realizado cambios en el DOM, antes de que el navegador lo pinte.


10. ¿Se puede tener más de un useEffect en el mismo componente?

Respuesta:
Sí, se pueden tener tantos useEffect como se necesiten y se ejecutarán en el orden en que aparecen.