Cheatsheet React
Biblioteca JavaScript para interfaces
React
Componentes
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 propsUn 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
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.valueEn 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, mdUsa 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
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
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 teclaLos 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
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
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
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
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 = /sobreNext.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 vitestVitest 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.