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.

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

tsx
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

tsx
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 (&&)

tsx
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;
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
EjemploTipoDescripción
trueBooleanEl booleano verdadero literal
1, -1, 3.14NumberCualquier número distinto de 0
"hola", "false"StringCualquier cadena no vacía
[]ArrayUn array vacío también es truthy
{}ObjectUn objeto vacío también es truthy
function() {}FunctionLas funciones son truthy
Infinity, -InfinityNumberValores infinitos
Symbol()SymbolTodos 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.

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;

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.

tsx
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?

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.

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";
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:

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

Compartir datos entre componentes

Compartir datos entre componentes 2

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 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
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.

sh
npm i bootstrap@5.3.7

src\index.css

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

src\app.tsx

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

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

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

tsx
setTasks([...tasks, newTask]);
  • Qué hace:
    Toma el valor actual de tasks en el momento en que se ejecuta la función, le agrega newTask, 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 de tasks, 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

tsx
setTasks((prevTasks) => [...prevTasks, newTask]);
  • Qué hace:
    Le pasas a setTasks 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:

tsx
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:

tsx
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).

tsx
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ísticaFormulario ControladoFormulario No Controlado
DefiniciónEl estado de los inputs es gestionado por React.El estado de los inputs es gestionado por el DOM.
Acceso al valorA través del estado (useState, useReducer, etc).A través de referencias (ref) o directamente del DOM.
SincronizaciónEl valor del input siempre está sincronizado con React.El valor puede cambiar fuera del control de React.
onChange/valueRequiere pasar ambos (value y onChange).Solo necesita defaultValue o nada. Se puede usar ref.
ValidaciónFá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 valoresFácil de resetear cambiando el estado.Se debe manipular el DOM o usar refs para resetear.
RendimientoPuede causar más renders al cambiar el estado.Menos renders; solo cuando realmente lo necesitas.
Uso típicoFormularios 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} />
VentajasControl total, validación fácil, valores sincronizados.Simplicidad, mejor para formularios grandes o simples.
DesventajasMá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).

¿Por qué se inicializa en null?

typescript
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?

  • , useRef siempre devuelve un objeto con una propiedad llamada current.
  • 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.

ts
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
};