DevTools

Cheatsheet React

Biblioteca JavaScript para interfaces

Volver a los lenguajes
React
122 tarjetas encontradas
Categorías:
Versiones:

Componentes


16 cards
Componente funcional
function Hola() {
  return <h1>Hola Mundo!</h1>;
}

export default Hola;

Un componente funcional es una función JavaScript que retorna JSX. Es la forma estándar de crear componentes en React moderno.

Composición de componentes
function App() {
  return (
    <div>
      <Header />
      <Main />
      <Footer />
    </div>
  );
}

Componer es armar la interfaz combinando componentes menores dentro de otros. Cada componente queda simple y reutilizable.

Spread de props
const props = { nombre: "Ana", edad: 30 };

// Pasar todas las props de una vez:
<Perfil {...props} />

// Equivale a:
<Perfil nombre="Ana" edad={30} />

El spread (...) esparce un objeto como props individuales. Útil para reenviar props sin listarlas una a una.

Renderizar con &&
function Notificaciones({ count }) {
  return (
    <div>
      <h1>Mensajes</h1>
      {count > 0 && (
        <span className="badge">{count}</span>
      )}
    </div>
  );
}

El operador && renderiza el lado derecho solo si la condición es verdadera. Más corto que un ternario cuando no hay alternativa.

Componente con arrow function
const Hola = () => <h1>Hola Mundo!</h1>;

export default Hola;

Versión más corta con arrow function. Muy usada para componentes simples de una línea.

Atributos en JSX
<input
  type="text"
  placeholder="Nombre"
  disabled={true}
  tabIndex={1}
  data-id="campo-nombre"
/>

Los atributos siguen el patrón HTML, pero con nombres en camelCase (tabIndex) y valores dinámicos entre { }.

Componente como variable
function App({ tipo }) {
  const Componente = tipo === "admin"
    ? PainelAdmin
    : PainelUser;

  return <Componente />;
}

Puedes guardar un componente en una variable (con mayúscula) y renderizarlo dinámicamente. El nombre debe empezar por letra mayúscula.

Componente puro (memo)
const Item = React.memo(function Item({ nombre }) {
  console.log("renderizou:", nombre);
  return <li>{nombre}</li>;
});

// Solo re-renderiza si "nombre" cambia
// Comparación superficial (shallow) de las props

Un componente puro (React.memo) solo re-renderiza cuando las props cambian. Evita renders innecesarios en listas grandes.

JSX básico
function Cartao() {
  return (
    <div className="cartao">
      <h2>Título</h2>
      <p>Contenido aqui</p>
      <img src="/foto.jpg" alt="Foto" />
    </div>
  );
}

JSX se parece a HTML, pero usa className en vez de class. Un componente debe retornar un único elemento raíz.

Renderizar en el DOM
import { createRoot } from "react-dom/client";
import App from "./App";

const root = createRoot(
  document.getElementById("root")
);

root.render(<App />);

createRoot conecta React a un elemento HTML (normalmente un div#root). render dibuja el componente principal en la página.

Early return
function Perfil({ user }) {
  if (!user) return <p>Faz login</p>;
  if (user.bloqueado) return <p>Cuenta suspensa</p>;

  return (
    <div>
      <h1>{user.nombre}</h1>
      <p>{user.email}</p>
    </div>
  );
}

El early return trata los casos especiales al inicio con un return anticipado. El código principal queda limpio, sin ternarios anidados.

Fragment
function Linea() {
  return (
    <>
      <td>Nombre</td>
      <td>Valor</td>
    </>
  );
}

// Equivalente:
import { Fragment } from "react";
return <Fragment>...</Fragment>;

El Fragment (<>...</>) agrupa elementos sin crear un nodo extra en el DOM. Esencial cuando un componente necesita retornar múltiples hermanos.

Expresiones en JSX
function Perfil({ nombre, edad }) {
  return (
    <div>
      <p>{nombre}</p>
      <p>{edad + 1} anios</p>
      <p>{"texto".toUpperCase()}</p>
    </div>
  );
}

Usa { } para insertar cualquier expresión JavaScript dentro del JSX: variables, cálculos o llamadas a funciones.

Comentarios en JSX
function Cartao() {
  return (
    <div>
      {/* Esto es un comentario en JSX */}
      <h2>Título</h2>
      {/* <p>Texto removido</p> */}
    </div>
  );
}

Dentro del JSX, los comentarios usan {/* ... */}. Fuera del JSX (en JavaScript), usa // normalmente.

Props con TypeScript
interface Props {
  nombre: string;
  edad?: number;  // opcional
  onSalvar: (id: number) => void;
}

function Perfil({ nombre, edad = 0, onSalvar }: Props) {
  return <p>{nombre}, {edad}</p>;
}

Con TypeScript, defines una interface para las props. El compilador avisa si falta una prop obligatoria o el tipo es incorrecto.

Children prop
function Card({ título, children }) {
  return (
    <div className="card">
      <h2>{título}</h2>
      <div className="corpo">{children}</div>
    </div>
  );
}

// Uso - el contenido entre las tags se convierte en children:
<Card título="Perfil">
  <p>Nombre: Ana</p>
  <button>Editar</button>
</Card>

La prop children recibe todo lo que se coloca entre las tags de apertura y cierre del componente. Ideal para wrappers como cards, modales y layouts.

Props e Estado


17 cards
Props
function Saludo({ nombre, edad }) {
  return <p>Hola {nombre}, {edad} anios</p>;
}

<Saludo nombre="Ana" edad={30} />

Las props son los datos pasados de un componente padre a un hijo. Funcionan como los argumentos de una función.

Estado con array
const [ítems, setItens] = useState([]);

// Añadir:
setItens([...ítems, novoItem]);

// Eliminar:
setItens(ítems.filter(i => i.id !== id));

// Actualizar:
setItens(ítems.map(i =>
  i.id === id ? { ...i, nombre: "nuevo" } : i
));

Los arrays de estado se actualizan de forma inmutable: crea un nuevo array con filter, map o spread, en vez de alterar el original.

Actualización asíncrona
const [count, setCount] = useState(0);

// ❌ Usa el valor antiguo:
setCount(count + 1);
setCount(count + 1);  // +1, no +2!

// ✅ Forma funcional (correcta):
setCount(prev => prev + 1);
setCount(prev => prev + 1);  // +2 ✓

El estado es asíncrono: dentro del mismo evento, el valor no cambia inmediatamente. Usa la forma funcional prev => para actualizaciones encadenadas.

Controlled vs Uncontrolled
// Controlled: React controla el valor
const [valor, setValor] = useState("");
<input value={valor} onChange={e => setValor(e.target.value)} />

// Uncontrolled: el DOM lo controla (con ref)
const ref = useRef();
<input ref={ref} defaultValue="texto" />
// Leer: ref.current.value

En controlled, React es la fuente de verdad (value + onChange). En uncontrolled, el DOM guarda el valor y lo lees con ref. Prefiere controlled.

Estado vs Props
function ContadorPai() {
  const [total, setTotal] = useState(0);  // ESTADO (dueño)

  return (
    <>
      <Mostrador valor={total} />          {/* PROP (solo lectura) */}
      <button onClick={() => setTotal(total + 1)}>
        +1
      </button>
    </>
  );
}

function Mostrador({ valor }) {
  // valor es una PROP - no puede ser modificada aquí
  return <p>Total: {valor}</p>;
}

El estado (useState) son datos que el componente controla y puede cambiar. Las props son datos recibidos del padre, de solo lectura. El dueño del estado lo pasa como prop.

Las props son de solo lectura
function Botón({ texto }) {
  // texto = "nuevo";  ❌ Error!
  return <button>{texto}</button>;
}

Una prop nunca debe ser modificada dentro del componente que la recibe. Para datos que cambian, usa el estado.

Lifting state up
function Pai() {
  const [valor, setValor] = useState("");
  return (
    <Hijo valor={valor} onChange={setValor} />
  );
}

function Hijo({ valor, onChange }) {
  return (
    <input
      value={valor}
      onChange={e => onChange(e.target.value)}
    />
  );
}

Cuando dos componentes necesitan el mismo estado, súbelo al padre común y pásalo hacia abajo vía props.

Estado derivado
const [ítems, setItens] = useState([]);

// ✅ Calcula durante el render:
const total = ítems.reduce((s, i) => s + i.precio, 0);
const ativos = ítems.filter(i => i.activo);

// ❌ No lo guardes en estado:
// const [total, setTotal] = useState(0);

Si un valor puede ser calculado a partir de props o estado existente, no lo guardes en estado separado. Calcúlalo directamente en el render.

Callback props
// El padre pasa una función como prop:
function Pai() {
  const [ítems, setItens] = useState([]);
  return (
    <Hijo onAdicionar={(item) =>
      setItens(prev => [...prev, item])
    } />
  );
}

// El hijo llama a la callback:
function Hijo({ onAdicionar }) {
  return (
    <button onClick={() => onAdicionar("nuevo")}>
      Adicionar
    </button>
  );
}

El patrón callback props permite al hijo comunicarse con el padre. El hijo llama a la función y el padre decide qué hacer con los datos.

useState
import { useState } from "react";

function Contador() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Clic: {count}
    </button>
  );
}

