Muy buenas, les saluda Miguel y en esta ocasión les traigo este nuevo tutorial.
Índice
Una de las reglas más fundamentales en la arquitectura de componentes
En este artículo, discutiré una de las reglas más fundamentales en la arquitectura de componentes. Esta regla fundamental a menudo se viola, lo que lleva a problemas que son muy difíciles (si no imposibles) de resolver.
Aunque proporcionaré ejemplos y hablaré en términos de React, todo es aplicable a otras bibliotecas / marcos de interfaz de usuario, y probablemente a cualquier programa basado en componentes, donde los componentes pueden tener comunicaciones bidireccionales.
Comencemos por definir los términos controlado y no controlado. En una de las publicaciones de blog oficiales de React, lo definen de la siguiente manera:
Los datos que se pasan como accesorios se pueden considerar controlados (porque el componente principal controla esos datos). Los datos que existen solo en estado interno pueden considerarse no controlados (porque el padre no puede cambiarlos directamente).
Es importante señalar aquí que es incorrecto llamar a un componente controlado o no controlado porque un componente no es ninguno. En cambio, nos referimos a los datos como controlados / no controlados. Y de hecho, como veremos pronto, un solo componente puede controlarse y descontrolarse sin violar ninguna regla, siempre que no se haga por el mismo valor.
Esta regla a menudo se viola al mezclar los dos conceptos, creando un componente con valores de datos controlados y no controlados. El uso de tal componente puede conducir a una violación del principio de fuente única de verdad. Además, como demostraré más adelante, dicho componente será incapaz de manejar ciertos escenarios.
Un ejemplo
El ejemplo más común es un componente de entrada. Dicho componente puede recibir información del usuario, pero también puede recibir información de la propia aplicación (por ejemplo, al establecer el valor inicial). Esta comunicación de datos bidireccional es lo que hace que este tipo de componente sea propenso a problemas de mezcla controlados / incontrolados.
Aquí hay un ejemplo básico de un componente de este tipo:
const TextInput = ({value = '', onChange = () => null}) => { const [innerValue, setInnerValue] = useState(value); const handleOnChange = e => { setInnerValue(e.target.value); onChange(e.target.value); }; return ( <input type="text" value={innerValue} onChange={handleOnChange}/> ); }
Como puede ver, el componente recibe su valor inicial como prop, así como un manejador onChange
. En este punto, el accesorio value
está controlado y no controlado, ya que el valor se comunica de ida y vuelta a la capa contenedora del componente a través de los accesorios value
y onChange
, pero, al mismo tiempo, el valor se mantiene en un estado interno.
El primer y más evidente problema con esto es la violación del principio de fuente única de verdad. Eso es porque ahora potencialmente puede tener el mismo valor en dos ubicaciones de memoria diferentes. Si estas ubicaciones no están sincronizadas, su aplicación comenzará a hacer cosas raras.
El problema menos aparente aquí son los problemas que enfrentará cuando intente utilizar la comunicación bidireccional que este componente debe admitir.
Digamos que desea tener un botón de reinicio en algún lugar de su aplicación, que, al hacer clic, reiniciará el valor de su componente de entrada. Para que funcione, tendremos que ajustar un poco nuestro componente para escuchar los cambios en el accesorio value
. Podemos hacer eso usando el gancho useEffect
:
const TextInput = ({value = '', onChange = () => null}) => { const [innerValue, setInnerValue] = useState(value); const handleOnChange = e => { setInnerValue(e.target.value); onChange(e.target.value); }; // Update the inner value when the external value changes // to support bi-directional data communication useEffect(() => { setInnerValue(innerValue => { if (value !== innerValue) { return value; } return innerValue; }); }, [value, setInnerValue]); return ( <input type="text" value={innerValue} onChange={handleOnChange}/> ); }
Eso ya hace las cosas más complejas y menos legibles, y esta es la versión limpia. Esto se vuelve mucho peor cuando hacemos esto en un componente de clase, con todas esas cosas buenas getDerivedStateFromProps
. Pero al menos está funcionando, ¿verdad?
Supongamos que al hacer clic en el botón de reinicio se pasa una cadena vacía ‘’
como valor para nuestro componente. Ahora, suponga que nos gustaría que el valor inicial también fuera una cadena vacía ‘’
. ¿Qué pasaría si el usuario escribe algo y luego hace clic en el botón de reinicio? ¡Nada!
No pasará nada porque el accesorio value
no ha cambiado, sigue siendo una cadena vacía, por lo que el componente no se volverá a renderizar. Incluso si pudiéramos encontrar una manera de obligarlo a volver a renderizar, el useEffect
nunca será llamado desde value
es el mismo.
Podemos, por supuesto, agregar otro accesorio llamado initialValue
, pero eso solo cubrirá ese caso específico. Todavía es posible tener un escenario diferente en el que necesitamos establecer el valor interno a lo que value
ya es. No podemos agregar otro accesorio para cada caso, ¿verdad?
Levantando el Estado
La solución a esto es muy simple: mantenga el estado en la capa contenedora (es decir, cualquier ancestro, no solo el padre directo), en lugar del estado interno. Esto le permite mantener un flujo de datos bidireccional limpio sin violar ningún principio. También hace que sus componentes sean mucho menos complejos y complicados.
Una versión mejor de nuestro horrible componente de entrada se vería así:
const TextInput = ({value = '', onChange = () => null}) => ( <input type="text" value={value} onChange={e => onChange(e.target.value)}/> );
¿No se ve mucho mejor? Sin embargo, este componente no solo es atractivo, sino que también tiene una gran personalidad, no viola ningún principio. Para completar nuestro ejemplo, una aplicación básica con un botón de reinicio se vería así:
const App = () => { const [value, setValue] = useState(''); return ( <div id="app"> <button onClick={() => setValue('')}>Reset</button> <TextInput value={value} onChange={setValue}/> </div> ); }
La única desventaja de esto es que ahora debemos tener un estado siempre que estemos usando este componente. Pero ese es un pequeño precio a pagar, y la mayoría de las aplicaciones probablemente tengan algún estado central de todos modos.
Mezcla legalmente controlada y no controlada
Como mencioné anteriormente, el concepto de controlado / no controlado se refiere a los accesorios de un componente en lugar del componente en sí. Esto significa que está perfectamente bien tener un estado interno en un componente con algunos accesorios controlados , siempre que este estado no afecte ni se vea afectado por estos accesorios.
Con eso en mente, agreguemos a la definición inicial citada al principio del artículo:
Un solo valor dentro del conjunto de datos de un componente (propiedades y estado), puede ser controlado o no controlado, pero no ambos. Sin embargo, un solo componente puede tener una mezcla de valores controlados y no controlados.
Para ilustrar esto, agreguemos un estado interno a nuestro TextInput
, que agrega una .focused
clase cuando el componente está enfocado:
const TextInput = ({value = '', onChange = () => null}) => { const [focused, setFocused] = useState(false); return ( <input type="text" value={value} onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} className={focused ? 'focused' : ''} onChange={e => onChange(e.target.value)}/> ); };
Este componente ahora está mezclando legalmente conceptos controlados / no controlados . Está controlado por el apoyo value
y no controlado por el estado focused
. Esto está perfectamente bien, ya que todavía tenemos una fuente única para nuestras verdades y no enfrentaremos los problemas discutidos anteriormente.
Cambio entre controlado y no controlado sobre la marcha
A veces necesitamos que nuestro componente esté controlado en ciertos casos, pero descontrolado en otros, para el mismo accesorio. Esto nos lleva a otro punto clave con respecto a todo este concepto:
Un solo valor dentro del conjunto de datos de un componente (accesorios y estado) puede ser controlado y no controlado, siempre que no sea al mismo tiempo.
Para ilustrar esto, ampliemos nuestro ejemplo de enfoque anterior. Digamos que queremos poder controlar el estado de enfoque de nuestra entrada externamente, pero solo en ciertos casos. Cuando no necesitamos controlarlo externamente, nos gustaría que tuviera un estado interno. Esto nos permite evitar agregar la placa useState
de caldera a nivel del contenedor cuando no necesitamos controlarla.
Esto se puede hacer mediante la introducción de 3 nuevos apoyos: focused
, onFocus
y onBlur
. Por simplicidad, he eliminado los accesorios value
/ onChange
:
const TextInput = ({focused: externalFocused, onFocus = () => null, onBlur = () => null}) => { const [internalFocused, setInternalFocused] = useState(false); // If the focused prop is undefined, the internal state will be used instead. const focused = externalFocused === undefined ? internalFocused : externalFocused; const handleOnFocus = e => { if (externalFocused === undefined) { setInternalFocused(true); } onFocus(e); } const handleOnBlur = e => { if (externalFocused === undefined) { setInternalFocused(false); } onBlur(e); } return ( <input type="text" onFocus={handleOnFocus} onBlur={handleOnBlur} className={focused ? 'focused' : ''}/> ) }
En el ejemplo anterior, dejar el focused
vacío lo convertirá en un valor no controlado y el componente usará un estado interno para él. Sin embargo, pasar un valor booleano a esa propiedad lo hará controlado , y se ignorará el estado interno.
Esto nos permite cambiar entre controlado / no controlado sobre la marcha pasando un valor especial a focused
(en este caso, ese valor especial es undefined
).
El concepto clave aquí es que no puede haber un caso en el que tanto el estado interno como el accesorio se utilicen simultáneamente. Si usamos el prop, el estado interno será ignorado y viceversa. De modo que ese valor no se puede controlar y descontrolar al mismo tiempo, lo que nos permite evitar todos los problemas asociados con eso.
Conclusión
Espero que esto aclare estos conceptos y subraye la importancia de no mezclarlos. Según mi experiencia, mezclar incorrectamente conceptos controlados / no controlados para un solo valor puede conducir a defectos muy desagradables y que mantenerlos adecuadamente separados hace que los componentes sean más limpios, legibles y robustos.
Gracias por leer.
Añadir comentario