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.
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
import MyButton from "./components/MyButton";
const App = () => {
const user = {
name: "Ignacio",
imageUrl: "https://i.pravatar.cc/150?img=3",
imageSize: 90,
};
return (
<>
<div>
<h1>Hola: {user.name}</h1>
<img
src={user.imageUrl}
alt={`imagen-${user.name}`}
style={{
width: user.imageSize,
height: user.imageSize,
borderRadius: "50%",
}}
/>
</div>
<MyButton />
<br />
<p>Lorem, ipsum dolor.</p>
</>
);
};
export default App;
Renderizado condicional
import MyButton from "./components/MyButton";
const App = () => {
const user = {
name: "Ignacio",
imageUrl: "https://i.pravatar.cc/150?img=3",
imageSize: 90,
loggedIn: false,
};
if (!user.loggedIn) {
return <h1>Por favor, inicia sesión</h1>;
}
return (
<>
<div>
<h1>Hola: {user.name}</h1>
<img
src={user.imageUrl}
alt={`imagen-${user.name}`}
style={{
width: user.imageSize,
height: user.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 = {
name: "Ignacio",
imageUrl: "https://i.pravatar.cc/150?img=3",
imageSize: 90,
loggedIn: true,
};
const status = "active";
return (
<>
<div>
<h2>Status: {status}</h2>
{status === "active" && <p>El usuario está activo</p>}
{user.loggedIn ? (
<>
<h1>Hola: {user.name}</h1>
<img
src={user.imageUrl}
alt={`imagen-${user.name}`}
style={{
width: user.imageSize,
height: user.imageSize,
borderRadius: "50%",
}}
/>
</>
) : (
<h1>Please log in</h1>
)}
</div>
<MyButton />
<br />
<p>Lorem, ipsum dolor.</p>
</>
);
};
export default App;
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 |
Ejemplo | Tipo | Descripción |
---|---|---|
true | Boolean | El booleano verdadero literal |
1 , -1 , 3.14 | Number | Cualquier número distinto de 0 |
"hola" , "false" | String | Cualquier cadena no vacía |
[] | Array | Un array vacío también es truthy |
{} | Object | Un objeto vacío también es truthy |
function() {} | Function | Las funciones son truthy |
Infinity , -Infinity | Number | Valores infinitos |
Symbol() | Symbol | Todos los símbolos |
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 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;
Eventos
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 = () => {
const handleClick = () => {
console.log("Button clicked!");
};
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.
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";
import "./button.css";
const CounterButton = () => {
const [counter, setCounter] = useState(0);
const handleIncrementCounter = () => {
setCounter(counter + 1);
};
return (
<button
className="btn"
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 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:
const [input, setInput] = useState("");
return (
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
);
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 es un atributo que se usa para establecer el valor de un campo de formulario. En React, puedes vincular el valor del campo al estado del componente para que se actualice automáticamente cuando el estado cambie.
npm i bootstrap@5.3.7
src\index.css
@import "bootstrap/dist/css/bootstrap.min.css";
src\app.tsx
import type { FormEvent } from "react";
import { useState } from "react";
import TodoList from "./components/todo-list";
export interface Task {
id: number;
title: string;
completed: boolean;
}
const App = () => {
const [input, setInput] = useState("");
const [tasks, setTasks] = useState<Task[]>([]);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (input.trim() === "") return setInput("");
const newTask: Task = {
id: Date.now(),
title: input.trim(),
completed: false,
};
// El callback de setTasks recibe el estado anterior (prevTasks) y retorna un nuevo array con la nueva tarea añadida.
// Los tres puntos (...) se llaman operador de propagación o spread operator en JavaScript
// “expande” o “desempaqueta” todos los elementos de prev dentro de un nuevo array.
// Así, [...prev, tareaA] crea un nuevo array, copiando todos los elementos de prev y agregando tareaA al final.
// Esto es importante porque React necesita un nuevo array para detectar cambios y volver a renderizar el componente.
setTasks((prevTasks) => [...prevTasks, newTask]);
setInput(""); // Limpiar el input después de agregar la tarea
};
const handleToggle = (id: number) => {
setTasks((prevTasks) =>
prevTasks.map((task) =>
task.id === id ? { ...task, completed: !task.completed } : task
)
);
};
return (
<div className="container mt-5">
<h1 className="text-center my-5">Lista de tareas</h1>
<form
className="d-flex gap-2 mb-5"
onSubmit={handleSubmit}
>
<input
type="text"
className="form-control"
placeholder="Añadir nueva tarea"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button
className="btn btn-primary"
type="submit"
>
Agregar
</button>
</form>
<TodoList
tasks={tasks}
handleToggle={handleToggle}
/>
</div>
);
};
export default App;
src\components\todo-list.tsx
import type { Task } from "../app";
import TodoItem from "./todo-item";
interface Props {
tasks: Task[];
handleToggle: (id: number) => void;
}
const TodoList = ({ tasks, handleToggle }: Props) => {
if (tasks.length === 0) {
return (
<div className="alert alert-info text-center">
No hay tareas pendientes
</div>
);
}
return (
<ul className="list-group">
{tasks.map((task) => (
<TodoItem
key={task.id}
task={task}
handleToggle={handleToggle}
/>
))}
</ul>
);
};
export default TodoList;
src\components\todo-item.tsx
import type { Task } from "../app";
interface Props {
task: Task;
handleToggle: (id: number) => void;
}
const TodoItem = ({ task, handleToggle }: Props) => {
return (
<li
className={`list-group-item d-flex justify-content-between align-items-center ${
task.completed ? "list-group-item-success" : ""
}`}
>
<span
className={`${task.completed ? "text-decoration-line-through" : ""}`}
onClick={() => handleToggle(task.id)}
style={{ cursor: "pointer" }}
>
{task.title}
</span>
<button
className="btn btn-sm btn-outline-secondary"
onClick={() => handleToggle(task.id)}
>
{task.completed ? "Deshacer" : "Completar"}
</button>
</li>
);
};
export default TodoItem;
useState: Actualización del estado
En React, el hook useState
te permite actualizar el estado de dos formas principales al llamar a su función de actualización (setState
/ setTasks
):
1. Pasando el valor directamente
setTasks([...tasks, newTask]);
- Qué hace:
Toma el valor actual detasks
en el momento en que se ejecuta la función, le agreganewTask
, y lo establece como el nuevo estado. - Riesgo:
Si hay múltiples actualizaciones al estado en poco tiempo (por ejemplo, en eventos rápidos o asincrónicos), podrías estar usando un valor desactualizado detasks
, porque el estado anterior podría haber cambiado y tu copia ya no es la más reciente.
2. Pasando una función de callback
setTasks((prevTasks) => [...prevTasks, newTask]);
- Qué hace:
Le pasas asetTasks
una función que recibe el estado más actualizado (prevTasks
) y retorna el nuevo estado. - Ventaja:
Siempre usas la versión más reciente del estado, aunque haya varias actualizaciones pendientes. - Recomendado cuando:
Vas a actualizar el estado en función del valor anterior (por ejemplo, agregar, quitar, modificar elementos).
¿Cuál es la diferencia práctica?
- Usar la función de callback es más seguro cuando el nuevo estado depende del anterior, porque React garantiza que te dará el valor más actualizado, incluso si hay varias actualizaciones pendientes.
- Usar el valor directamente es seguro sólo si estás seguro de que no habrá otras actualizaciones simultáneas o asincrónicas que puedan hacer que tu copia esté desactualizada.
Ejemplo ilustrativo
Supón que dos eventos llaman a setTasks
casi al mismo tiempo:
❌ Con valor directamente:
setTasks([...tasks, tareaA]);
// ...casi al mismo tiempo...
setTasks([...tasks, tareaB]);
// Resultado: solo una tarea se agrega, porque ambos usaron la misma "foto" de tasks.
✅ Con callback:
setTasks((prev) => [...prev, tareaA]);
// ...casi al mismo tiempo...
setTasks((prev) => [...prev, tareaB]);
// Resultado: ambas tareas se agregan, porque cada llamada recibe el estado actualizado.
Formularios no controlados
Los formularios no controlados son aquellos en los que el valor de los campos del formulario no está vinculado directamente al estado del componente. En su lugar, puedes acceder a los valores de los campos directamente desde el DOM usando referencias (refs
).
import type { FormEvent } from "react";
import { useRef, useState } from "react";
import TodoList from "./components/todo-list";
export interface Task {
id: number;
title: string;
completed: boolean;
}
const App = () => {
// const [input, setInput] = useState("");
const [tasks, setTasks] = useState<Task[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!inputRef.current) return; // Verificamos si existe el input, salimos si no
const title = inputRef.current.value.trim();
if (!title) return (inputRef.current.value = ""); // Si el título está vacío, limpiamos el input y salimos
const newTask: Task = {
id: Date.now(),
title,
completed: false,
};
setTasks((prevTasks) => [...prevTasks, newTask]);
inputRef.current.value = ""; // No es necesario el !
inputRef.current.focus(); // Tampoco aquí
};
const handleToggle = (id: number) => {
setTasks((prevTasks) =>
prevTasks.map((task) =>
task.id === id ? { ...task, completed: !task.completed } : task
)
);
};
return (
<div className="container mt-5">
<h1 className="text-center my-5">Lista de tareas</h1>
<form
className="d-flex gap-2 mb-5"
onSubmit={handleSubmit}
>
<input
type="text"
className="form-control"
placeholder="Añadir nueva tarea"
// value={input}
// onChange={(e) => setInput(e.target.value)}
ref={inputRef}
/>
<button
className="btn btn-primary"
type="submit"
>
Agregar
</button>
</form>
<TodoList
tasks={tasks}
handleToggle={handleToggle}
/>
</div>
);
};
export default App;
Característica | Formulario Controlado | Formulario No Controlado |
---|---|---|
Definición | El estado de los inputs es gestionado por React. | El estado de los inputs es gestionado por el DOM. |
Acceso al valor | A través del estado (useState, useReducer, etc). | A través de referencias (ref) o directamente del DOM. |
Sincronización | El valor del input siempre está sincronizado con React. | El valor puede cambiar fuera del control de React. |
onChange/value | Requiere pasar ambos (value y onChange ). | Solo necesita defaultValue o nada. Se puede usar ref . |
Validación | Fácil validación en tiempo real con lógica de React. | Más difícil de validar en tiempo real; se valida al final. |
Reseteo de valores | Fácil de resetear cambiando el estado. | Se debe manipular el DOM o usar refs para resetear. |
Rendimiento | Puede causar más renders al cambiar el estado. | Menos renders; solo cuando realmente lo necesitas. |
Uso típico | Formularios pequeños, validación dinámica, experiencia rica. | Formularios grandes, migraciones, compatibilidad con librerías externas. |
Ejemplo de input | <input value={valor} onChange={e => setValor(e.target.value)} /> | <input defaultValue="texto" ref={miRef} /> |
Ventajas | Control total, validación fácil, valores sincronizados. | Simplicidad, mejor para formularios grandes o simples. |
Desventajas | Más código, más renders, puede ser menos eficiente en formularios grandes. | Menos control, validación y manipulación menos directa. |
¿Qué es useRef
y para qué se utiliza?
Definición:
useRef
es un hook de React que te permite acceder y guardar una referencia mutable a un elemento del DOM o a un valor que quieres mantener entre renderizados, sin que cause un nuevo render.¿Para qué se usa?
- Para acceder a elementos del DOM directamente (por ejemplo, un
<input>
para enfocarlo o leer su valor). - Para guardar valores persistentes que no provocan re-renderizado (como IDs, temporizadores, etc).
- Para acceder a elementos del DOM directamente (por ejemplo, un
¿Por qué se inicializa en null
?
const inputRef = useRef<HTMLInputElement>(null);
- React aún no ha renderizado el elemento, así que al principio la referencia apunta a
null
. - Una vez que el
<input ref={inputRef} />
se monta,inputRef.current
apunta al elemento del DOM. - Por eso, el valor inicial es
null
(no hay nada referenciado todavía).
¿Siempre nos devuelve un current
?
- Sí,
useRef
siempre devuelve un objeto con una propiedad llamadacurrent
. - Ese objeto es:typescript
{ current: valorActual; }
- Al principio,
current
es lo que pusiste en el argumento inicial (null
en este caso). - Luego, si lo usas como ref en un elemento,
current
pasa a ser ese elemento DOM.
¿Qué es el !
en TypeScript y por qué aquí es necesario?
Otra forma de usar useRef
es con el Non-Null Assertion Operator (!
), que le dice a TypeScript que confías en que current
no será null
en ese momento.
Además de utilizar inputRef.current?.value
donde el ?
es el Optional Chaining Operator que evita errores si current
es null
o undefined
.
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
const title = inputRef.current?.value.trim();
if (!title) return;
const newTask: Task = {
id: Date.now(),
title,
completed: false,
};
setTasks((prevTasks) => [...prevTasks, newTask]);
inputRef.current!.value = ""; // Clear the input field
inputRef.current!.focus(); // Refocus the input field
};