useState crea una variable de estado y la función para actualizarla. Cuando el estado cambia, el componente re-renderiza.

Destructuring de props
// Sin destructuring:
function Perfil(props) {
  return <p>{props.nombre}, {props.edad}</p>;
}

// Con destructuring (más limpio):
function Perfil({ nombre, edad }) {
  return <p>{nombre}, {edad}</p>;
}

El destructuring extrae las props directamente en los parámetros. Hace el código más corto y legible, sobre todo con muchas props.

Inmutabilidad
const [user, setUser] = useState({ nombre: "Ana", edad: 30 });

// ❌ Muta el objeto (¡no re-renderiza!)
user.nombre = "Ray";
setUser(user);

// ✅ Crea un nuevo objeto
setUser({ ...user, nombre: "Ray" });

// ✅ Array: nuevo array
setItens([...ítems, nuevo]);

React solo re-renderiza si la referencia cambia. Mutar el objeto y pasar la misma referencia = no pasa nada. Crea siempre uno nuevo con spread.

Default props
function Botón({ color = "azul", tamano = "md", children }) {
  return (
    <button className={`btn btn-${color} btn-${tamano}`}>
      {children}
    </button>
  );
}

// Uso:
<Botón>Patrón</Botón>           // azul, md
<Botón color="rojo">Error</Botón> // rojo, md

Usa destructuring con defaults para definir valores por defecto en las props. El componente funciona incluso sin pasar todas las props.

Estado con objeto
const [form, setForm] = useState({
  nombre: "",
  email: ""
});

setForm(prev => ({
  ...prev,
  nombre: "Ana"
}));

Para objetos, usa el spread (...) para copiar los campos existentes y actualizar solo algunos. Nunca mutes el objeto directamente.

Prop drilling
// El tema pasa por varios niveles sin ser usado:
function App() {
  const [tema, setTema] = useState("dark");
  return <Painel tema={tema} />;
}
function Painel({ tema }) {
  return <Botón tema={tema} />;  // solo lo reenvía
}
function Botón({ tema }) {
  return <button className={tema} />;
}

Prop drilling es pasar props por varios componentes que solo las reenvían. Para muchos niveles, usa el Context (ver Patrones Avanzados).

Estado con función (lazy init)
// ❌ Calcula en todos los renders:
const [datos, setDados] = useState(
  JSON.parse(localStorage.getItem("x"))
);

// ✅ Calcula solo una vez:
const [datos, setDados] = useState(
  () => JSON.parse(localStorage.getItem("x"))
);

Pasa una función a useState cuando el valor inicial es costoso de calcular. React solo la ejecuta en el primer render (lazy initialization).

useReducer (estado complejo)
import { useReducer } from "react";

const inicial = { count: 0, step: 1 };

function reducer(estado, acción) {
  switch (acción.type) {
    case "incrementar":
      return { ...estado, count: estado.count + estado.step };
    case "reset":
      return inicial;
    default:
      return estado;
  }
}

function Contador() {
  const [estado, dispatch] = useReducer(reducer, inicial);
  return (
    <button onClick={() => dispatch({ type: "incrementar" })}>
      {estado.count}
    </button>
  );
}

useReducer es una alternativa a useState para estado con lógica compleja. Un reducer centraliza las transiciones y dispatch envía acciones.

Hooks


20 cards
useEffect
import { useEffect } from "react";

useEffect(() => {
  document.title = `Cliques: ${count}`;

  return () => {
    // cleanup (opcional)
  };
}, [count]);

useEffect ejecuta código después del render. El array de dependencias controla cuándo vuelve a ejecutarse.

