Clase 30 - React - Estado interno
#react
#state
#hooks
⚛️ Estado Interno en React
El estado interno (state) es un objeto que representa la información que puede cambiar a lo largo del tiempo en un componente React. Cuando el estado cambia, React vuelve a renderizar el componente, actualizando la interfaz de usuario.
Características del estado:
- Es privado y local para cada componente.
- Es inmutable - no se debe modificar directamente.
- Los cambios de estado desencadenan un re-renderizado.
- Se inicializa en la primera renderización.
Cómo definir el estado:
En componentes funcionales (recomendado):
import { useState } from 'react';
function Counter() { const [count, setCount] = useState(0);
return ( <div> <p>Valor actual: {count}</p> <button onClick={() => setCount(count + 1)}>Incrementar</button> <button onClick={() => setCount(prev => prev - 1)}>Decrementar</button> </div> );}En componentes de clase:
class Counter extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; }
increment = () => { this.setState({ count: this.state.count + 1 }); }
render() { return ( <div> <p>Valor actual: {this.state.count}</p> <button onClick={this.increment}>Incrementar</button> </div> ); }}Trabajando con diferentes tipos de estado:
Estado primitivo:
const [name, setName] = useState('');const [age, setAge] = useState(0);const [active, setActive] = useState(true);Estado con objetos:
const [user, setUser] = useState({ name: '', email: '', age: 0});
// Actualizar propiedades específicasconst updateName = (newName) => { setUser(prev => ({ ...prev, name: newName }));};Estado con arrays:
const [tasks, setTasks] = useState([]);
// Agregar elementoconst addTask = (newTask) => { setTasks(prev => [...prev, newTask]);};
// Eliminar elementoconst deleteTask = (index) => { setTasks(prev => prev.filter((_, i) => i !== index));};✨ Buenas prácticas para el estado:
- Nunca mutes el estado directamente: Siempre usa los setters.
- Usa la función de actualización:
setState(prev => prev + 1)para actualizaciones basadas en el estado anterior. - Mantén el estado lo más simple posible: Divide estados complejos en varios estados sencillos.
- Considera si algo realmente necesita estar en el estado: Puede ser una prop o un valor calculado.
- Coloca el estado en el nivel más bajo posible: Evita “prop drilling” innecesario.
🛞 Ciclo de Vida de un Componente
Los componentes en React tienen tres fases principales: montaje, actualización y desmontaje.
Fases del ciclo de vida:
- Montaje (Mounting): El componente se crea e inserta en el DOM.
- Actualización (Updating): El componente se actualiza cuando cambian props o estado.
- Desmontaje (Unmounting): El componente se elimina del DOM.
En componentes de clase:
class MyComponent extends React.Component { constructor(props) { super(props); this.state = { data: null }; console.log('1. Constructor'); }
componentDidMount() { console.log('2. ComponentDidMount - Después del primer render'); // Ideal para: fetch de datos, suscripciones, timers fetch('/api/datos') .then(res => res.json()) .then(data => this.setState({ data })); }
componentDidUpdate(prevProps, prevState) { console.log('3. ComponentDidUpdate - Después de actualizaciones'); // Ideal para: reaccionar a cambios en props o estado if (prevProps.id !== this.props.id) { this.fetchNewData(); } }
componentWillUnmount() { console.log('4. ComponentWillUnmount - Antes de desmontar'); // Ideal para: limpiar timers, cancelar requests, desuscribirse clearInterval(this.interval); }
render() { console.log('Render'); return <div>{this.state.data ? 'Datos cargados' : 'Cargando...'}</div>; }}En componentes funcionales con useEffect:
import { useState, useEffect } from 'react';
function CompleteExample() { const [count, setCount] = useState(0); const [data, setData] = useState(null);
// Equivalente a componentDidMount y componentDidUpdate useEffect(() => { console.log('Componente montado o count actualizado'); document.title = `Count: ${count}`; }, [count]);
// Equivalente a componentDidMount (solo se ejecuta una vez) useEffect(() => { console.log('Solo al montar'); fetch('/api/datos') .then(res => res.json()) .then(data => setData(data)); }, []);
// Con limpieza (equivalente a componentWillUnmount) useEffect(() => { const interval = setInterval(() => { setCount(c => c + 1); }, 1000);
return () => { console.log('Limpiando interval'); clearInterval(interval); }; }, []);
return ( <div> <p>Count: {count}</p> <p>Datos: {data ? 'Cargados' : 'Cargando...'}</p> </div> );}🪝 Hooks Fundamentales
useState
El hook más básico para manejar estado local:
import { useState } from 'react';
function UserForm() { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [error, setError] = useState({});
const validateForm = () => { const newErrors = {}; if (!name.trim()) newErrors.name = 'El nombre es required'; if (!email.includes('@')) newErrors.email = 'Email inválido';
setError(newErrors); return Object.keys(newErrors).length === 0; };
const handleSubmit = (e) => { e.preventDefault(); if (validateForm()) { console.log('Formulario válido', { name, email }); } };
return ( <form onSubmit={handleSubmit}> <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="Nombre" /> {errores.name && <span>{errores.name}</span>}
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" /> {errores.email && <span>{errores.email}</span>}
<button type="submit">Enviar</button> </form> );}useEffect
Para manejar efectos secundarios:
import { useState, useEffect } from 'react';
function UseEffectExample() { const [user, serUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
// Fetch inicial useEffect(() => { const fetchUser = async () => { try { setLoading(true); const response = await fetch('/api/usuario'); const data = await response.json(); serUser(data); } catch (err) { setError(err.message); } finally { setLoading(false); } };
fetchUser(); }, []);
// Suscripción a eventos useEffect(() => { const handleResize = () => { console.log('Ventana redimensionada'); };
window.addEventListener('resize', handleResize);
return () => { window.removeEventListener('resize', handleResize); }; }, []);
// Efecto condicional useEffect(() => { if (user) { console.log('Usuario cargado:', user.nombre); } }, [user]);
if (loading) return <div>Cargando...</div>; if (error) return <div>Error: {error}</div>;
return <div>Hola, {user?.nombre}</div>;}🔨 Hooks Personalizados
Los hooks personalizados permiten reutilizar lógica entre componentes:
// Hook personalizado para fetch de datosfunction useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchData = async () => { try { setLoading(true); const response = await fetch(url); if (!response.ok) throw new Error('Request failed'); const result = await response.json(); setData(result); } catch (err) { setError(err.message); } finally { setLoading(false); } };
fetchData(); }, [url]);
return { data, loading, error };}
// Hook para local storagefunction useLocalStorage(key, initialValue) { const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { return initialValue; } });
const setValue = (value) => { try { setStoredValue(value); window.localStorage.setItem(key, JSON.stringify(value)); } catch (error) { console.error('Error saving to localStorage:', error); } };
return [storedValue, setValue];}
// Uso de hooks personalizadosfunction ComponentWithCustomHooks() { const { data: users, loading, error } = useFetch('/api/users'); const [favorites, setFavorites] = useLocalStorage('favorites', []);
if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error}</div>;
return ( <div> <h2>Users</h2> {users?.map(user => ( <div key={user.id}> <span>{user.name}</span> <button onClick={() => setFavorites([...favorites, user.id])}> Add to favorites </button> </div> ))} </div> )}✨ Buenas Prácticas y Patrones
Manejo de errores
import { useState, useEffect } from 'react';
function ComponentWithErrorHandling() { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null);
const fetchData = async () => { try { setLoading(true); setError(null); const response = await fetch('/api/data'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); setData(result); } catch (err) { setError(err.message); } finally { setLoading(false); } };
useEffect(() => { fetchData(); }, []);
if (loading) return <div>Cargando...</div>; if (error) return <div>Error: {error}</div>;
return <div>{/* Renderizar data */}</div>;}🎯 Ejercicios Prácticos
Ejercicio 1: Lista de Tareas
Crea un componente que permita:
- Agregar nuevas tareas.
- Marcar tareas como completadas.
- Eliminar tareas.
- Filtrar tareas (todas, completadas, pendientes)
Ejercicio 2: Formulario de Contacto
Implementa un formulario con:
- Validación en tiempo real.
- Manejo de errores.
- Estados de carga.
- Envío de datos.
Ejercicio 3: Hook Personalizado
Crea un hook useTimer que:
- Maneje un temporizador.
- Permita pausar/reanudar.
- Tenga función de reset.
- Ejecute callback cuando llegue a cero.