Bombeamos ganchos React con FRP

Habiendo dominado los ganchos, muchos desarrolladores de React han experimentado euforia, finalmente obteniendo un kit de herramientas simple y conveniente que le permite implementar tareas con significativamente menos código. Pero, ¿significa esto que los ganchos estándar useState y useReducer que se ofrecen listos para usar son todo lo que necesitamos para administrar el estado?


En mi opinión, en su forma cruda, su uso no es muy conveniente, es más probable que puedan considerarse como la base para construir ganchos de administración estatal realmente convenientes. Los propios desarrolladores de React fomentan el desarrollo de ganchos personalizados, entonces, ¿por qué no hacerlo? Debajo del corte, veremos un ejemplo muy simple y comprensible, lo que está mal con los ganchos ordinarios y cómo se pueden mejorar, tanto que se niegan completamente a usarlos en su forma pura.


Hay un cierto campo para la entrada, condicionalmente, un nombre. Y hay un botón al hacer clic en el cual debemos realizar una solicitud al servidor con el nombre ingresado (una búsqueda determinada). Parece que podría ser más fácil? Sin embargo, la solución está lejos de ser obvia. La primera implementación ingenua:


const App = () => { const [name, setName] = useState(''); const [request, setRequest] = useState(); const [result, setResult] = useState(); useEffect(() => { fetch('//example.api/' + name).then((data) => { setResult(data.result); }); }, [request]); return <div> <input onChange={e => setName(e.target.value)}/> <input type="submit" value="Check" onClick={() => setRequest(name)}/> { result && <div>Result: { result }</div> } </div>; } 

¿Qué está mal aquí? Si el usuario, ingresando algo en el campo, envía el formulario dos veces, solo la primera solicitud funcionará para nosotros, porque en el segundo clic, la solicitud no cambiará y useEffect no funcionará. Si imaginamos que nuestra aplicación es un servicio de búsqueda de tickets, y el usuario puede enviar el formulario una y otra vez sin hacer cambios, ¡tal implementación no funcionará para nosotros! Usar el nombre como una dependencia para useEffect también es inaceptable, de lo contrario, el formulario se enviará inmediatamente cuando cambie el texto. Bueno, tienes que mostrar ingenio.


 const App = () => { const [name, setName] = useState(''); const [request, setRequest] = useState(); const [result, setResult] = useState(); useEffect(() => { fetch('//example.api/' + name).then((data) => { setResult(data.result); }); }, [request]); return <div> <input onChange={e => setName(e.target.value)}/> <input type="submit" value="Check" onClick={() => setRequest(!request)}/> { result && <div>Result: { result }</div> } </div>; } 

Ahora, con cada clic, cambiaremos el significado de la solicitud al opuesto, lo que logrará el comportamiento deseado. Esta es una muleta muy pequeña e inocente, pero hace que el código sea algo confuso de entender. Quizás ahora te parezca que succiono el problema de mi dedo e inflo su escala. Bueno, para responder si es cierto o no, debe comparar este código con otras implementaciones que ofrecen un enfoque más expresivo.


Veamos este ejemplo a nivel teórico usando abstracción de hilo. Es muy conveniente para describir el estado de las interfaces de usuario. Entonces, tenemos dos flujos: datos ingresados ​​en el campo de texto (nombre $) y un flujo de clics en el botón de envío del formulario (clic $). A partir de ellos, necesitamos crear un tercer flujo combinado de solicitudes al servidor.


 name$ __(C)____(Ca)_____(Car)____________________(Carl)___________ click$ ___________________________()______()________________()_____ request$ ___________________________(Car)___(Car)_____________(Carl)_ 

Aquí está el comportamiento que necesitamos lograr. Cada flujo tiene dos aspectos: el valor que tiene y el momento en el que los valores fluyen a través de él. En diversas situaciones, podemos necesitar uno u otro aspecto, o ambos. Puedes comparar esto con el ritmo y la armonía de la música. Las secuencias para las cuales solo el tiempo de respuesta es esencial también se denominan señales.