useRef
import { useRef } from "react";

function Foco() {
  const inputRef = useRef(null);

  return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>
        Focar
      </button>
    </>
  );
}

useRef guarda un valor persistente que no causa re-render. Muy usado para acceder directamente a elementos del DOM.

useDeferredValue
import { useDeferredValue } from "react";

function Búsqueda({ query }) {
  const deferred = useDeferredValue(query);
  const resultados = useMemo(
    () => lista.filter(i => i.includes(deferred)),
    [deferred]
  );
  return <Lista ítems={resultados} />;
}

useDeferredValue aplaza la actualización de un valor, manteniendo la UI responsiva. La lista se actualiza enseguida, los resultados vienen después.

useEffect con AbortController
useEffect(() => {
  const controller = new AbortController();

  fetch("/api/datos", {
    signal: controller.signal,
  })
    .then(r => r.json())
    .then(setDados)
    .catch(err => {
      if (err.name !== "AbortError") throw err;
    });

  return () => controller.abort();
}, [id]);

El AbortController cancela peticiones pendientes cuando el componente se desmonta o la dependencia cambia. Evita memory leaks y race conditions.

useSyncExternalStore
import { useSyncExternalStore } from "react";

// Suscribir una store externa (ej.: localStorage):
function useLocalStorage(key) {
  return useSyncExternalStore(
    (callback) => {
      window.addEventListener("storage", callback);
      return () => window.removeEventListener("storage", callback);
    },
    () => localStorage.getItem(key)
  );
}

// Uso:
function Perfil() {
  const nombre = useLocalStorage("nombre");
  return <p>Hola, {nombre}</p>;
}

useSyncExternalStore suscribe fuentes de datos externas a React (stores, browser APIs). Garantiza lecturas consistentes sin tearing.

useEffect: buscar datos
const [datos, setDados] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
  fetch("/api/productos")
    .then(r => r.json())
    .then(data => {
      setDados(data);
      setLoading(false);
    });
}, []);

Con el array de dependencias vacío [], el efecto se ejecuta solo una vez, cuando el componente monta. Ideal para cargar datos.

useMemo
import { useMemo } from "react";

const total = useMemo(
  () => ítems.reduce((s, i) => s + i.precio, 0),
  [ítems]
);

useMemo memoriza el resultado de un cálculo pesado y solo lo recalcula cuando las dependencias cambian. Evita trabajo repetido.

useImperativeHandle
const Modal = forwardRef((props, ref) => {
  const [abierto, setAberto] = useState(false);

  useImperativeHandle(ref, () => ({
    abrir: () => setAberto(true),
    cerrar: () => setAberto(false),
  }));

  return abierto ? <div>Modal</div> : null;
});

// En el padre: modalRef.current.abrir();

useImperativeHandle expone métodos personalizados del hijo al padre vía ref. El padre llama a ref.current.abrir() directamente.

Custom hook: useFetch
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(r => r.json())
      .then(d => { setData(d); setLoading(false); })
      .catch(e => { setError(e); setLoading(false); });
  }, [url]);

  return { data, loading, error };
}

// Uso: const { data, loading } = useFetch("/api/x");

Un useFetch encapsula el patrón de data fetching. Devuelve data, loading y error. Reutilizable en cualquier componente.

Reglas de los Hooks
// CORRECTO - en la cima del componente:
function App() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);
  useEffect(() => { }, []);
  // ...
}

// INCORRECTO - dentro de una condición:
function App() {
  if (logado) {
    const [x, setX] = useState(0);  // ❌ ERROR
  }
  for (...) {
    useEffect(() => { });           // ❌ ERROR
  }
}

Los hooks deben llamarse siempre en el mismo orden, en la cima del componente. Nunca dentro de condiciones, bucles o funciones anidadas. React depende del orden de llamada.

useEffect: dependencias
// Se ejecuta en todos los renders:
useEffect(() => { ... });

// Se ejecuta solo al montar:
useEffect(() => { ... }, []);

// Se ejecuta cuando "id" cambia:
useEffect(() => { ... }, [id]);

El array de dependencias decide cuándo se ejecuta el efecto: sin array se ejecuta siempre, vacío se ejecuta una vez, con valores se ejecuta cuando esos valores cambian.

useCallback
import { useCallback } from "react";

const handleClick = useCallback(
  (id) => setSelecionado(id),
  []
);

useCallback memoriza una función para que la referencia sea estable entre renders. Útil al pasar funciones a hijos optimizados.

useEffect vs useLayoutEffect
// useEffect: tras pintar el browser (asíncrono)
useEffect(() => {
  console.log("depois do paint");
}, []);

// useLayoutEffect: antes del paint (síncrono)
useLayoutEffect(() => {
  // Medir el DOM antes de que el usuario lo vea
  const rect = ref.current.getBoundingClientRect();
  setPosicao(rect.top);
}, []);

useLayoutEffect se ejecuta antes de que el browser pinte. Úsalo para mediciones del DOM que necesitan estar listas antes del render visual.

useId
import { useId } from "react";

function Campo({ label }) {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </>
  );
}

// Cada instancia genera un ID único: ":r1:", ":r2:"...

useId() genera IDs únicos y estables para accesibilidad (label + input). Nunca usar Math.random() para IDs de accesibilidad.

Hook use() (React 19)
import { use, Suspense } from "react";

// use() lee una promise dentro del render:
function Perfil({ userPromise }) {
  const user = use(userPromise);  // suspende hasta resolver
  return <p>{user.nombre}</p>;
}

// Envolver con Suspense:
<Suspense fallback={<Spinner />}>
  <Perfil userPromise={fetchUser()} />
</Suspense>

use() (React 19) lee el valor de una promise directamente en el render, suspendiendo el componente hasta que resuelva. A diferencia de useContext, puede llamarse dentro de condiciones.

useEffect: cleanup
useEffect(() => {
  const timer = setInterval(() => {
    setSegundos(s => s + 1);
  }, 1000);

  return () => clearInterval(timer);
}, []);

La función retornada hace la limpieza antes de que el componente se desmonte o el efecto se repita. Esencial para timers, listeners y suscripciones.

Custom Hook
function useLocalStorage(key, inicial) {
  const [valor, setValor] = useState(
    () => JSON.parse(localStorage.getItem(key)) ?? inicial
  );

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(valor));
  }, [key, valor]);

  return [valor, setValor];
}

