Hooks esenciales en React: useState para estado local y useEffect para sincronización

Capítulo 8

Tiempo estimado de lectura: 7 minutos

+ Ejercicio

useState: estado local con actualizaciones inmutables

useState te permite guardar y actualizar estado dentro de un componente funcional. La idea clave es que el estado se trata como inmutable: en lugar de modificar un objeto/array existente, creas uno nuevo. Esto ayuda a que React detecte cambios y renderice de forma predecible.

Firma y reglas prácticas

  • const [valor, setValor] = useState(inicial)
  • setValor(nuevoValor) reemplaza el estado.
  • setValor(prev => siguiente) usa una función de actualización (recomendada cuando el siguiente estado depende del anterior).
  • No llames hooks dentro de condiciones o bucles; deben ejecutarse siempre en el mismo orden.

Paso a paso: contador con actualización segura

Cuando el nuevo valor depende del anterior (por ejemplo, incrementar), usa la forma funcional para evitar problemas con actualizaciones agrupadas (batching) o eventos rápidos.

import { useState } from "react";export function Counter() {  const [count, setCount] = useState(0);  function inc() {    setCount(prev => prev + 1);  }  function dec() {    setCount(prev => prev - 1);  }  return (    <div>      <p>Count: {count}</p>      <button onClick={dec}>-</button>      <button onClick={inc}>+</button>    </div>  );}

Actualizaciones inmutables con objetos

Si tu estado es un objeto, evita mutarlo directamente. Crea una copia con ... y cambia solo lo necesario.

const [profile, setProfile] = useState({ name: "Ada", email: "ada@dev.com" });function onNameChange(e) {  const nextName = e.target.value;  setProfile(prev => ({ ...prev, name: nextName }));}

Actualizaciones inmutables con arrays

Para agregar, eliminar o editar elementos, usa métodos que devuelvan un nuevo array (map, filter, spread).

const [todos, setTodos] = useState([{ id: 1, text: "Leer" }]);function addTodo(text) {  setTodos(prev => [...prev, { id: Date.now(), text }]);}function removeTodo(id) {  setTodos(prev => prev.filter(t => t.id !== id));}function renameTodo(id, text) {  setTodos(prev => prev.map(t => (t.id === id ? { ...t, text } : t)));}

Error frecuente: mutar y luego “setear” el mismo objeto

Este patrón es problemático porque mantiene la misma referencia y puede causar renders inconsistentes.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

// Evitar: mutación directaconst [settings, setSettings] = useState({ dark: false });function toggle() {  settings.dark = !settings.dark; // muta  setSettings(settings); // misma referencia}

En su lugar:

function toggle() {  setSettings(prev => ({ ...prev, dark: !prev.dark }));}

useEffect: sincronizar con el mundo externo

useEffect sirve para ejecutar efectos secundarios: trabajo que “sale” del render y sincroniza tu componente con algo externo (red, timers, suscripciones, APIs del navegador como localStorage). Un efecto se ejecuta después de que React pinta la UI.

Modelo mental: render vs efecto

  • El render debe ser puro: dado el mismo estado/props, produce el mismo UI.
  • El efecto es para sincronización: “cuando X cambie, haz Y afuera”.
  • Si algo puede calcularse a partir de estado/props, normalmente NO es un efecto (es cálculo derivado).

Sintaxis y dependencias

useEffect(() => {  // efecto  return () => {    // cleanup (opcional)  };}, [dep1, dep2]);
DependenciasCuándo correUso típico
[]Al montar (y cleanup al desmontar)Inicialización, cargar datos una vez, suscribirse
[a]Al montar y cuando a cambieSincronizar con un valor específico
(sin array)En cada renderRaro; suele indicar un problema o falta de dependencias

Cleanup: limpiar timers y suscripciones

Si tu efecto crea algo que debe deshacerse (timer, listener, suscripción), devuelve una función de limpieza. React la ejecuta antes de re-ejecutar el efecto y al desmontar.

import { useEffect, useState } from "react";export function Clock() {  const [now, setNow] = useState(() => new Date());  useEffect(() => {    const id = setInterval(() => setNow(new Date()), 1000);    return () => clearInterval(id);  }, []);  return <p>{now.toLocaleTimeString()}</p>;}

Patrones esenciales con useEffect

1) Cargar datos al montar (fetch) con manejo de estados

Patrón típico: loading, error, data. Además, evita actualizar estado si el componente se desmonta o si llega una respuesta vieja.

import { useEffect, useState } from "react";export function UsersList() {  const [users, setUsers] = useState([]);  const [loading, setLoading] = useState(true);  const [error, setError] = useState(null);  useEffect(() => {    const controller = new AbortController();    async function load() {      try {        setLoading(true);        setError(null);        const res = await fetch("/api/users", { signal: controller.signal });        if (!res.ok) throw new Error("HTTP " + res.status);        const data = await res.json();        setUsers(data);      } catch (e) {        if (e.name !== "AbortError") setError(e);      } finally {        setLoading(false);      }    }    load();    return () => controller.abort();  }, []);  if (loading) return <p>Cargando...</p>;  if (error) return <p>Error: {String(error.message || error)}</p>;  return (    <ul>      {users.map(u => (<li key={u.id}>{u.name}</li>))}    </ul>  );}

2) Reaccionar a cambios específicos

