Trabajando en React es posible que, bajo los entresijos de algún proyecto, te hayas encontrado componentes que reciben alguna prop como esta:
<SomeInputField setValue={setValue} /> // 🤔
Si tienes suficiente experiencia en React puede que evites esto sin siquiera pensarlo. No obstante, qué bueno es poder razonarlo para ayudar al equipo a estar en la misma página.
Si por el contrario llevas poco tiempo o ya has cogido el vicio, puede que estés pensando que no es tan malo.
Bueno, yo también he estado ahí. Let's talk about this.
Cuál es el problema
Tal vez sea verdad que hay casos pequeños donde no supone un problema aparente, pero por favor, echa un vistazo a este otro ejemplo:
<SomeComponent
setForm={setForm}
setCart={setCart}
setUser={setUser}
setMeFree={setMeFree}
/>
Ahora considera estas preguntas básicas:
¿Cuándo cambia cada estado?
¿Qué o quién los hace cambiar?
¿Cómo cambian exactamente?
¿Dónde está el código que lo hace?
Como puedes observar, el código no nos da información suficiente para responder a ninguna de las preguntas. Básicamente, no sabemos nada de estos estados.
Pero solo tengo que entrar al código de
SomeComponent
, ¿no? — Alguien dirá
Pues vamos adentro:
// En algún lugar de SomeComponent.jsx
<YetAnotherComponent
setForm={setForm}
setCart={setCart}
setUser={setUser}
setMeFree={setMeFree}
/>
¡Vaya! Empieza la frustración. Un bucle al más puro estilo Inception, y seguimos sin respuestas.
Es más, los setter pasarán a otros componentes hermanos y seguirán enviándose mediante props a más y más hijos en la jerarquía, multiplicando la cantidad de archivos de código a revisar.
¿Ves cuál es el problema? El estado es un elemento clave de la aplicación ¡y así es difícil saber lo que hace!
Cómo escapar de esto
Volvamos a las preguntas de antes: ¿cuándo cambia el estado? ¿cómo? ¿dónde?... ¿No sería genial poder responderlas sin tener que ir a otros archivos?
Atentos, porque hay una forma y es estándar. Y si existe un estándar es porque presenta ventajas frente a otros métodos.
Seguir estándares suele ahorrar muchos problemas.
Fíjate en el código nativo
Definitivamente no hay ninguna prop estándar con nombre de setter.
Un
input
no recibesetValue
¿Lo has visto en componentes nativos? ¿kits de UI? ¿librerías populares? Nada.
Lo que sí habrás visto es esto:
<input onChange={...} /> // 👀
Así es, un input nativo no ofrece una prop setValue
, sino una prop onChange
.
Esta prop de evento espera una función, donde típicamente se usa el contenido del argumento event
para hacer una actualización de estado:
<input onChange={
event => setValue(event.target.value)
} />
Piensa en la información tan valiosa que tenemos aquí:
El nombre de la prop ya te da una idea de cuándo sucede
Es fácil imaginar la acción responsable del cambio
Se ve exactamente cómo cambia el estado
Todo el código relevante está a la vista
¿No es genial?
La clave: división de responsabilidad
Vamos a señalar el motivo por el que esto funciona tan bien.
Cuando hay división de responsabilidad, cada pieza del sistema tiene un propósito bien definido y delimitado. A menudo se puede entender por sí misma sin acudir al resto.
Esto es importante en esta profesión, porque cuanto menos tiempo tardes en entender, antes podrás empezar a tocar.
Siguiendo con la división de responsabilidad: el input
nativo no quiere saber nada de cómo se cambia tu estado, eso es problema de tu componente padre.
El input
solo te habla de lo que él controla gracias a la event prop onChange
:
Cuándo ocurre un cambio
Los detalles de ese cambio
Así que, en vez de enviar un state setter como prop, ha llegado el momento de imitar el patrón nativo: las event props.
Hazlo en tus componentes
El estado y sus valores son responsabilidad única del componente que declara el useState
. Por ejemplo, si tuvieras un componente que gestiona una lista:
// List.jsx
const [list, setList] = useState([])
Digamos que vamos a tener un hijo Item
con la capacidad de borrarse del listado.
El padre List
es responsable de hacer cada setList
en su propio código. No delegará esa responsabilidad en Item
:
// También en List.jsx
const handleRemoveItem = id => setList(list.filter(...))
return <Item onRemove={handleRemoveItem} />
Item
no debe saber nada sobre la lista completa del padre ni cómo ha de cambiar, pero hay dos cosas que sí son su responsabilidad:
Cuándo se ha de borrar: porque tendrá un botón para hacerlo
Cuál se ha de borrar: porque sabrá su propio id
Como ya hemos visto, en lugar de recibir setList
directamente vamos a exponer una event prop que permita al padre estar al tanto de estas cosas:
function Item ({ onRemove }) {
const myId = 27
return <button onClick={() => onRemove(myId)}>❌</button>
}
Pero Item
nunca decidirá qué hacer con un estado que no es suyo, y de hecho desde su código no tendrá acceso explícito a ningún setter ajeno.
Ya está, cada componente tiene su responsabilidad y ninguno invade la del otro. Vamos a mirar el código completo de List
una última vez:
// ✅
function List () {
const [list, setList] = useState([])
const handleRemoveItem = id => setList(list.filter(...))
return <Item onRemove={handleRemoveItem} />
}
Todas las preguntas sobre el estado tienen respuesta sin salir del archivo:
¿Cuándo cambia el estado? — Al suceder la acción "borrar"
¿Qué o quién lo hace cambiar? — El usuario que interactúa con
Item
¿Cómo cambia exactamente? — Con un
.filter()
, está a la vista¿Dónde está el código que lo hace? — En el mismo archivo
Pero, solo por saborear una vez más la diferencia:
// ❌
function List () {
const [list, setList] = useState([])
return <Item setList={setList} />
}
En contraste, aquí ya no tendríamos ni idea de cuándo, dónde, cómo o qué se está haciendo con el estado.
Conclusión
❌ Pasar state setters como props oscurece la lógica de estado fragmentándola en diferentes archivos, lo que resulta en dificultades para entenderla.
const [value, setValue] = useState('')
<SomeInputField setValue={setValue} /> // ???
✅ El estándar nativo de las event props ayuda a delimitar correctamente la responsabilidad del estado y sus cambios junto al código donde se declara el estado.
const [value, setValue] = useState('')
<AwesomeInputField onChange={next => setValue(next)} />