Un custom hook es una función que empieza por use y reutiliza lógica con estado entre varios componentes.

useDebugValue
function useOnline() {
  const [online, setOnline] = useState(navigator.onLine);

  useDebugValue(online ? "Online" : "Offline");

  return online;
}

// Visible en React DevTools:
// useOnline: "Online"

useDebugValue muestra un valor personalizado en React DevTools para custom hooks. Facilita la depuración sin console.log.

useTransition
import { useTransition, useState } from "react";

function Búsqueda() {
  const [isPending, startTransition] = useTransition();
  const [resultados, setResultados] = useState([]);

  const buscar = (texto) => {
    startTransition(() => {
      setResultados(filtrar(texto)); // no bloquea el input
    });
  };

  return (
    <>
      <input onChange={e => buscar(e.target.value)} />
      {isPending ? <Spinner /> : <Lista ítems={resultados} />}
    </>
  );
}

useTransition marca las actualizaciones como no urgentes. El input se mantiene responsivo mientras la lista pesada se actualiza en background.

useOptimistic (React 19)
import { useOptimistic } from "react";

function Likes({ total, gostar }) {
  const [otimista, setOtimista] = useOptimistic(total);

  async function handleClick() {
    setOtimista(otimista + 1);  // muestra ya, antes de la respuesta
    await gostar();             // el estado real viene del servidor
  }

  return (
    <button onClick={handleClick}>
      Gosto: {otimista}
    </button>
  );
}

useOptimistic (React 19) muestra de inmediato el resultado esperado, antes de que el servidor responda. Si la petición falla, el valor vuelve automáticamente al estado real.

Eventos e Formulários


15 cards
Event handlers
function Botón() {
  const handleClick = (e) => {
    e.preventDefault();
    console.log("clicado!");
  };

  return (
    <button onClick={handleClick}>
      Clicar
    </button>
  );
}

Los eventos usan nombres en camelCase (onClick, onChange, onSubmit) y reciben una función, no un string.

Checkbox
const [aceite, setAceite] = useState(false);

<label>
  <input
    type="checkbox"
    checked={aceite}
    onChange={e => setAceite(e.target.checked)}
  />
  Aceito os termos
</label>

En el checkbox usa checked (no value) y lee e.target.checked para saber si quedó marcado o desmarcado.