Cuando un valor cambia (por ejemplo, un término de búsqueda), ejecutas un efecto que sincroniza con el exterior. Aquí el array de dependencias es el contrato: el efecto depende de query, así que debe incluirlo.

import { useEffect, useState } from "react";export function SearchUsers() {  const [query, setQuery] = useState("");  const [results, setResults] = useState([]);  useEffect(() => {    if (query.trim() === "") {      setResults([]);      return;    }    const controller = new AbortController();    async function run() {      const res = await fetch(`/api/users?q=${encodeURIComponent(query)}`, {        signal: controller.signal,      });      const data = await res.json();      setResults(data);    }    run().catch(() => {});    return () => controller.abort();  }, [query]);  return (    <div>      <input value={query} onChange={e => setQuery(e.target.value)} />      <ul>{results.map(r => (<li key={r.id}>{r.name}</li>))}</ul>    </div>  );}

3) Sincronizar con localStorage

Un caso común es persistir una preferencia. La lectura inicial puede hacerse en el inicializador de useState (para evitar leer en cada render) y la escritura en un efecto que depende del valor.

import { useEffect, useState } from "react";export function ThemeToggle() {  const [theme, setTheme] = useState(() => {    return localStorage.getItem("theme") || "light";  });  useEffect(() => {    localStorage.setItem("theme", theme);  }, [theme]);  return (    <button onClick={() => setTheme(t => (t === "light" ? "dark" : "light"))}>      Theme: {theme}    </button>  );}

4) Separar efectos por responsabilidad

Si un componente hace varias sincronizaciones (por ejemplo, título del documento y analytics), es mejor tener efectos separados. Esto reduce dependencias innecesarias y hace más fácil razonar sobre el cleanup.

useEffect(() => {  document.title = `Perfil: ${name}`;}, [name]);useEffect(() => {  analytics.track("profile_view", { userId });}, [userId]);

Cómo evitar bucles de render y otros errores frecuentes

Dependencias omitidas

Si usas variables del scope (props/estado/funciones) dentro del efecto, normalmente deben estar en el array de dependencias. Omitirlas puede causar que el efecto use valores “viejos” (stale) y se desincronice.

// Problema: usa "query" pero no está en dependenciasuseEffect(() => {  fetch(`/api?q=${query}`);}, []);

Arreglo:

useEffect(() => {  fetch(`/api?q=${query}`);}, [query]);

Efectos que actualizan estado sin control (bucle)

Si un efecto hace setState y ese estado está en sus dependencias, puedes crear un ciclo infinito. Pregunta: ¿estoy actualizando algo que a su vez dispara el mismo efecto sin una condición?

// Bucle: cada vez que "count" cambia, lo incrementa otra vezuseEffect(() => {  setCount(count + 1);}, [count]);

Soluciones típicas:

  • Si quieres que ocurra una sola vez, usa [].
  • Si depende de un evento externo, mueve la actualización al manejador del evento.
  • Si necesitas derivar un valor, no uses efecto: calcula en render o usa memoización.

Confundir efecto con cálculo derivado

Si un valor se puede obtener a partir de estado/props, no lo guardes en estado con un efecto, porque introduces duplicación y riesgo de desincronización.

// Evitar: estado derivado + efectoconst [fullName, setFullName] = useState("");useEffect(() => {  setFullName(first + " " + last);}, [first, last]);

Mejor: calcular directamente.

const fullName = first + " " + last;

Dependencias que cambian por identidad (objetos/funciones inline)

Si pones un objeto o función creada en cada render dentro de dependencias, el efecto se disparará siempre. En lugar de eso, mueve la creación dentro del efecto o estabiliza referencias (por ejemplo, con useMemo/useCallback en capítulos posteriores si aplica).

// Se recrea en cada render, dispara el efecto siempreconst options = { headers: { "X-App": "demo" } };useEffect(() => {  fetch("/api", options);}, [options]);

Alternativa simple: crear dentro del efecto.

useEffect(() => {  const options = { headers: { "X-App": "demo" } };  fetch("/api", options);}, []);

Ahora responde el ejercicio sobre el contenido:

¿Qué práctica ayuda a evitar un bucle infinito cuando un useEffect actualiza estado relacionado con sus dependencias?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Si un efecto actualiza un estado que está en sus dependencias sin control, puede dispararse una y otra vez. Para evitarlo, agrega una condición, usa [] si debe correr una vez, o mueve la actualización al evento adecuado.

Siguiente capítulo

Flujo de datos en React: fuente de verdad, derivaciones y elevación de estado

Arrow Right Icon
Portada de libro electrónico gratuitaReact para principiantes: mentalidad de componentes y manejo de estado
67%

React para principiantes: mentalidad de componentes y manejo de estado

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.