Fundamentos de React JS con TypeScript
Objetivos
- ¿Qué es React?
- Comenzar un proyecto con React y TypeScript
- JSX
- Componentes
- Props
- Hooks, useState, useEffect
WARNING
- Es recomendable conocer los fundamentos de JS antes de comenzar este curso.
- curso de JS gratis
- curso de git y github
Extensiones VSCode
React y Themes
TypeScript
- ESLint: Linter para Javascript y Typescript.
- Prettier: Formateador de código.
- Error Lens: Muestra mensajes de error y advertencia en línea.
- JSON to TS: Convierte JSON a interfaces de Typescript.
- Pretty TypeScript Erros: Mejora la visualización de errores de 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.
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:
return <h1>Hola</h1>;
Es transformado por herramientas como Babel a algo como:
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
- React + Vite + TypeScript
- React Router v7 + Vite (antes Remix)
- Next.js (con su App Router moderno y SWC)
🧩 Comparativa: Vite vs React Router v7 vs Next.js
Característica | Vite + React + TS | React Router v7 + Vite | Next.js (App Router) |
---|---|---|---|
⚙️ Tooling base | Vite + Esbuild | Vite + Esbuild | Webpack (o Turbopack), SWC |
🔁 Enrutamiento | Manual (usualmente con React Router) | Automático y basado en archivos (routes/ ) | Automático basado en carpetas (app/ ) |
🧠 Aprendizaje | Más simple, ideal para comenzar | Intermedio: estructura y loaders | Complejo: muchas convenciones y APIs |
🚀 Renderizado | Solo Cliente | Client-Side + Data Loaders | SSR, 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) | Manual | Más estructurada con loaders | Integrada fácilmente con middlewares |
🧪 Testing | Compatible con Vitest / Jest | Compatible | Compatible |
🎨 CSS / Styling | Libre (Tailwind, CSS, etc.) | Libre | Libre |
🧩 Plugins y Ecosistema | Flexible | Más nuevo, pequeño ecosistema | Ecosistema enorme y consolidado |
🧠 Mental model (arquitectura) | React puro | “React con Routing + Loader” | Fullstack Framework (SSR + API + routing) |
🧱 Ideal para… | Aprender React, apps SPA simples | Apps SPA modernas con mejor estructura | Apps 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:
node -v
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:
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.
- Ejemplo:
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.
- Ejemplo:
camelCase:
- Ejemplo:
userCard.js
- Uso común: No es muy común para archivos, pero se usa mucho para nombres de variables y funciones.
- Ejemplo:
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.
- Ejemplo:
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.
const App = () => {
return (
<>
<h1>Hola App</h1>
<MyButton />
</>
);
};
export default App;
function MyButton() {
return <button>Click me</button>;
}
ClassName
import "./button.css";
const MyButton = () => {
return <button className="btn">Click me export</button>;
};
export default MyButton;
src\components\button.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 {}
.
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:
<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-size
→fontSize
,background-color
→backgroundColor
- 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"
- Si la propiedad espera un valor con unidades (ej:
Ejemplo completo:
<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:
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 (&&)
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:
Valor | Tipo | Descripción |
---|---|---|
false | Boolean | El booleano falso literal |
0 | Number | Cero numérico |
-0 | Number | Cero negativo |
0n | BigInt | Cero en BigInt |
"" | String | Cadena vacía |
null | Object | Ausencia de valor |
undefined | Undefined | Variable sin definir |
NaN | Number | Resultado 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.
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;
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.
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;
// 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í:
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ística | interface | type |
---|---|---|
Extensión (herencia) | Sí, con extends | Sí, con & (intersección de tipos) |
Declaración múltiple | Sí, se fusionan | No, da error si se redeclara |
Uniones (A | B ) | No | Sí |
Alias a primitivos | No | Sí |
Implementación en clases | Sí | No directamente |
Usado para objetos | Sí | Sí |
Usado para utilidades avanzadas | No | Sí (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.
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?
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 usamosonClick
(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 |
---|---|---|
onClick | Al hacer clic | handleClick |
onChange | Al cambiar valor (input, select, etc) | handleChange |
onSubmit | Al enviar formulario | handleSubmit |
onMouseEnter | Al pasar el mouse encima | handleMouseEnter |
onMouseLeave | Al sacar el mouse del área | handleMouseLeave |
onFocus | Al enfocar un input | handleFocus |
onBlur | Al perder el foco un input | handleBlur |
onKeyDown | Al presionar una tecla | handleKeyDown |
onKeyUp | Al soltar una tecla | handleKeyUp |
onDoubleClick | Al hacer doble clic | handleDoubleClick |
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.
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:
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
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:
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.
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;
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:
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.
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;
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.
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.
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:
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.
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
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
npm i bootstrap@5.3.7
src\index.css
@import "bootstrap/dist/css/bootstrap.min.css";
Código:
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 objetotouched
(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:
const persona = {
nombre: "Juan",
edad: 30,
};
// Así obtienes el tipo de persona:
type PersonaType = typeof persona;
Esto es equivalente a haber escrito manualmente:
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:
- Una función que contiene el código del efecto secundario.
- 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
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.
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
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;
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:
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.
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:
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 };
};
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
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:
typescriptimport { useQuery } from "@tanstack/react-query"; const { data, isLoading, error } = useQuery({ queryKey: ["todos"], queryFn: () => fetch("/api/todos").then((res) => res.json()), });
2. SWR
Ventajas:
- Simple y ligero.
- Revalidación automática (Stale While Revalidate).
- Caching y sincronización automática.
Uso típico:
typescriptimport useSWR from "swr"; const fetcher = (url) => fetch(url).then((res) => res.json()); const { data, error, isLoading } = useSWR("/api/todos", fetcher);
3. Axios (con hooks)
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.
- Cliente HTTP muy popular (no hook por defecto, pero puedes usar hooks de la comunidad como
Ejemplo usando hook de la comunidad (
axios-hooks
):typescriptimport 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)
Ventajas:
- Consulta y cacheo de datos GraphQL.
- Manejo automático de loading, error y actualización de caché.
Uso típico:
typescriptimport { 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.