Upload de archivos
function Upload() {
  const [fichero, setFicheiro] = useState(null);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const formData = new FormData();
    formData.append("fichero", fichero);
    await fetch("/api/upload", {
      method: "POST",
      body: formData,
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="file"
        onChange={e => setFicheiro(e.target.files[0])} />
      <button>Enviar</button>
    </form>
  );
}

Para el upload, usa FormData y el input type="file". No definas Content-Type — el browser define el multipart automáticamente.

Varios inputs con name
function Form() {
  const [datos, setDados] = useState({
    nombre: "", email: "", ciudad: ""
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setDados(prev => ({ ...prev, [name]: value }));
  };

  return (
    <>
      <input name="nombre" value={datos.nombre} onChange={handleChange} />
      <input name="email" value={datos.email} onChange={handleChange} />
      <input name="ciudad" value={datos.ciudad} onChange={handleChange} />
    </>
  );
}

Un único handler trata todos los inputs usando el atributo name. La computed property [name] actualiza el campo correcto dinámicamente.

Controlled input
function Form() {
  const [nombre, setName] = useState("");

  return (
    <input
      value={nombre}
      onChange={(e) => setName(e.target.value)}
      placeholder="Nombre"
    />
  );
}

En un input controlado, el valor viene del estado y cada tecla lo actualiza. React es la única fuente de verdad.

Radio buttons
const [opción, setOpcion] = useState("a");

<label>
  <input
    type="radio"
    value="a"
    checked={opción === "a"}
    onChange={e => setOpcion(e.target.value)}
  />
  Opción A
</label>

Cada radio tiene un value y queda checked cuando corresponde al estado. El onChange guarda el valor elegido.

Validación en tiempo real
const [email, setEmail] = useState("");
const [error, setErro] = useState("");

const handleChange = (e) => {
  const valor = e.target.value;
  setEmail(valor);

  if (valor && !valor.includes("@")) {
    setErro("Email inválido");
  } else {
    setErro("");
  }
};

<input value={email} onChange={handleChange} />
{error && <span className="error">{error}</span>}

La validación en tiempo real verifica en cada tecla. Muestra el error de inmediato y lo limpia cuando el input se vuelve válido.

Select y textarea controlados
function Form() {
  const [país, setPais] = useState("pt");
  const [msg, setMsg] = useState("");

  return (
    <>
      <select value={país} onChange={e => setPais(e.target.value)}>
        <option value="pt">Portugal</option>
        <option value="br">Brasil</option>
        <option value="ao">Angola</option>
      </select>

      <textarea
        value={msg}
        onChange={e => setMsg(e.target.value)}
        rows={4}
      />
    </>
  );
}

select y textarea también se controlan con value + onChange. En el select, el value corresponde al atributo value de la option seleccionada.

Formulario con submit
function FormLogin() {
  const [email, setEmail] = useState("");

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

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={email}
        onChange={e => setEmail(e.target.value)}
      />
      <button type="submit">Entrar</button>
    </form>
  );
}

Usa onSubmit en el form y e.preventDefault() para evitar la recarga de la página. Lee los valores del estado.

Eventos de teclado
<input
  onKeyDown={(e) => {
    if (e.key === "Enter") {
      buscar();
    }
  }}
/>

// e.key: la tecla presionada
// e.code: el código físico de la tecla

Los eventos onKeyDown/onKeyUp detectan teclas. Usa e.key para saber cuál fue presionada (ej.: "Enter").

Formulario con reset
const inicial = { nombre: "", email: "" };
const [form, setForm] = useState(inicial);

const handleSubmit = async (e) => {
  e.preventDefault();
  await fetch("/api", {
    method: "POST",
    body: JSON.stringify(form),
  });
  setForm(inicial);  // limpiar
};

<form onSubmit={handleSubmit}>
  <input value={form.nombre}
    onChange={e => setForm({ ...form, nombre: e.target.value })} />
  <button>Enviar</button>
</form>

Para limpiar el formulario tras el submit, basta con reponer el estado al objeto inicial. Los inputs controlados se actualizan automáticamente.

Actions: useActionState (React 19)
import { useActionState } from "react";
import { useFormStatus } from "react-dom";

async function crear(estadoAnterior, formData) {
  await fetch("/api/tareas", {
    method: "POST",
    body: formData,
  });
  return { ok: true };
}

function Botón() {
  const { pending } = useFormStatus();
  return (
    <button disabled={pending}>
      {pending ? "A guardar..." : "Guardar"}
    </button>
  );
}

function Form() {
  const [estado, action] = useActionState(crear, null);
  return (
    <form action={action}>
      <input name="título" />
      <Botón />
    </form>
  );
}

Con Actions (React 19), el form usa action={fn} y envía sin handlers manuales. useActionState guarda el resultado y useFormStatus expone el estado pending — sin useState ni setLoading.

Pasar argumentos
<button onClick={() => eliminar(item.id)}>
  Remover
</button>

<li onMouseEnter={() => setHover(id)}>
  {nombre}
</li>

Para pasar parámetros al handler, envuélvelo en una arrow function. Así controlas exactamente lo que se envía.

Debounce en búsqueda
const [texto, setTexto] = useState("");
const [resultados, setResultados] = useState([]);

useEffect(() => {
  const timer = setTimeout(() => {
    fetch(`/api?q=${texto}`)
      .then(r => r.json())
      .then(setResultados);
  }, 300);

  return () => clearTimeout(timer);
}, [texto]);

El debounce espera a que el usuario deje de escribir antes de buscar. El cleanup cancela el timer anterior en cada tecla.

Enviar con Enter
function Chat() {
  const [msg, setMsg] = useState("");

  const enviar = () => {
    if (!msg.trim()) return;
    console.log("Enviado:", msg);
    setMsg("");
  };

  return (
    <input
      value={msg}
      onChange={e => setMsg(e.target.value)}
      onKeyDown={e => {
        if (e.key === "Enter" && !e.shiftKey) {
          e.preventDefault();
          enviar();
        }
      }}
    />
  );
}

Usa onKeyDown para detectar la tecla Enter. Verifica e.key === "Enter" y previene el default para no insertar una nueva línea.

Listas e Condicionais


15 cards
Renderizar lista
function ListaProdutos({ productos }) {
  return (
    <ul>
      {productos.map(p => (
        <li key={p.id}>{p.nombre}</li>
      ))}
    </ul>
  );
}

Usa map para transformar cada item del array en un elemento JSX. React renderiza la lista completa.

Lista vacía
{ítems.length === 0 ? (
  <p>Nenhum item encontrado.</p>
) : (
  <ul>
    {ítems.map(i => <li key={i.id}>{i.nombre}</li>)}
  </ul>
)}

Trata siempre el estado vacío con un mensaje amigable, en vez de mostrar una lista en blanco.

Ordenación de lista
const [ordem, setOrdem] = useState("asc");

const ordenados = useMemo(() => {
  return [...ítems].sort((a, b) =>
    ordem === "asc"
      ? a.nombre.localeCompare(b.nombre)
      : b.nombre.localeCompare(a.nombre)
  );
}, [ítems, ordem]);

<button onClick={() =>
  setOrdem(o => o === "asc" ? "desc" : "asc")
}>
  Ordenar: {ordem}
</button>

Crea una copia con [...ítems] antes de ordenar (sort muta el original). El useMemo evita reordenar en cada render.

Infinite scroll
const [pagina, setPagina] = useState(1);
const [ítems, setItens] = useState([]);

useEffect(() => {
  fetch(`/api/ítems?page=${pagina}`)
    .then(r => r.json())
    .then(nuevos => setItens(prev => [...prev, ...nuevos]));
}, [pagina]);

// Detectar el final de la página:
const onScroll = () => {
  if (window.innerHeight + window.scrollY >=
      document.body.offsetHeight - 200) {
    setPagina(p => p + 1);
  }
};
window.addEventListener("scroll", onScroll);

El infinite scroll carga más datos cuando el usuario llega al final. Detecta la posición del scroll e incrementa la página.

La importancia del key
// ✅ Correcto: id único y estable
{ítems.map(i => <li key={i.id}>{i.nombre}</li>)}

// ❌ Evitar: usar el índice
{ítems.map((i, idx) => <li key={idx}>{i.nombre}</li>)}

El key identifica cada item de forma única. Usar el índice puede causar bugs cuando la lista se reordena o filtra.

Variable de contenido
let contenido;

if (loading) {
  contenido = <Spinner />;
} else if (error) {
  contenido = <p>Error ao cargar</p>;
} else {
  contenido = <Datos datos={datos} />;
}

return <div>{contenido}</div>;

Para condiciones con varias ramas, asigna el JSX a una variable con if/else y renderízala al final. Más legible que ternarios anidados.

Paginación simple
const [pagina, setPagina] = useState(1);
const porPagina = 10;

const inicio = (pagina - 1) * porPagina;
const visibles = ítems.slice(inicio, inicio + porPagina);
const totalPaginas = Math.ceil(ítems.length / porPagina);

return (
  <>
    {visibles.map(i => <Cartao key={i.id} {...i} />)}
    <button disabled={pagina === 1}
      onClick={() => setPagina(p => p - 1)}>Anterior</button>
    <span>{pagina} / {totalPaginas}</span>
    <button disabled={pagina === totalPaginas}
      onClick={() => setPagina(p => p + 1)}>Siguiente</button>
  </>
);

La paginación muestra solo una porción de los datos con slice. Los botones anterior/siguiente controlan la página actual.

Lista agrupada
const grupos = ítems.reduce((acc, item) => {
  (acc[item.categoria] ??= []).push(item);
  return acc;
}, {});

return (
  <div>
    {Object.entries(grupos).map(([cat, lista]) => (
      <section key={cat}>
        <h3>{cat}</h3>
        <ul>
          {lista.map(i => <li key={i.id}>{i.nombre}</li>)}
        </ul>
      </section>
    ))}
  </div>
);

Usa reduce() para agrupar items por categoría y luego Object.entries() para iterar los grupos con sus items.

Renderizado condicional
// Ternario:
{logado ? <Painel /> : <Login />}

// && (cortocircuito):
{errores.length > 0 && (
  <div className="error">{errores[0]}</div>
)}

El ternario elige entre dos opciones. El operador && muestra algo solo cuando la condición es verdadera.

Listas anidadas
{categorias.map(cat => (
  <div key={cat.id}>
    <h3>{cat.nombre}</h3>
    <ul>
      {cat.ítems.map(item => (
        <li key={item.id}>{item.nombre}</li>
      ))}
    </ul>
  </div>
))}

Para listas dentro de listas, haz un map anidado. Cada nivel necesita su propio key único.

Filtro en tiempo real
const [búsqueda, setBusca] = useState("");

const filtrados = useMemo(() => {
  if (!búsqueda) return ítems;
  const termo = búsqueda.toLowerCase();
  return ítems.filter(i =>
    i.nombre.toLowerCase().includes(termo)
  );
}, [ítems, búsqueda]);

return (
  <>
    <input value={búsqueda}
      onChange={e => setBusca(e.target.value)}
      placeholder="Buscar..." />
    {filtrados.map(i => <Cartao key={i.id} {...i} />)}
  </>
);

El filtro en tiempo real usa useMemo para recalcular solo cuando la búsqueda o los items cambian. La lista se actualiza en cada tecla.

Condicionales con objeto
const icones = {
  éxito: "✅",
  error: "❌",
  aviso: "⚠️",
  info: "ℹ️",
};

function Alerta({ tipo, msg }) {
  return (
    <div className={`alerta-${tipo}`}>
      <span>{icones[tipo] ?? "❓"}</span>
      {msg}
    </div>
  );
}

// Uso:
<Alerta tipo="éxito" msg="Guardado!" />

Un objeto de lookup reemplaza largas cadenas de if/else o switch. El operador ?? da un fallback para claves inexistentes.

Filtrar y transformar
const ativos = productos
  .filter(p => p.activo)
  .map(p => (
    <Cartao key={p.id} producto={p} />
  ));

return <div>{ativos}</div>;

Combina filter (para seleccionar) con map (para renderizar). Primero filtras, luego transformas en componentes.

Ternario vs &&
// Ternario: dos resultados posibles
{admin ? <PainelAdmin /> : <PainelUser />}

// &&: muestra solo si es verdadero
{errores.length > 0 && <Alerta errores={errores} />}

// Cuidado: 0 && <X /> renderiza "0"!
{count > 0 && <p>{count} ítems</p>}

Usa un ternario cuando hay dos opciones y && para mostrar u ocultar. Atención: 0 && ... muestra el cero — verifica con > 0.

Virtualización (react-window)
import { FixedSizeList } from "react-window";

<FixedSizeList
  height={400}
  itemCount={10000}
  itemSize={35}
  width="100%"
>
  {({ index, style }) => (
    <div style={style}>Item {index}</div>
  )}
</FixedSizeList>

La virtualización renderiza solo los items visibles en el viewport. Con 10.000 items, solo ~20 están en el DOM. Esencial para listas enormes.

Estilos e CSS


10 cards
Inline styles
const estilo = {
  color: "white",
  backgroundColor: "blue",
  padding: "1rem",
  borderRadius: "8px"
};

<div style={estilo}>Hola</div>
<p style={{ fontSize: 14 }}>Texto</p>

El atributo style recibe un objeto JavaScript con propiedades en camelCase. Bueno para estilos dinámicos simples.

Variables CSS (tema)
/* tema.css */
:root {
  --color-primaria: #3b82f6;
  --spacing: 1rem;
}

/* componente */
.cartao {
  color: var(--color-primaria);
  padding: var(--spacing);
}

Las variables CSS (--nombre) centralizan colores y espaciados. Cambiar el tema es tan simple como cambiar el valor en la raíz.

CSS-in-JS (styled-components)
import styled from "styled-components";

const Botón = styled.button`
  background: ${props => props.$primario ? "#0070f3" : "#eee"};
  color: ${props => props.$primario ? "#fff" : "#333"};
  padding: 8px 16px;
  border-radius: 6px;
  border: none;
`;

// Uso:
<Botón $primario>Salvar</Botón>
<Botón>Cancelar</Botón>

styled-components permite escribir CSS real dentro del JS. Las props dinámicas generan estilos condicionales sin clases.

CSS Modules
/* Botón.module.css */
.botón { padding: 8px 16px; }
.primario { background: blue; }
import styles from "./Botón.module.css";

<button className={styles.botón}>Clicar</button>

Los CSS Modules dan un nombre único a cada clase, evitando conflictos globales. El estilo queda con alcance local al componente.

Animaciones CSS
/* styles.css */
@keyframes aparecer {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

.cartao {
  animation: aparecer 0.3s ease-out;
}
// En el componente:
<div className="cartao">Contenido</div>

Las animaciones CSS (@keyframes) dan transiciones suaves sin JavaScript. Aplica la clase al elemento y el browser anima automáticamente.

Tailwind CSS
function Card({ título, activo }) {
  return (
    <div className={
      "rounded-lg p-4 shadow " +
      (activo ? "bg-blue-500 text-white" : "bg-white text-gray-800")
    }>
      <h2 className="text-xl font-bold">{título}</h2>
      <p className="mt-2 text-sm opacity-75">Contenido do card</p>
    </div>
  );
}

// Con template literals:
<div className={`p-4 ${activo ? "bg-blue-500" : "bg-white"}`}>

Tailwind usa clases utilitarias directamente en el JSX. Combina con ternarios o template literals para estilos condicionales.

className condicional
<div className={`cartao ${activo ? "activo" : ""}`}>

<button className={`btn ${
  variante === "primary"
    ? "btn-primary"
    : "btn-secondary"
}`}>

Combina clases fijas y dinámicas con template literals. La clase se aplica según la condición.

clsx / classnames
import clsx from "clsx";

<button className={clsx(
  "btn",
  {
    "btn-primary": variante === "primary",
    "btn-lg": grande,
    "disabled": !activo,
  }
)}>
  Clicar
</button>

La librería clsx junta clases condicionalmente de forma limpia. Evita template literals complejos con múltiples condiciones.

Estilos dinámicos
function Barra({ progresso }) {
  return (
    <div
      style={{
        width: `${progresso}%`,
        backgroundColor: progresso > 80 ? "green" : "orange"
      }}
    />
  );
}

Calcula estilos a partir del estado o props. El objeto de estilo se recrea en cada render con los valores actuales.

Dark mode toggle
const [tema, setTema] = useState("light");

useEffect(() => {
  document.documentElement.dataset.tema = tema;
}, [tema]);

<button onClick={() =>
  setTema(t => t === "light" ? "dark" : "light")
}>
  {tema === "light" ? "🌙" : "☀️"}
</button>

/* CSS: */
[data-tema="dark"] { --fondo: #1a1a2e; --texto: #eee; }
[data-tema="light"] { --fondo: #fff; --texto: #333; }

El dark mode alterna un atributo en el html y el CSS usa variables para cambiar los colores. El estado guarda la preferencia.

Padrões Avançados


13 cards
Context API
import { createContext, useContext } from "react";

const ThemeCtx = createContext("light");

// Provider:
<ThemeCtx.Provider value="dark">
  <App />
</ThemeCtx.Provider>

// Consumer:
function Botón() {
  const tema = useContext(ThemeCtx);
  return <button className={tema}>...</button>;
}

El Context comparte datos con todo el árbol de componentes sin pasar props por cada nivel (prop drilling).

forwardRef
import { forwardRef } from "react";

const Input = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});

// En el padre:
const ref = useRef();
<Input ref={ref} />
ref.current.focus();

forwardRef permite pasar una ref a un elemento dentro de un componente hijo. Útil para enfocar inputs o medir elementos.

Error Boundary
import { Component } from "react";

class ErrorBoundary extends Component {
  state = { error: null };

  static getDerivedStateFromError(error) {
    return { error };
  }

  render() {
    if (this.state.error) {
      return <h1>Algo salió mal: {this.state.error.message}</h1>;
    }
    return this.props.children;
  }
}

// Uso:
<ErrorBoundary>
  <ComponenteQuePodeFalhar />
</ErrorBoundary>

Un Error Boundary captura errores de renderizado en los hijos y muestra un fallback. Aún requiere un class component (el único caso).

Dividir Context (performance)
// ❌ Un context gigante re-renderiza a todos:
<Ctx.Provider value={{ user, tema, dispatch }}>

// ✅ Contexts separados re-renderizan solo a quien usa:
<UserCtx.Provider value={user}>
  <TemaCtx.Provider value={tema}>
    <App />
  </TemaCtx.Provider>
</UserCtx.Provider>

// O memoriza el value:
<Ctx.Provider value={useMemo(
  () => ({ user, dispatch }), [user]
)}>

Cambiar un context re-renderiza todos los consumidores. Divide en contexts pequeños o memoriza el value con useMemo para que cada componente solo se re-renderice cuando los datos que usa cambien.

Estado global simple
const AppCtx = createContext();

function AppProvider({ children }) {
  const [state, dispatch] = useReducer(
    reducer, estadoInicial
  );
  return (
    <AppCtx.Provider value={{ state, dispatch }}>
      {children}
    </AppCtx.Provider>
  );
}

// En cualquier componente:
const { state, dispatch } = useContext(AppCtx);

Combina useContext + useReducer para crear un estado global simple, sin librerías externas.

Compound Components
function Tabs({ children }) {
  const [activa, setAtiva] = useState(0);
  return (
    <div>
      {children.map((child, i) =>
        cloneElement(child, {
          activa: i === activa,
          onClick: () => setAtiva(i),
        })
      )}
    </div>
  );
}

<Tabs>
  <Tab label="Perfil">...</Tab>
  <Tab label="Ajustes">...</Tab>
</Tabs>

En el patrón Compound Components, los componentes padre e hijo comparten estado implícitamente. La API queda declarativa y flexible.

Portal (createPortal)
import { createPortal } from "react-dom";

function Modal({ abierto, children, onFechar }) {
  if (!abierto) return null;

  return createPortal(
    <div className="overlay" onClick={onFechar}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body  // renderiza fuera del árbol
  );
}

createPortal renderiza los hijos en un nodo DOM diferente (ej.: body). Ideal para modales, tooltips y dropdowns que necesitan escapar del overflow.

Lazy loading
import { lazy, Suspense } from "react";

const Painel = lazy(() => import("./Painel"));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Painel />
    </Suspense>
  );
}