En nuestro caso, hacer clic en $ es una señal pura: no importa qué valor fluya a través de él (indefinido / verdadero / Evento / lo que sea), es importante solo cuando esto sucede. Nombre del caso $
lo contrario: sus cambios de ninguna manera implican cambios en el sistema, pero es posible que necesitemos su significado en algún momento. Y a partir de estos dos flujos necesitamos hacer el tercero, tomando desde el primer tiempo, desde el segundo valor.


En el caso de Rxjs, tenemos un operador casi listo para esto:


 const names$ = fromEvent(...); const click$ = fromEvent(...); const request$ = click$.pipe(withLatestFrom(name$), map(([name]) => fromPromise(fetch(...)))); 

Sin embargo, el uso práctico de Rx en React puede ser bastante inconveniente. Una opción más adecuada es la biblioteca mrr , construida sobre los mismos principios funcionales-reactivos que Rx, pero especialmente adaptada para su uso con React según el principio de "reactividad total" y conectada como un gancho.


 import useMrr from 'mrr/hooks'; const App = props => { const [state, set] = useMrr(props, { result: [name => fetch('//example.api/' + name).then(data => data.result), '-name', 'submit'], }); return <div> <input value={state.name} onChange={set('name')}/> <input type="submit" value="Check" onClick={set('submit')}/> { state.result && <div>Result: { state.result }</div> } </div>; } 

La interfaz useMrr es similar a useState o useReducer: devuelve un objeto de estado (valores de todos los hilos) y un setter para poner los valores en hilos. Pero dentro de todo es un poco diferente: cada campo de estado (= secuencia), excepto aquellos en los que colocamos valores directamente de los eventos DOM, se describe mediante una función y una lista de subprocesos principales, cuyo cambio hará que el niño se recalcule. En este caso, los valores de los hilos primarios serán sustituidos en la función. Si solo queremos obtener el valor de la secuencia, pero no responder a su cambio, entonces escribimos un "menos" delante del nombre, como en el caso del nombre.


Conseguimos el comportamiento deseado, en esencia, en una línea. Pero esto no es solo brevedad. Comparemos los resultados obtenidos con más detalle y, en primer lugar, con respecto a un parámetro como la legibilidad y la claridad del código resultante.


En mrr, podrá separar casi por completo la "lógica" de la "plantilla": no tendrá que escribir ningún manejador imperativo complejo en JSX. Todo es extremadamente declarativo: simplemente asignamos el evento DOM al flujo correspondiente, prácticamente sin conversión (para los campos de entrada, el valor e.target.value se extrae automáticamente, a menos que especifique lo contrario), y ya en la estructura useMrr describimos cómo se forman los flujos base filiales Por lo tanto, en el caso de las transformaciones de datos sincrónicas y asincrónicas, siempre podemos rastrear fácilmente cómo se forma nuestro valor.


Comparando con Px: ni siquiera tuvimos que usar operadores adicionales: si, como resultado, las funciones de mrr reciben una promesa, esperará automáticamente hasta que se resuelva y ponga los datos recibidos en la secuencia. Además, en lugar de withLatestFrom, utilizamos
escucha pasiva (signo menos), lo cual es más conveniente. Imagine que además del nombre necesitaremos enviar otros campos. Luego, en mrr agregaremos otra secuencia de escucha pasiva:


 result: [(name, surname) => fetch(...), '-name', '-surname', 'submit'], 

Y en Rx tienes que esculpir uno más con LatestFrom con un mapa, o primero combinar nombre y apellido en una secuencia.


Pero volvamos a ganchos y mrr. Un registro más legible de las dependencias, que siempre muestra cómo se forman los datos, es quizás una de las principales ventajas. La interfaz actual useEffect básicamente no permite responder a flujos de señal, razón por la cual
Tengo que pensar en diferentes giros.


