Saltar al contenido

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íficas
const updateName = (newName) => {
setUser(prev => ({
...prev,
name: newName
}));
};

Estado con arrays:

const [tasks, setTasks] = useState([]);
// Agregar elemento
const addTask = (newTask) => {
setTasks(prev => [...prev, newTask]);
};
// Eliminar elemento
const 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:

  1. Montaje (Mounting): El componente se crea e inserta en el DOM.
  2. Actualización (Updating): El componente se actualiza cuando cambian props o estado.
  3. 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 datos
function 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 storage
function 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 personalizados
function 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.