11 · Estado y propiedad en React#
Objetivo#
Al terminar esta lección podrás diseñar la arquitectura de estado de una aplicación React determinando con precisión qué componente debe poseer cada dato, calculando valores derivados de forma síncrona y pura durante el render sin duplicar fuentes de verdad, y sincronizando tu interfaz con sistemas externos mediante useEffect y funciones de limpieza (cleanup) simétricas que eviten fugas de memoria y respuestas obsoletas.
Antes de empezar#
- Completa 10 · TypeScript y el DOM y comprende el funcionamiento de tipos, uniones discriminadas y desestructuración de objetos en JavaScript.
- Los ejemplos de esta lección son fragmentos de código en TSX diseñados para un entorno moderno de React (versión 18 o superior) con TypeScript.
- Puedes experimentar con estos componentes utilizando un entorno local con Vite (
npm create vite@latest) o en entornos de prueba en el navegador como StackBlitz o CodeSandbox. - Si encuentras algún concepto técnico desconocido, consulta el glosario técnico.
Diccionario de conceptos de la lección (Glosario desde cero)#
1. Props vs Estado (useState)#
- Intuición cotidiana:
- Props: La información impresa en tu pasaporte (nombre, fecha de nacimiento, nacionalidad). Te la otorga una entidad externa (el gobierno); tú no puedes alterar unilateralmente tu pasaporte tachándolo con bolígrafo.
- Estado: Lo que llevas puesto hoy (tu camisa, tus gafas, tu reloj). Tú decides cambiarte de ropa cuando quieras según la temperatura o tu preferencia.
- Definición técnica precisa:
- *Props (Properties):* Argumentos de entrada inmutables pasados de un componente padre a un componente hijo en el árbol de React. El hijo no debe mutar sus props; cualquier cambio debe solicitarse al padre a través de callbacks.
- *Estado (State con
useState): Memoria interna encapsulada dentro de un componente que retiene información entre sucesivos ciclos de renderizado. Al invocar su función actualizadora (setter*), React programa un nuevo ciclo de render para reflejar el cambio en la pantalla. - Ejemplo en código:
// tasks proviene de las props (pertenece al padre)
// query es estado local (pertenece a este componente)
function TaskSearch({ tasks }: { tasks: Task[] }) {
const [query, setQuery] = useState("");
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
- Concepción errónea: Copiar una prop en el estado inicial de un hijo (
const [title, setTitle] = useState(props.initialTitle)) esperando que si la prop del padre cambia en el futuro, el estado del hijo se actualice automáticamente. El argumento deuseStatesolo se evalúa en el primer montaje.
2. Estado mínimo (Single Source of Truth) vs Estado derivado (Derived State)#
- Intuición cotidiana: Si tienes una cesta de frutas con 3 manzanas y 2 naranjas:
- Estado mínimo: La lista de frutas en la cesta.
- Estado derivado: El conteo total de frutas (5). No necesitas anotar el número "5" en un papel separado y actualizarlo cada vez que alguien come una fruta; basta con contar lo que hay en la cesta en el momento en que alguien pregunte.
- Definición técnica precisa:
- *Estado mínimo (Single Source of Truth): El conjunto más pequeño e irreductible de datos necesarios para describir completamente el estado actual de la interfaz. Si un valor puede calcularse a partir de otros datos ya existentes (props o estado), no debe almacenarse en el estado*.
- *Estado derivado (Derived State):* Expresión o cálculo computado en tiempo real durante la ejecución de la función del componente en el render. Garantiza que la vista nunca esté desfasada respecto a los datos de origen.
- Ejemplo en código:
// CORRECTO: tasks y query son la única fuente de verdad; visibleTasks se deriva
const [tasks, setTasks] = useState<Task[]>([]);
const [query, setQuery] = useState("");
const visibleTasks = tasks.filter(t => t.title.toLowerCase().includes(query.toLowerCase()));
const pendingCount = tasks.filter(t => !t.done).length;
- Concepción errónea: Crear
const [filteredTasks, setFilteredTasks] = useState([])y sincronizarlo con unuseEffect. Esto introduce dos fuentes de verdad que inevitablemente divergen, provocando desincronizaciones y renders redundantes.
3. Renderizado, Reconciliación e Instantánea de render (Render as a Snapshot)#
- Intuición cotidiana: Un animador dibujando los fotogramas de una película de dibujos animados: cada fotograma es un dibujo completo e inmutable en papel congelado en ese instante de tiempo. Para mostrar movimiento, el animador no borra con goma el fotograma anterior; dibuja un fotograma nuevo completo y lo proyecta rápidamente en la pantalla.
- Definición técnica precisa:
- *Renderizado (Render):* Proceso en el que React invoca la función de tu componente para obtener la descripción del árbol de elementos UI (nodos de JSX / Virtual DOM) correspondientes a las props y estado de ese momento.
- *Instantánea de render (Snapshot): Dentro de la ejecución de una función de componente concreta, las variables de props y estado son constantes e inmutables. Forman una fotografía fija capturada por la clausura (closure*) de JavaScript.
- *Reconciliación (Reconciliation): Algoritmo interno (React Fiber) que compara el nuevo árbol de elementos virtuales contra el árbol del render anterior (diffing*) y calcula el conjunto mínimo de mutaciones necesarias para aplicar sobre el DOM real del navegador.
- Ejemplo conceptual:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1); // Solicita un nuevo render con count = 1
console.log(count); // ¡Imprime 0! La variable local en esta instantánea sigue valiendo 0.
}
return <button onClick={handleClick}>{count}</button>;
}
- Concepción errónea: Creer que llamar a
setCount(5)muta inmediatamente la variable localcounten la línea siguiente. Los setters solicitan un render futuro; no alteran la memoria de la función actual.
4. Inmutabilidad e igualdad referencial (Object.is)#
- Intuición cotidiana: Si tienes un contrato firmado en papel, no puedes tachar cláusulas con corrector líquido y seguir afirmando que es el documento legal original. Si se requiere una modificación, se redacta y firma un contrato completamente nuevo con una nueva fecha y número de folio.
- Definición técnica precisa:
- Inmutabilidad: Principio de diseño donde los objetos y arreglos no se modifican directamente tras su creación. En su lugar, cualquier cambio produce una nueva referencia en memoria con los valores actualizados y los elementos no modificados copiados superficialmente (shallow copy con el operador spread
...). - Igualdad referencial (
Object.is): Algoritmo que utiliza React para determinar si el estado ha cambiado. Compara los punteros de memoria de la referencia anterior y la nueva:prevRef stackrel{?}{=} nextRefSi mutas un arreglo directamente (tasks.push(newTask)), la dirección de memoria sigue siendo idéntica; React evalúa que nada ha cambiado y cancela el renderizado (bailout). - Ejemplo en código:
// INCORRECTO: Muta la referencia en memoria -> React ignora el cambio
tasks.push({ id: 2, title: "Mutated", done: false });
setTasks(tasks); // Object.is(tasks, tasks) === true -> NO HAY RE-RENDER
// CORRECTO: Nueva referencia en memoria -> React detecta el cambio
setTasks([...tasks, { id: 2, title: "Immutable", done: false }]);
- Concepción errónea: Pensar que crear nuevos objetos y arrays con spread causa problemas de rendimiento en interfaces comunes. La comparación de referencias
Object.ises de orden O(1) (extremadamente rápida), mientras que comparar recursivamente estructuras profundas sería inviable.
5. Actualizador funcional (setState(prev => ...))#
- Intuición cotidiana: Dejar una nota adhesiva en la puerta de la cocina que dice: "Quien abra la nevera, añada 1 al número de refrescos comprados". Da igual quién llegue primero o cuántos mensajes se acumulen; cada persona toma el valor actual en ese instante y le suma 1 ordenadamente.
- Definición técnica precisa: Firma sobrecargada de la función actualizadora de estado que acepta una función pura en lugar de un valor directo:
setCount(prevCount => prevCount + 1). React encola esta función en su cola de actualizaciones y le inyecta el valor de estado más reciente garantizado en el momento exacto en que se procesa el lote de renderizado (batching). - Ejemplo en código:
// Sin actualizador funcional (problema de captura de instantánea):
setCount(count + 1); // Si count era 0 -> solicita 1
setCount(count + 1); // Sigue leyendo count = 0 -> solicita 1. Resultado final: 1
// Con actualizador funcional:
setCount(prev => prev + 1); // Encola: 0 + 1 = 1
setCount(prev => prev + 1); // Encola: 1 + 1 = 2. Resultado final: 2
- Concepción errónea: Pensar que el actualizador funcional solo sirve cuando hay múltiples setters consecutivos. Es fundamental siempre que la siguiente versión del estado dependa del valor anterior, especialmente en callbacks asíncronos o temporizadores.
6. Efecto secundario (useEffect) vs Cálculo puro de render#
- Intuición cotidiana:
- Cálculo puro de render: Una receta de cocina escrita en un libro: si multiplicas los ingredientes por 2 para 4 comensales, es una operación matemática pura en tu cabeza; no afecta al mundo exterior.
- Efecto secundario (
useEffect): Encender el horno eléctrico o hacer una llamada telefónica para pedir los ingredientes al supermercado. Altera el estado del mundo físico fuera de tu cabeza. - Definición técnica precisa:
- Cálculo de render: Toda operación sincrónica que toma props y estado y produce JSX sin alterar ninguna variable global, sin mutar objetos externos y sin comunicarse con APIs del navegador. Debe ser determinista y pura.
useEffect: Hook de React diseñado exclusivamente para sincronizar el componente con un sistema externo que no está bajo el control del ciclo de renderizado de React (por ejemplo: el objeto globalwindow, una conexión WebSocket, una API imperativa del navegador comoIntersectionObserver, o temporizadoressetInterval). Se ejecuta de forma asíncrona después de que el navegador ha pintado la pantalla.- Ejemplo en código:
// Antipatrón: Usar un Effect para un cálculo matemático interno
useEffect(() => {
setCount(tasks.length); // ¡PROHIBIDO! Provoca un render extra innecesario
}, [tasks]);
// Uso legítimo de useEffect: Suscribirse a un evento del sistema operativo
useEffect(() => {
function handleResize() { console.log(window.innerWidth); }
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
- Concepción errónea: Tratar a
useEffectcomo un "manejador de eventos para cuando cambie una variable". Los eventos del usuario pertenecen a los event handlers (onClick,onSubmit), no a Effects.
7. Arreglo de dependencias (Dependency Array)#
- Intuición cotidiana: Una lista de verificación para un guardia de seguridad: "Solo activa la alarma si cambia la persona de la lista o la contraseña de la puerta". Si ninguna de esas dos cosas cambia entre turnos, el guardia no hace nada y descansa.
- Definición técnica precisa: Segundo argumento opcional pasado a
useEffect(yuseMemo/useCallback) compuesto por un arreglo de valores reactivos (props, estado o variables calculadas durante el render). React compara cada elemento del arreglo contra su valor en el render anterior utilizandoObject.is. Si todos los valores son idénticos, React omite la ejecución del efecto en ese ciclo. - Tres modalidades fundamentales:
useEffect(() => { ... })(Sin arreglo): Se ejecuta tras cada renderizado.useEffect(() => { ... }, [])(Arreglo vacío): Se ejecuta únicamente una vez tras el montaje inicial del componente.useEffect(() => { ... }, [id, query])(Con dependencias): Se ejecuta en el montaje y cada vez queidoquerycambien de referencia.
- Concepción errónea: Ocultar dependencias intencionalmente para "evitar que el efecto se ejecute muchas veces" (desactivar la regla de linter
react-hooks/exhaustive-deps). Ocultar dependencias produce stale closures (clausuras con valores obsoletos y bugs difíciles de reproducir).
8. Función de limpieza (Cleanup function)#
- Intuición cotidiana: Alquilar un departamento: al entrar (montaje) conectas el servicio de gas e internet. Al mudarte (desmontaje), es tu obligación estricta llamar a la compañía de gas para dar de baja el suministro. Si no llamas, el gas sigue corriendo y la factura te seguirá cobrando indefinidamente.
- Definición técnica precisa: Función devuelta opcionalmente por el callback de
useEffect:return () => { ... }. React ejecuta esta función en dos momentos exactos:
- Antes de volver a ejecutar el efecto con nuevas dependencias (para deshacer la suscripción del render anterior).
- Cuando el componente se desmonta de la jerarquía de la interfaz (para liberar recursos).
- Ejemplo en código:
useEffect(() => {
const timerId = setInterval(() => { console.log("Tick"); }, 1000);
// Función de limpieza simétrica obligatoria:
return () => {
clearInterval(timerId); // Previene fuga de memoria
};
}, []);
- Concepción errónea: Pensar que la limpieza solo se ejecuta cuando el componente desaparece de la pantalla. Si el efecto tiene dependencias que cambian con frecuencia, la función de limpieza se ejecuta en cada cambio, limpiando el estado viejo antes de iniciar el nuevo.
9. Propiedad key en listas#
- Intuición cotidiana: En el guardarropa de un teatro, cada abrigo recibe una ficha numerada metálica única. Aunque la gente entre y salga desordenadamente, el empleado del guardarropa siempre te devuelve exactamente tu abrigo buscando tu número de ficha. Si el empleado identificara los abrigos por "la posición en la que están colgados", el primer abrigo que retiren desfasaría todas las posiciones de los demás.
- Definición técnica precisa: Propiedad especial de cadena o número que debe asignarse a cada elemento generado al iterar sobre una colección en JSX (
tasks.map(t => <Item key={t.id} />)). La clave le otorga a cada nodo una identidad persistente y estable a través de los renders. El algoritmo de reconciliación utiliza lakeypara distinguir qué elementos fueron agregados, eliminados, reordenados o modificados en el DOM real. - Ejemplo comparativo:
// CORRECTO: Identificador único universal y persistente del dominio
{tasks.map(task => <li key={task.id}>{task.title}</li>)}
// ANTIPATRÓN PELIGROSO: Usar el índice del array en colecciones mutables
{tasks.map((task, index) => <li key={index}>{task.title}</li>)}
- Concepción errónea: Usar
key={index}. Si la lista se reordena o se borra un elemento intermedio, los índices cambian, provocando que React asocie estados internos de componentes hijos (como inputs de texto o animaciones) al elemento equivocado.
10. Memoización (useMemo)#
- Intuición cotidiana: Una libreta de notas al lado del teléfono donde anotas el resultado de una multiplicación matemática gigantesca de 15 dígitos. Cada vez que tu jefe te pregunta el resultado con los mismos dos números, lees tu libreta al instante en 1 segundo en lugar de tardar 20 minutos recalculando a mano en la pizarra.
- Definición técnica precisa: Hook de optimización de rendimiento que almacena en caché el resultado del cálculo de una función pura entre renders:
const val = useMemo(() => computeExpensive(a, b), [a, b]). React solo vuelve a ejecutar la función si al menos una de las dependencias ha cambiado de referencia. - Ejemplo en código:
const sortedTasks = useMemo(() => {
// Solo se re-ejecuta si tasks o sortBy cambian:
return [...tasks].sort((a, b) => a.title.localeCompare(b.title));
}, [tasks, sortBy]);
- Concepción errónea: Envolver absolutamente todos los cálculos en
useMemopor reflejo.useMemotiene un costo de memoria y tiempo de CPU (debe reservar memoria para las dependencias y ejecutar comparacionesObject.isen cada render). Si el cálculo es trivial (como filtrar 50 elementos),useMemopuede ser más lento que el cálculo directo.
Modelo mental y desempacado técnico paso a paso#
1. La cascada de dos pasos y el parpadeo visual (Render Cascade & UI Glitch)#
Examinemos la diferencia arquitectónica fundamental entre duplicar estado derivado con useEffect vs calcularlo síncronamente durante el render:
sequenceDiagram
autonumber
actor U as Usuario
participant Comp as Componente React
participant Eff as useEffect
participant DOM as DOM del Navegador / Pantalla
Note over Comp,DOM: CASO A: Antipatrón con useState + useEffect (2 Pases)
U->>Comp: Teclea letra "b" en el buscador
Comp->>Comp: setQuery("b") dispara ciclo de render 1
Comp->>DOM: Pinta Pantalla (Render 1): query="b", ¡pero visibleTasks aún tiene datos viejos!
DOM-->>U: Parpadeo visual: UI desincronizada momentáneamente
Comp->>Eff: Ejecuta Effect tras pintar: setVisibleTasks(tasks.filter(...))
Eff->>Comp: setVisibleTasks dispara ciclo de render 2
Comp->>DOM: Pinta Pantalla (Render 2): visibleTasks finalmente actualizado
Note over Comp,DOM: CASO B: Estado Mínimo y Cálculo Síncrono (1 Solo Pase)
U->>Comp: Teclea letra "b" en el buscador
Comp->>Comp: setQuery("b") dispara ciclo de render 1
Comp->>Comp: const visibleTasks = tasks.filter(...) en memoria instantánea
Comp->>DOM: Pinta Pantalla (Render 1): query="b" Y lista filtrada a la vez
DOM-->>U: Actualización limpia, instantánea y sin parpadeo (Cero glitches)
Anatomía de la falla en el Caso A:#
- Paso 1 (Render inconsistente): Cuando el usuario escribe una letra,
setQueryprograma un render. En este primer render,queryvale"b", pero la variable de estadovisibleTasksaún almacena la lista anterior completa. React genera el HTML y el navegador pinta la pantalla. Durante una fracción de segundo, el usuario ve el input con el texto nuevo pero la lista con los elementos no filtrados. - Paso 2 (Efecto tardío y segundo render): Una vez pintada la pantalla incoherente, el navegador entrega el control a React para ejecutar los
useEffect. El efecto detecta quequerycambió y llama asetVisibleTasks(filtered). Esta llamada obliga a React a programar inmediatamente un segundo ciclo de renderizado completo, recalculando el Virtual DOM y pintando la pantalla de nuevo. - Consecuencias: Doble consumo de ciclos de CPU, posible parpadeo visual en pantallas de alta tasa de refresco, y código un 50% más largo y propenso a errores de sincronización.
La elegancia del Caso B:#
Al derivar visibleTasks directamente en el cuerpo de la función:
const [query, setQuery] = useState("");
const visibleTasks = tasks.filter(t => t.title.toLowerCase().includes(query.trim().toLowerCase()));
React ejecuta la función del componente, calcula la lista filtrada en microsegundos en la memoria RAM, y entrega al navegador el DOM definitivo y perfectamente sincronizado en un único pase.
2. Por qué setQuery("abc") no muta la variable local query (Clausuras de JS)#
Uno de los mayores motivos de desconcierto al aprender React es este código:
function SearchBox() {
const [query, setQuery] = useState("");
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value);
console.log("Current query:", query); // ¡Imprime la cadena vieja, no el nuevo valor!
}
return <input value={query} onChange={handleChange} />;
}
¿Por qué ocurre esto?
- La función del componente es una función JavaScript ordinaria:
Cuando React renderiza
SearchBox, invoca la función:SearchBox(). - *Variables locales en la pila de ejecución (Call Stack):*
Dentro de esa invocación concreta,
const [query, setQuery] = useState("")asigna a la constante localqueryel valor de la instantánea actual (supongamos,""). - Naturaleza de
consty el Scope de la función:queryes una constante inmutable en el ámbito léxico de esa ejecución de la función. Ninguna biblioteca externa (ni siquiera React) tiene el poder de alterar el valor de una constante local en la pila de ejecución de JavaScript mientras se está ejecutando. - Lo que hace realmente
setQuery:setQuery(nuevoValor)no toca la variablequery. Lo que hace es depositar un mensaje en la cola interna de trabajo de React: > "Por favor, programa una nueva llamada aSearchBox()para el futuro, y en esa próxima llamada entrégamenuevoValorcomo el primer elemento deuseState". - Por lo tanto, cualquier línea de código dentro de
handleChangeque leaqueryestará leyendo la variable congelada en la instantánea de ese render.
3. El ciclo de vida de useEffect y el doble montaje en React 18 Strict Mode#
Para comprender cómo opera la sincronización y la función de limpieza, examinemos el ciclo de vida temporal:
[ Render 1 (id = 1) ]
│
▼
[ Pintado en DOM ]
│
▼
[ Ejecuta Effect 1 (se suscribe a id = 1) ]
│
▼ (Usuario cambia selección a id = 2)
[ Render 2 (id = 2) ]
│
▼
[ Pintado en DOM ]
│
▼
[ 1. Ejecuta Cleanup de Effect 1 (desuscribe id = 1) ] <-- ¡Limpieza previa!
│
▼
[ 2. Ejecuta Effect 2 (se suscribe a id = 2) ]
│
▼ (Componente desaparece de la pantalla)
[ Desmontaje (Unmount) ]
│
▼
[ Ejecuta Cleanup de Effect 2 (desuscribe id = 2) ]
¿Por qué React 18 Strict Mode monta, desmonta y vuelve a montar en desarrollo?#
En entornos de desarrollo con <React.StrictMode>, React monta intencionalmente cada componente dos veces seguidas:
¿Por qué hace esto el equipo de React?
- Muchos desarrolladores olvidan escribir la función de limpieza (
return () => ...). - Si registras un listener global en
window.addEventListenero inicias un temporizador y olvidas limpiarlo, el componente parece funcionar bien la primera vez. Pero cada vez que el usuario navega de una pantalla a otra, se acumulan escuchadores zombis invisibles en la memoria. - Al forzar un ciclo inmediato de desmontaje y remontaje en desarrollo, React expone de forma inmediata cualquier fuga de memoria o efecto secundario asimétrico antes de que llegue a producción.
Diagrama#
flowchart TD
A[Estado mínimo: tareas y consulta] --> B[Render calcula visibleTasks]
B --> C[UI muestra lista]
D[Evento del usuario] --> E[Setter actualiza estado]
E --> A
F[Sistema externo] <--> G[Effect y cleanup]
El efecto conecta con fuera de React; calcular una lista filtrada no necesita ese puente.
Ejemplo razonado#
Este módulo TSX muestra un componente completo con estado mínimo, derivación síncrona, mutación inmutable por ID mediante actualizador funcional, y un componente con sincronización externa limpia:
import { useState, useEffect } from "react";
// 1. Data Contracts
export type Task = {
id: number;
title: string;
done: boolean;
};
// 2. Canonical Task List with Derived State and Immutable Updates
export function TaskManager({ initialTasks }: { initialTasks: Task[] }) {
// Minimal State: only what changes independently and cannot be derived
const [tasks, setTasks] = useState<Task[]>(initialTasks);
const [query, setQuery] = useState<string>("");
// Derived State (Computed synchronously in 1 pass during render)
const normalizedQuery = query.trim().toLowerCase();
const visibleTasks = tasks.filter((task) =>
task.title.toLowerCase().includes(normalizedQuery)
);
const totalCount = tasks.length;
const completedCount = tasks.filter((t) => t.done).length;
// Immutable Toggle Handler using Functional Updater
function handleToggleTask(taskId: number) {
setTasks((previousTasks) =>
previousTasks.map((task) =>
task.id === taskId ? { ...task, done: !task.done } : task
)
);
}
return (
<section aria-labelledby="manager-heading">
<h2 id="manager-heading">Task Manager</h2>
<p aria-live="polite">
Completed {completedCount} of {totalCount} tasks.
</p>
<label htmlFor="task-search">
Search tasks
</label>
<input
id="task-search"
type="search"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Filter by title..."
/>
{visibleTasks.length === 0 ? (
<p>No tasks match your criteria.</p>
) : (
<ul>
{visibleTasks.map((task) => (
<li key={task.id}>
<label>
<input
type="checkbox"
checked={task.done}
onChange={() => handleToggleTask(task.id)}
/>
<span style={{ textDecoration: task.done ? "line-through" : "none" }}>
{task.title}
</span>
</label>
</li>
))}
</ul>
)}
</section>
);
}
// 3. External System Synchronization with Symmetrical Cleanup
export function WindowScrollIndicator() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
// Symmetrical event listener definition
function handleScroll() {
setScrollY(window.scrollY);
}
window.addEventListener("scroll", handleScroll, { passive: true });
// Cleanup: removes the exact same listener function reference
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []); // Empty dependencies: mounts once, cleans up on unmount
return (
<aside style={{ position: "fixed", bottom: 10, right: 10, background: "#eee", padding: "4px 8px" }}>
Scroll position: {Math.round(scrollY)}px
</aside>
);
}
Por qué esta arquitectura es óptima:#
visibleTasks,totalCountycompletedCountson valores derivados puros calculados durante el render. No existe ningúnuseEffectpara recalcularlos.handleToggleTaskproduce un nuevo array con una nueva copia del objeto modificado ({ ...task, done: !task.done }), preservando intactas las referencias de las tareas no afectadas.- Cada elemento de la lista utiliza
key={task.id}, garantizando que si el arreglo se filtra o se reordena, el estado visual de los checkboxes permanezca ligado a la tarea correcta. WindowScrollIndicatorimplementa una función de limpieza simétrica exacta, garantizando que desmontar el componente elimine el listener del objeto globalwindow.
Práctica independiente: Ciclo RED arrow GREEN arrow REFACTOR#
Trabaja con una lista inicial local de tres tareas:
const initialTasks: Task[] = [
{ id: 1, title: "Read architecture docs", done: false },
{ id: 2, title: "Write domain tests", done: false },
{ id: 3, title: "Review pull requests", done: true },
];
Fase 1: RED (Predicciones y verificación de invariantes)#
Antes de ejecutar el código en el navegador, completa la siguiente matriz de predicción en tus notas:
| Escenario | Entrada en buscador (query) | Lista visibleTasks esperada (IDs) | Invariante de memoria |
|---|---|---|---|
| A | "" (cadena vacía) | [1, 2, 3] (Todas las tareas) | Longitud = 3. |
| B | " wri " (espacios y mayúsculas) | [2] ("Write domain tests") | El filtrado debe aplicar trim() y toLowerCase(). |
| C | "missing" | [] (Arreglo vacío) | Longitud = 0. La UI debe mostrar mensaje de "No tasks match". |
| D | Toggle completado en ID 2 | Tarea 2 pasa a done: true | Object.is(tasks[0], originalTasks[0]) === true (inmutabilidad referencial de elementos no tocados). |
Fase 2: GREEN (Implementación mínima)#
- Implementa el componente con estado mínimo para
tasksyquery. - Deriva
visibleTaskssíncronamente y comprueba que las 4 predicciones se cumplan sin ningún render extra.
Fase 3: REFACTOR (Suscripción y verificación de cleanup)#
- Registra un evento de teclado (
window.addEventListener("keydown", ...)) que limpie el campo de búsqueda cuando el usuario presione la teclaEscape. - Agrega un
console.log("Cleanup executed")dentro de la función retornada por el efecto y desmonta el componente condicionalmente en tu aplicación de prueba. Verifica en la consola de DevTools que el cleanup se ejecute de forma inmediata.
Registra tus predicciones y observaciones antes de consultar la guía de soluciones de frontend.
Preguntas de comprensión profunda y diagnóstico socrático#
Reflexiona sobre estas cinco preguntas de razonamiento técnico antes de avanzar:
Pregunta 1: La falacia de sincronizar estado con useEffect#
Un desarrollador junior escribe el siguiente código:
const [tasks, setTasks] = useState<Task[]>([]);
const [query, setQuery] = useState("");
const [filtered, setFiltered] = useState<Task[]>([]);
useEffect(() => {
setFiltered(tasks.filter(t => t.title.includes(query)));
}, [tasks, query]);
- Traza el número exacto de renders que se producen cuando el usuario teclea un caracter.
- ¿Qué se dibuja en la pantalla durante el intervalo entre el primer y el segundo render?
- ¿Qué sucede si una tercera función actualiza
taskspero olvida invocar la lógica del filtro? - Explica cómo refactorizar este componente a una sola línea de código sin ningún
useStateniuseEffectadicional.
Pregunta 2: Inmutabilidad referencial y el algoritmo Object.is#
Observa el siguiente manejador de eventos:
function handleAdd(newTitle: string) {
tasks.push({ id: Date.now(), title: newTitle, done: false });
setTasks(tasks);
}
- Cuando el usuario pulsa el botón para agregar una tarea, ¿por qué la lista en la pantalla no se actualiza a pesar de que se invocó
setTasks(tasks)? - Explica el rol de
Object.is(prev, next)en el motor de reconciliación de React. - ¿Cómo se reescribe esta función utilizando el actualizador funcional y el operador spread para garantizar la reactividad?
Pregunta 3: Instantáneas de render y temporizadores asíncronos#
Analiza este componente:
function DelayedLogger() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setTimeout(() => {
alert("Count is: " + count);
}, 3000);
}
return <button onClick={handleClick}>Increment and Alert</button>;
}
- Si el usuario hace clic tres veces consecutivas rápidamente (con 500 ms de intervalo entre cada clic), ¿qué tres números mostrarán las sucesivas alertas tres segundos después?
- Explica por qué la función dentro de
setTimeoutmuestra los valores viejos en lugar del valor final3. - ¿Cómo se relaciona este fenómeno con las clausuras léxicas (closures) de JavaScript y el concepto de Render as a Snapshot?
Pregunta 4: React 18 Strict Mode y fugas de memoria silenciosas#
- ¿Por qué en modo de desarrollo con
<React.StrictMode>el cuerpo de losuseEffectcon arreglo[]se ejecuta dos veces consecutivas al cargar la página? - Si un componente se suscribe a un WebSocket dentro de un efecto pero no devuelve una función de limpieza que cierre el socket (
socket.close()), ¿cuántas conexiones abiertas quedarán activas en el servidor de desarrollo tras cargar la página? - ¿Por qué este comportamiento de Strict Mode no es un "bug de React", sino una herramienta de diagnóstico para detectar código no preparado para el futuro?
Pregunta 5: La propiedad key en listas dinámicas#
- Un desarrollador renderiza una lista de tareas usando el índice como clave:
{tasks.map((task, index) => (
<TaskItem key={index} task={task} />
))}
Cada TaskItem tiene un estado interno local const [notes, setNotes] = useState("").
- Si el usuario elimina la primera tarea de la lista (índice 0), ¿qué le ocurre al estado local
notesde los elementos restantes en la pantalla? - ¿Por qué el algoritmo de reconciliación asocia el estado del elemento borrado con la tarea que ahora ocupa el índice 0?
- ¿Cuál es la regla de oro para seleccionar una
keyválida y cuándo es el único escenario donde usar el índice es seguro?
Code smell#
Síntoma: Estado duplicado para datos computables (Derived State Storage Smell)#
El componente almacena items, visibleItems, selectedItemCount y isAllSelected en cuatro variables de estado independientes con useState.
- Evidencia observable: Múltiples bloques de
useEffectencadenados para sincronizar los estados secundarios; interfaces que muestran datos contradictorios momentáneamente (por ejemplo, el mensaje "3 seleccionados" cuando solo hay 2 casillas marcadas); lentitud y parpadeo de pantalla. - Hipótesis del problema: Los desarrolladores conciben el estado de React como una base de datos relacional tradicional donde cada columna debe tener una tabla, en lugar de identificar el conjunto mínimo irreductible de verdades.
- Consecuencia concreta: Explosión combinatoria de estados intermedios imposibles de probar y depurar.
- Tratamiento mínimo justificado: Eliminar
visibleItems,selectedItemCounteisAllSelecteddel estado. Dejar únicamenteitemsy la consulta o selección activa como estado; calcular todos los demás valores como constantes locales durante el render.
Criterio de salida#
Puedes considerar superada esta lección cuando:
- Puedes justificar formalmente por qué cada dato en tu componente es una prop, un estado o un cálculo derivado puro.
- No utilizas
useEffectpara calcular filtros, conteos o transformaciones entre dos variables de estado existentes. - Aplicas inmutabilidad estricta en todas las actualizaciones de estado, produciendo nuevas referencias con spread o métodos de array no mutables (
map,filter). - Utilizas el actualizador funcional
setState(prev => ...)siempre que el siguiente valor dependa del estado anterior. - Cada
useEffectque conecta con un recurso externo devuelve una función de limpieza simétrica y rigurosa. - Asignas identificadores estables y únicos a la propiedad
keyen listas dinámicas, evitando el uso de índices de array.
Lecturas oficiales#
- React: Aprender React: Fundamentos canónicos de componentes, props y estado.
- React: Elegir la estructura del estado: Principios para evitar redundancia y estados contradictorios.
- React: You Might Not Need an Effect: Guía obligatoria sobre cómo calcular datos durante el render en lugar de usar efectos.
- React: Referencia oficial de useEffect: Ciclo de vida, arreglo de dependencias y limpieza.
- React: Referencia oficial de useState: Instantáneas de render y actualizador funcional.
- Glosario técnico del curso: Definiciones de referencia rápida para términos de React y arquitectura de interfaces.
Siguiente paso#
Continúa con 12 · Integración de extremo a extremo: conectarás estos componentes con endpoints HTTP reales, gestionando el ciclo completo de peticiones asíncronas, estados de carga, fallos de red y condiciones de carrera mediante AbortController.