lazy carga el componente solo cuando se necesita (code-splitting). Suspense muestra un fallback durante la carga.

Slot pattern (children)
function Layout({ header, sidebar, children }) {
  return (
    <div className="layout">
      <header>{header}</header>
      <aside>{sidebar}</aside>
      <main>{children}</main>
    </div>
  );
}

<Layout
  header={<Nav />}
  sidebar={<Menu />}
>
  <Pagina />
</Layout>

El patrón slot usa props para inyectar JSX en zonas específicas del layout. Más explícito que children para múltiples áreas.

Higher-Order Component (HOC)
function comAuth(Componente) {
  return function ComponenteProtegido(props) {
    const { user } = useAuth();

    if (!user) {
      return <p>Haz login para continuar.</p>;
    }

    return <Componente {...props} user={user} />;
  };
}

// Uso:
const PainelProtegido = comAuth(Painel);

// En el router:
<PainelProtegido />

Un HOC es una función que recibe un componente y devuelve uno nuevo con comportamiento extra. Útil para autenticación, logging o inyección de props.

Render props
function Datos({ url, children }) {
  const [datos, setDados] = useState(null);

  useEffect(() => {
    fetch(url).then(r => r.json()).then(setDados);
  }, [url]);

  return children(datos);
}

<Datos url="/api/user">
  {(user) => <p>{user?.nombre}</p>}
