A medida que tu aplicación crece, es de ayuda ser más intencional sobre cómo se organiza tu estado y cómo los datos fluyen entre tus componentes. El estado redundante o duplicado es una fuente común de errores. En este capítulo, aprenderás cómo estructurar bien tu estado, cómo mantener la lógica de actualización de estado y cómo compartir el estado entre componentes distantes.
En este capítulo
- Cómo pensar en los cambios de la interfaz de usuario como cambios de estado
- Cómo estructurar bien el estado
- Cómo «levantar el estado» para compartirlo entre componentes
- Cómo controlar si el estado se preserva o se reinicia
- Cómo consolidar una lógica de estado compleja en una función
- Cómo pasar la información sin »prop drilling» (perforación de prop)
- Cómo escalar la administración del estado a medida que crece tu aplicación
Reacción a la entrada de datos con el estado
Con React, no modificarás la interfaz de usuario directamente desde el código. Por ejemplo, no escribirás comandos como «deshabilitar el botón», «habilitar el botón», «mostrar el mensaje de éxito», etc. En su lugar, describirás la interfaz de usuario que deseas ver para los diferentes estados visuales de tu componente («estado inicial», «estado de escritura», «estado de éxito»), y luego activar los cambios de estado en respuesta a la entrada del usuario. Esto es similar a cómo los diseñadores piensan sobre la interfaz de usuario.
Aquí tenemos un formulario de preguntas construido con React. Fíjate en cómo utiliza la variable de estado status
para determinar si se activa o desactiva el botón de envío, y si se muestra el mensaje de éxito en su lugar.
import { useState } from 'react'; export default function Form() { const [answer, setAnswer] = useState(''); const [error, setError] = useState(null); const [status, setStatus] = useState('typing'); if (status === 'success') { return <h1>That's right!</h1> } async function handleSubmit(e) { e.preventDefault(); setStatus('submitting'); try { await submitForm(answer); setStatus('success'); } catch (err) { setStatus('typing'); setError(err); } } function handleTextareaChange(e) { setAnswer(e.target.value); } return ( <> <h2>Cuestionario sobre ciudades</h2> <p> ¿En qué ciudad hay un cartel publicitario que convierte el aire en agua potable? </p> <form onSubmit={handleSubmit}> <textarea value={answer} onChange={handleTextareaChange} disabled={status === 'submitting'} /> <br /> <button disabled={ answer.length === 0 || status === 'submitting' }> Enviar </button> {error !== null && <p className="Error"> {error.message} </p> } </form> </> ); } function submitForm(answer) { // Simulando una respuesta que viene de la red return new Promise((resolve, reject) => { setTimeout(() => { let shouldError = answer.toLowerCase() !== 'lima' if (shouldError) { reject(new Error('Good guess but a wrong answer. Try again!')); } else { resolve(); } }, 1500); }); }
¿Listo para aprender este tema?
Lee Reaccionar a la entrada de datos con el estado para aprender a enfocar las interacciones con una mentalidad basada en el estado.
Lee másElegir la estructura del estado
Estructurar bien el estado puede marcar la diferencia entre un componente que sea agradable de modificar y depurar, y uno que sea una fuente constante de errores. El principio más importante es que el estado no debe contener información redundante o duplicada. Si hay algún estado innecesario, es fácil olvidarse de actualizarlo, ¡e introducir errores!
Por ejemplo, este formulario tiene una variable de estado redundante fullName
:
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [fullName, setFullName] = useState(''); function handleFirstNameChange(e) { setFirstName(e.target.value); setFullName(e.target.value + ' ' + lastName); } function handleLastNameChange(e) { setLastName(e.target.value); setFullName(firstName + ' ' + e.target.value); } return ( <> <h2>Regístrate</h2> <label> Nombre:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Apellido:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Tu ticket será emitido a: <b>{fullName}</b> </p> </> ); }
Puedes eliminarlo y simplificar el código calculando fullName
mientras el componente se está renderizando:
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const fullName = firstName + ' ' + lastName; function handleFirstNameChange(e) { setFirstName(e.target.value); } function handleLastNameChange(e) { setLastName(e.target.value); } return ( <> <h2>Regístrate</h2> <label> Nombre:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Apellido:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Tu ticket será emitido a: <b>{fullName}</b> </p> </> ); }
Esto puede parecer un pequeño cambio, pero muchos errores en las aplicaciones React se solucionan de esta manera.
¿Listo para aprender este tema?
Lee Elegir la estructura del estado para aprender a diseñar la forma del estado para evitar errores.
Lee másCompartir el estado entre componentes
A veces, quieres que el estado de dos componentes cambie a la vez siempre. Para hacerlo, quita el estado de ambos, muévelo a su padre común más cercano, y luego pásalo a ellos vía props. Esto se conoce como «levantar el estado», y es una de las cosas más comunes que harás escribiendo código React.
En este ejemplo, sólo un panel debe estar activo a la vez. Para conseguirlo, en lugar de mantener el estado activo dentro de cada panel individual, el componente padre mantiene el estado y especifica las props para sus hijos.
import { useState } from 'react'; export default function Accordion() { const [activeIndex, setActiveIndex] = useState(0); return ( <> <h2>Alma Ata, Kazajistán</h2> <Panel title="Acerca de" isActive={activeIndex === 0} onShow={() => setActiveIndex(0)} > Con una población de unos 2 millones de habitantes, Alma Ata es la mayor ciudad de Kazajistán. De 1929 a 1997 fue su capital. </Panel> <Panel title="Etimología" isActive={activeIndex === 1} onShow={() => setActiveIndex(1)} > El nombre proviene de <span lang="kk-KZ">алма</span>, la palabra en kazajo para "manzana", y suele traducirse como "lleno de manzanas". De hecho, se cree que la región que rodea a Alma Ata es el hogar ancestral de la manzana, y el <i lang="la">Malus Silvestris</i> se considera un candidato probable para el ancestro de la manzana doméstica moderna. </Panel> </> ); } function Panel({ title, children, isActive, onShow }) { return ( <section className="panel"> <h3>{title}</h3> {isActive ? ( <p>{children}</p> ) : ( <button onClick={onShow}> Mostrar </button> )} </section> ); }
¿Listo para aprender este tema?
Lee Compartir estado entre componentes para aprender a levantar el estado y mantener los componentes sincronizados.
Lee másPreservar y reiniciar el estado
Cuando se vuelve a renderizar un componente, React necesita decidir qué partes del árbol se mantienen (y se actualizan), y qué partes se descartan o se vuelven a crear desde cero. En la mayoría de los casos, el comportamiento automático de React funciona bastante bien. Por defecto, React conserva las partes del árbol que «coinciden» con el árbol de componentes previamente renderizado.
Sin embargo, a veces esto no es lo que quieres. Por ejemplo, en esta aplicación, si se escribe un mensaje y luego se cambia de destinatario no se reinicia la entrada. Esto puede hacer que el usuario envíe accidentalmente un mensaje a la persona equivocada:
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat contact={to} /> </div> ) } const contacts = [ { name: 'Taylor', email: 'taylor@mail.com' }, { name: 'Alice', email: 'alice@mail.com' }, { name: 'Bob', email: 'bob@mail.com' } ];
React permite anular el comportamiento por defecto, y forzar a un componente a reiniciar su estado pasándole una key
diferente, como <Chat key={email} />
. Esto le dice a React que si el destinatario es diferente, debe ser considerado como un componente Chat
diferente que necesita ser recreado desde cero con los nuevos datos (y entradas de UI). Ahora al cambiar de destinatario siempre se reinicia el campo de entrada, aunque se renderice el mismo componente.
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat key={to.email} contact={to} /> </div> ) } const contacts = [ { name: 'Taylor', email: 'taylor@mail.com' }, { name: 'Alice', email: 'alice@mail.com' }, { name: 'Bob', email: 'bob@mail.com' } ];
¿Listo para aprender este tema?
Lee Preservar y reiniciar el estado para aprender la vida del estado y cómo controlarla.
Lee másExtracción de la lógica de estado en un reductor
Los componentes con muchas actualizaciones de estado repartidas entre muchos controladores de eventos pueden resultar abrumadores. Para estos casos, puedes consolidar toda la lógica de actualización de estado fuera de tu componente en una sola función, llamada «reductor». Tus controladores de eventos se vuelven concisos porque sólo especifican las «acciones» del usuario. Al final del archivo, la función reductora especifica cómo debe actualizarse el estado en respuesta a cada acción.
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer( tasksReducer, initialTasks ); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId }); } return ( <> <h1>Itinerario de Praga</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'changed': { return tasks.map(t => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter(t => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Visitar el Museo Kafka', done: true }, { id: 1, text: 'Ver espectáculo de títeres', done: false }, { id: 2, text: 'Foto del muro de Lennon', done: false } ];
¿Listo para aprender este tema?
Lee Extraer la lógica de estado en un reductor para aprender a consolidar la lógica en la función reductora.
Lee másPasar datos en profundidad con el contexto
Normalmente, se pasa información de un componente padre a un componente hijo a través de props. Pero pasar props puede ser un inconveniente si necesitas pasar alguna prop a través de muchos componentes, o si muchos componentes necesitan la misma información. Context permite que el componente padre haga que cierta información esté disponible para cualquier componente en el árbol por debajo de él -sin importar lo profundo que sea- sin pasarla explícitamente a través de props.
Aquí, el componente Heading
determina su nivel de encabezamiento «preguntando» a la Section
más cercana por su nivel. Cada Section
rastrea su propio nivel preguntando a la Section
padre y añadiéndole uno. Cada Section
proporciona información a todos los componentes que se encuentran por debajo de ella sin necesidad de pasar props—lo hace a través del contexto.
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading>Título</Heading> <Section> <Heading>Encabezado</Heading> <Heading>Encabezado</Heading> <Heading>Encabezado</Heading> <Section> <Heading>Sub-encabezado</Heading> <Heading>Sub-encabezado</Heading> <Heading>Sub-encabezado</Heading> <Section> <Heading>Sub-sub-encabezado</Heading> <Heading>Sub-sub-encabezado</Heading> <Heading>Sub-sub-encabezado</Heading> </Section> </Section> </Section> </Section> ); }
¿Listo para aprender este tema?
Lee Pasar datos en profundidad con el contexto para aprender a usar el contexto como una alternativa a pasar props.
Lee másEscalado con reductor y contexto
Los reductores permiten consolidar la lógica de actualización del estado de un componente. El contexto te permite pasar información en profundidad a otros componentes. Puedes combinar reductores y contexto para gestionar el estado de una pantalla compleja.
Con este enfoque, un componente principal con estado complejo lo gestiona con un reductor. Otros componentes en cualquier parte del árbol pueden leer su estado a través del contexto. También pueden enviar acciones para actualizar ese estado.
import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksProvider } from './TasksContext.js'; export default function TaskApp() { return ( <TasksProvider> <h1>Día libre en Kioto</h1> <AddTask /> <TaskList /> </TasksProvider> ); }
¿Listo para aprender este tema?
Lee Ampliación con Reductor y Contexto para aprender cómo se escala la gestión de estados en una aplicación en crecimiento.
Lee más¿Qué es lo siguiente?
Dirígete a Reaccionar a la entrada de datos con el estado para empezar a leer este capítulo página a página.
O, si ya estás familiarizado con estos temas, ¿por qué no lees sobre Escotillas de escape?