¿Y si paso un state setter como prop?

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.

😵‍💫
No quieres revisar 15 archivos para entender un estado

¿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 recibe setValue

¿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.

👉
Hay división de responsabilidad

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

Si imitamos el modelo nativo recibimos event props, no state setters

❌ 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)} />