</Datos>

En el patrón render props, children es una función que recibe datos del componente. Invierte el control de forma flexible.

Suspense para datos
// Con React Query / SWR:
function Productos() {
  const { data, isLoading } = useQuery(
    ["productos"],
    () => fetch("/api/productos").then(r => r.json())
  );

  if (isLoading) return <Spinner />;
  return <Lista ítems={data} />;
}

// O con Suspense nativo (React 19+):
<Suspense fallback={<Spinner />}>
  <Productos />
</Suspense>

Librerías como React Query o SWR gestionan caché, loading y errores. Suspense muestra el fallback mientras los datos cargan.

ref como prop (React 19)
// React 19: ref es una prop normal (sin forwardRef):
function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

// En el padre:
const ref = useRef();
<Input ref={ref} />
ref.current.focus();

// En React 18 era necesario:
// const Input = forwardRef((props, ref) => ...);

En React 19, ref pasa a ser una prop normal de los componentes de función — forwardRef deja de ser necesario. Basta recibir { ref } y aplicarla al elemento.

Ecossistema e CLI


16 cards
Crear proyecto (Vite)
npm create vite@latest mi-app -- --template react
cd mi-app
npm install
npm run dev

Vite crea un proyecto React con hot reload instantáneo. Es la forma recomendada de empezar un proyecto nuevo.