Otro punto es que la opción de ganchos ordinarios conlleva representaciones adicionales. Si el usuario acaba de hacer clic en el botón, esto aún no implica ningún cambio en la interfaz de usuario que la reacción necesita dibujar. Sin embargo, se llamará un render. En la variante con mrr, el estado devuelto solo se actualizará cuando ya haya llegado una respuesta del servidor. ¿Ahorrando en fósforos, dices? Bueno, tal vez. Pero para mí personalmente, el principio de "volver a representarte en cualquier situación incomprensible", que es la base de los ganchos básicos, causa rechazo.


Renders adicionales significan una nueva formación de controladores de eventos. Por cierto, aquí los ganchos comunes son todos malos. Los manejadores no solo son imprescindibles, sino que también deben regenerarse cada vez que se procesan. Y no será posible usar el almacenamiento en caché aquí, porque muchos manejadores deben estar bloqueados a las variables de componentes internos. Los manejadores mrr son más declarativos, y el almacenamiento en caché ya está integrado en mrr: set ('name') se generará solo una vez, y se sustituirá de la caché para los renderizados posteriores.


Con un aumento en la base del código, los controladores imperativos pueden volverse aún más engorrosos. Digamos que también necesitamos mostrar el número de envíos de formularios realizados por el usuario.


 const App = () => { const [request, makeRequest] = useState(); const [name, setName] = useState(''); const [result, setResult] = useState(false); const [clicks, setClicks] = useState(0); useEffect(() => { fetch('//example.api/' + name).then((data) => { setResult(data.result); }); }, [request]); return <div> <input onChange={e => setName(e.target.value)}/> <input type="submit" value="Check" onClick={() => { makeRequest(!request); setClicks(clicks + 1); }}/><br /> Clicked: { clicks } </div>; } 

No muy bien parecido. Por supuesto, puede representar el controlador como una función separada dentro del componente. La legibilidad aumentará, pero permanecerá el problema de regenerar la función con cada render, así como el problema de la imperativa. En esencia, este es un código de procedimiento regular, a pesar de la creencia generalizada de que React API está cambiando gradualmente hacia un enfoque funcional.


Para aquellos para quienes la escala del problema parece exagerada, puedo responder que, por ejemplo, los desarrolladores de React están conscientes del problema de la generación excesiva de controladores, ofreciéndonos inmediatamente una muleta en forma de useCallback.


En mrr:


 const App = props => { const [state, set] = useMrr(props, { $init: { clicks: 0, }, isValid: [name => fetch('//example.api/' + name).then(data => data.isValid), '-name', 'makeRequest'], clicks: [a => a + 1, '-clicks', 'makeRequest'], }); return <div> <input onChange={set('name')}/> <input type="submit" value="Check" onClick={set('makeRequest')}/> </div>; } 

Una alternativa más conveniente es useReducer, que le permite abandonar el imperativo de los controladores. Pero quedan otros problemas importantes: la falta de trabajo con las señales (ya que el mismo useEffect será responsable de los efectos secundarios), así como la peor legibilidad durante las conversiones asincrónicas (en otras palabras, es más difícil rastrear la relación entre los campos de la tienda, debido al mismo useEffect ) Si en mrr el gráfico de dependencia entre los campos de estado (subprocesos) es claramente visible de inmediato, en los ganchos debe mirar un poco hacia arriba y hacia abajo.


Además, compartir useState y useReducer en el mismo componente no es muy conveniente (de nuevo, habrá controladores complejos e imperativos que cambiarán algo en useState
y acción de envío), por lo que, muy probablemente, antes de desarrollar el componente, deberá aceptar una u otra opción.


Por supuesto, la consideración de todos los aspectos aún puede continuar. Para no ir más allá del alcance del artículo, tocaré algunos puntos menos importantes en su totalidad.