Estructura de carpetas
src/
├── components/    # Componentes reutilizables
├── pages/         # Páginas por ruta
├── hooks/         # Custom hooks
├── services/      # Llamadas a la API
├── utils/         # Funciones utilitarias
└── App.jsx

Una estructura organizada por responsabilidad facilita encontrar y mantener el código a medida que el proyecto crece.

Deploy (build)
# Build de producción
npm run build

# Preview local del build
npm run preview

# El output queda en dist/
# Haz deploy a: Vercel, Netlify, Cloudflare Pages

El build genera ficheros optimizados (minificados, con hash) en la carpeta dist/. Cualquier servidor estático o CDN los sirve.

TanStack Router
import { createRouter, RouterProvider } from "@tanstack/react-router";

const router = createRouter({
  routes: [
    { path: "/", component: Home },
    { path: "/users/$id", component: UserDetail },
  ],
});

function App() {
  return <RouterProvider router={router} />;
}

// En el componente:
const { id } = useParams({ from: "/users/$id" });

TanStack Router es type-safe por naturaleza: rutas, params y search se infieren automáticamente por TypeScript.

React Router
import { BrowserRouter, Routes, Route, Link }
  from "react-router-dom";

<BrowserRouter>
  <nav>
    <Link to="/">Inicio</Link>
    <Link to="/sobre">Sobre</Link>
  </nav>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/sobre" element={<Sobre />} />
    <Route path="/user/:id" element={<Perfil />} />
  </Routes>
</BrowserRouter>

React Router gestiona la navegación en una SPA. Define rutas con Route y navega con Link sin recargar la página.

Buenas prácticas
• Componentes pequeños y enfocados
• key estable en listas (no usar index)
• Estado lo más abajo posible
• useMemo/useCallback solo cuando sea necesario
• Separar lógica en custom hooks
• TypeScript para props tipadas

Convenciones que mantienen el código React limpio, rápido y fácil de mantener a largo plazo.

Next.js (básico)
npx create-next-app@latest mi-app

// app/page.jsx (Server Component por defecto)
export default function Home() {
  return <h1>Hola Next.js</h1>;
}

// app/api/datos/route.js (API route)
export async function GET() {
  return Response.json({ ok: true });
}

// Routing por carpetas: app/sobre/page.jsx = /sobre

Next.js es un framework React con routing por ficheros, SSR y API routes. Cada carpeta en app/ es una ruta automáticamente.

Vitest (tests)
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
import Contador from "./Contador";

test("mostra o contador", () => {
  render(<Contador />);
  expect(screen.getByText("0")).toBeInTheDocument();
});

test("incrementa ao clicar", async () => {
  render(<Contador />);
  await userEvent.click(screen.getByRole("button"));
  expect(screen.getByText("1")).toBeInTheDocument();
});

// Ejecutar: npx vitest

Vitest es el test runner por defecto de Vite. Con la Testing Library, prueba el comportamiento (clicar, ver texto) en vez de la implementación.

useParams y useNavigate
import { useParams, useNavigate }
  from "react-router-dom";

function Perfil() {
  const { id } = useParams();
  const navigate = useNavigate();

  const volver = () => navigate(-1);
  const irHome = () => navigate("/");
}

useParams lee parámetros de la URL (:id). useNavigate permite navegar programáticamente.

Variables de entorno
// Fichero .env
VITE_API_URL=https://api.ejemplo.com

// En el código (Vite):
const url = import.meta.env.VITE_API_URL;

// Create React App:
const url = process.env.REACT_APP_API_URL;

Las variables de entorno guardan configuraciones (URLs, claves públicas) fuera del código. En Vite, empiezan por VITE_.

Zustand (estado global)
import { create } from "zustand";

const useStore = create((set) => ({
  count: 0,
  incrementar: () => set(s => ({ count: s.count + 1 })),
  reset: () => set({ count: 0 }),
}));

// En cualquier componente:
function Contador() {
  const { count, incrementar } = useStore();
  return <button onClick={incrementar}>{count}</button>;
}

Zustand es una alternativa minimalista a Redux para estado global. Sin providers, sin boilerplate — un hook simple.

Server Components
// Server Component (por defecto en Next.js App Router):
// Corre en el servidor y puede acceder a la BD directamente:
async function Productos() {
  const productos = await db.query(
    "SELECT * FROM productos"
  );
  return (
    <ul>
      {productos.map(p => <li key={p.id}>{p.nombre}</li>)}
    </ul>
  );
}

// Client Component (necesita hooks/eventos):
"use client";
function Contador() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}

Los Server Components corren en el servidor y envían HTML listo — ideales para buscar datos sin enviar JavaScript extra. Usa "use client" solo en los componentes que necesitan hooks o eventos.

Fetch con async/await
function Productos() {
  const [datos, setDados] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function buscar() {
      const res = await fetch("/api/productos");
      const json = await res.json();
      setDados(json);
      setLoading(false);
    }
    buscar();
  }, []);

  if (loading) return <p>A cargar...</p>;
  return <Lista ítems={datos} />;
}

Patrón completo de data fetching: estado de loading, petición asíncrona en useEffect y renderizado condicional.

Axios (alternativa a fetch)
import axios from "axios";

useEffect(() => {
  axios.get("/api/productos")
    .then(res => setDados(res.data))
    .catch(err => setErro(err.message));
}, []);

// POST:
axios.post("/api/user", { nombre: "Ana" });

Axios es una librería de peticiones HTTP. Da el JSON ya procesado en res.data y tiene una API más simple que fetch.

React Hook Form
import { useForm } from "react-hook-form";

function Login() {
  const { register, handleSubmit, formState } = useForm();

  const onSubmit = (datos) => console.log(datos);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email", { required: true })} />
      {formState.errors.email && <span>Obligatorio</span>}
      <button>Entrar</button>
    </form>
  );
}

React Hook Form gestiona formularios con performance (sin re-render en cada tecla). El register conecta el input y la validación declarativa.

StrictMode
import { StrictMode } from "react";

root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

// En desarrollo, StrictMode:
// - renderiza todo 2x (detecta renders impuros)
// - ejecuta efectos 2x (detecta efectos sin cleanup)
// En producción no tiene ningún efecto.

StrictMode es una herramienta de desarrollo que detecta problemas: renderiza todo dos veces y ejecuta efectos de más, exponiendo renders impuros y efectos sin cleanup. No afecta a producción.