Registro centralizado, depuración. Como en mrr todas las transmisiones están contenidas en un concentrador, para la depuración es suficiente agregar un indicador:


 const App = props => { const [state, set] = useMrr(props, { $log: true, $init: { clicks: 0, }, isValid: [name => fetch('//example.api/' + name).then(data => data.isValid), '-name', 'makeRequest'], clicks: [a => a + 1, '-clicks', 'makeRequest'], }); ... 

Después de eso, todos los cambios en las transmisiones se mostrarán en la consola. Para acceder a todo el estado (es decir, los valores actuales de todos los subprocesos), hay un pseudo-flujo $ state:


 a: [({ name, click, result }) => { ... }, '$state', 'click'], 

Por lo tanto, si necesita o está muy acostumbrado al estilo editorial, puede escribir en estilo editor en mrr, devolviendo un nuevo valor de campo basado en el evento y todo el estado anterior. Pero lo contrario (escribir en useReducer o un editor en el estilo mrr) no funcionará, debido a la falta de reactividad en estos.


Trabaja con el tiempo. ¿Recuerdas dos aspectos de los flujos: significado y tiempo de respuesta, armonía y ritmo? Entonces, trabajar con el primero en ganchos ordinarios es bastante simple y conveniente, pero con el segundo, no. Al trabajar a lo largo del tiempo, me refiero a la formación de flujos secundarios, cuyo "ritmo" es diferente del de los padres. Esto es principalmente todo tipo de filtros, debowns, trotl, etc. Es muy probable que tenga que implementar todo esto. En mrr, puede usar declaraciones listas para usar listas para usar. El conjunto de caballeros MRR es inferior a la variedad de operadores Rx, pero tiene un nombre más intuitivo.


Interacción intercomponente. Recuerdo que en el Editor se consideraba una buena práctica crear solo una historia. Si usamos useReducer en muchos componentes,
Puede haber un problema con la organización de la interacción entre las partes. En MRR, los flujos pueden "fluir" libremente de un componente a otro hacia arriba o hacia abajo de la jerarquía, pero esto no creará problemas debido al enfoque declarativo. Más detalles
Este tema, así como otras características de la API de MRR, se describen en el artículo Actores + FRP en React


Conclusiones


Los nuevos ganchos de reacción son geniales y simplifican nuestras vidas, pero tienen algunos defectos que un gancho de propósito general de nivel superior (gestión del estado) puede solucionar. UseMrr de la biblioteca mrr funcional-reactiva fue propuesta y considerada como tal.


Problemas y sus soluciones:


  • recuentos innecesarios de datos en cada render (en mrr están ausentes debido a la reactividad basada en push)
  • Representaciones adicionales cuando un cambio de estado no implica un cambio en la interfaz de usuario
  • mala legibilidad del código con conversiones asincrónicas (en comparación con las sincrónicas). En mrr, el código asincrónico no es inferior al sincrónico en cuanto a legibilidad y expresividad. La mayoría de los problemas discutidos en un artículo reciente sobre useEffect en mrr son básicamente imposibles
  • controladores imperativos que no siempre se pueden almacenar en caché (en mrr se almacenan en caché automáticamente, casi siempre se pueden almacenar en caché, declarativos)
  • Usar useState y useReducer al mismo tiempo puede crear un código incómodo
  • falta de herramientas para convertir flujos a lo largo del tiempo (rebote, aceleración, condición de carrera)

En muchos puntos, se puede argumentar que pueden resolverse mediante ganchos personalizados. Pero esto es precisamente lo que se propone, pero en lugar de implementaciones dispares, para cada tarea separada, se propone una solución holística y consistente.


Muchos problemas se han vuelto demasiado familiares para que podamos ser claramente reconocidos. Por ejemplo, las conversiones asincrónicas siempre parecían más complicadas y confusas que las sincrónicas, y los ganchos en este sentido no son peores que los enfoques anteriores (eds, etc.). Para darse cuenta de esto como un problema, primero debe ver otros enfoques que ofrecen una mejor solución.


Este artículo no pretende imponer puntos de vista específicos, sino llamar la atención sobre el problema. Estoy seguro de que existen o se están creando otras soluciones que pueden convertirse en una alternativa valiosa, pero que aún no se conocen ampliamente. La próxima API React Cache también puede hacer una gran diferencia. Estaré encantado de criticar y discutir en los comentarios.


Los interesados ​​también pueden ver una presentación sobre este tema en kyivjs el 28 de marzo.

Source: https://habr.com/ru/post/445214/


All Articles