Hoy en día, pocas personas escriben en Perl, pero la famosa máxima de Larry Wall "Mantener las cosas simples fáciles y difíciles" se ha convertido en la fórmula generalmente aceptada para una tecnología efectiva. Se puede interpretar en el aspecto no solo de la complejidad de las tareas, sino también del enfoque: una tecnología ideal debería, por un lado, permitir el desarrollo rápido de aplicaciones medianas y pequeñas (incluyendo "solo escritura"), por otro lado, proporcionar herramientas para un desarrollo reflexivo aplicaciones complejas, donde la fiabilidad, el mantenimiento y la estructura son primordiales. O incluso, traducir al plano humano: ser accesible para los Jones y, al mismo tiempo, satisfacer las solicitudes de los Signyors.
Los editores, ahora populares, pueden ser criticados por ambos lados: tomemos al menos el hecho de que escribir incluso la funcionalidad elemental puede dar lugar a muchas líneas espaciadas en varios archivos, pero no profundizaremos, porque ya se ha dicho mucho sobre esto.
“Se supone que debes mantener todas las mesas en una habitación y las sillas en otra”
- Juha Paananen, creador de la biblioteca Bacon.js, sobre el Editor
La tecnología que se discutirá hoy no es una bala de plata, pero afirma ser más consistente con estos criterios.
Mrr es una biblioteca reactiva funcional que profesa el principio de "todo es flujo". Las principales ventajas proporcionadas por el enfoque funcional-reactivo en mrr son la concisión, la expresividad del código, así como un enfoque unificado para las transformaciones de datos sincrónicas y asincrónicas.
A primera vista, esto no parece una tecnología que sea fácilmente accesible para los principiantes: el concepto de transmisión puede ser difícil de entender, no está tan extendido en el front-end, asociado principalmente con bibliotecas tan tontas como Rx. Y lo más importante, no está del todo claro cómo explicar los flujos basados en el esquema básico "acción-reacción-actualización DOM". Pero ... ¡no hablaremos de manera abstracta sobre los flujos! Hablemos de cosas más comprensibles: eventos, condición.
Cocinar según la receta.
Sin entrar en la naturaleza de FRP, seguiremos un esquema simple para formalizar el área temática:
- haga una lista de datos que describa el estado de la página y se utilizará en la interfaz de usuario, así como sus tipos.
- haga una lista de eventos que ocurran o que el usuario genere en la página, y los tipos de datos que se transmitirán con ellos
- hacer una lista de procesos que ocurrirán en la página
- determinar las interdependencias entre ellos.
- Describa las interdependencias utilizando los operadores apropiados.
Al mismo tiempo, necesitamos conocer la biblioteca solo en la última etapa.
Entonces, tomemos un ejemplo simplificado de una tienda web, en la que hay una lista de productos con paginación y filtrado por categoría, así como una cesta.
Datos en función de los cuales se construirá la interfaz:
- lista de bienes (matriz)
- categoría seleccionada (línea)
- número de páginas con productos (número)
- lista de productos que están en la cesta (matriz)
- página actual (número)
- El número de productos en la cesta (número)
Eventos (por "eventos" se refieren solo a eventos momentáneos. Las acciones que ocurren por un tiempo - procesos - necesitan descomponerse en eventos separados):
- página de inicio (nula)
- selección de categoría (cadena)
- Agregar bienes a la cesta (objeto "bienes")
- Retirada de mercancías de la cesta (identificación de las mercancías que se eliminarán)
- vaya a la página siguiente de la lista de productos (número - número de página)
Procesos: son acciones que comienzan y luego pueden terminar con diferentes eventos a la vez o después de un tiempo. En nuestro caso, esta será la carga de datos del producto desde el servidor, lo que puede implicar dos eventos: finalización exitosa y finalización con un error.
Interdependencias entre eventos y datos. Por ejemplo, la lista de productos dependerá del evento: "carga exitosa de la lista de productos". Y “comience a cargar la lista de productos”: desde “abrir una página”, “seleccionar la página actual”, “seleccionar una categoría”. Haga una lista de la forma [elemento]: [... dependencias]:
{ requestGoods: ['page', 'category', 'pageLoaded'], goods: ['requestGoods.success'], page: ['goToPage', 'totalPages'], totalPages: ['requestGoods.success'], cart: ['addToCart', 'removeFromCart'], goodsInCart: ['cart'], category: ['selectCategory'] }
Oh ... pero este es casi el código para mrr!

Solo queda agregar funciones que describan la relación. Es posible que haya esperado que los eventos, los datos y los procesos sean entidades diferentes en MRR, pero no, ¡todo esto son hilos! Nuestra tarea es conectarlos correctamente.
Como puede ver, tenemos dos tipos de dependencias: "datos" del "evento" (por ejemplo, página de goToPage) y "datos" de los "datos" (bienesInCart del carrito). Para cada uno de ellos hay enfoques apropiados.
La forma más fácil es con "datos de datos": aquí simplemente agregamos una función pura, la "fórmula":
goodsInCart: [arr => arr.length, 'cart'],
Cada vez que se cambia la matriz del carrito, se volverá a calcular el valor de goodsInCart.
Si nuestros datos dependen de un evento, entonces todo también es bastante simple:
category: 'selectCategory', goods: [resp => resp.data, 'requestGoods.success'], totalPages: [resp => resp.totalPages, 'requestGoods.success'],
El diseño de la forma [función, ... hilos de discusión] es la base de mrr. Para una comprensión intuitiva, dibujando una analogía con Excel, los flujos en mrr también se llaman celdas, y las funciones por las cuales se calculan se llaman fórmulas.
Si nuestros datos dependen de varios eventos, debemos transformar sus valores individualmente y luego combinarlos en una secuencia usando el operador de fusión:
page: ['merge', [a => a, 'goToPage'], [(a, prev) => a < prev ? a : prev, 'totalPages', '-page'] ], cart: ['merge', [(item, arr) => [...arr, item], 'addToCart', '-cart'], [(id, arr) => arr.filter(item => item.id !== id), 'removeFromCart', '-cart'], ],
En ambos casos, nos referimos al valor anterior de la celda. Para evitar un bucle infinito, nos referimos al carrito y a las celdas de la página pasivamente (signo menos delante del nombre de la celda): sus valores se sustituirán en la fórmula, pero si cambian, el recálculo no comenzará.
Todos los hilos se crean a partir de otros hilos o se emiten desde el DOM. Pero, ¿qué pasa con la transmisión de "página de inicio"? Afortunadamente, no tiene que usar componentDidMount: en mrr hay una secuencia especial $ start, que indica que el componente se ha creado y montado.
Los "procesos" se calculan de forma asíncrona, mientras que emitimos ciertos eventos a partir de ellos, el operador "anidado" nos ayudará aquí:
requestGoods: ['nested', (cb, page, category) => { fetch("...") .then(res => cb('success', res)) .catch(e => cb('error', e)); }, 'page', 'category', '$start'],
Al usar el operador anidado, el primer argumento nos pasará una función de devolución de llamada para la emisión de ciertos eventos. En este caso, desde el exterior serán accesibles a través del espacio de nombres de la celda raíz, por ejemplo,
cb('success', res)
dentro de la fórmula requestGoods, se obtendrá la actualización de la celda requestGoods.success.
Para representar correctamente la página antes de calcular nuestros datos, puede especificar sus valores iniciales:
{ goods: [], page: 1, cart: [], },
Añadir marcado. Creamos un componente React utilizando la función withMrr, que acepta un diagrama de enlace reactivo y una función de representación. Para "poner" un valor en una secuencia, utilizamos la función $, que crea (y almacena en caché) controladores de eventos. Ahora nuestra aplicación totalmente funcional se ve así:
import { withMrr } from 'mrr'; const App = withMrr({ $init: { goods: [], cart: [], page: 1, }, requestGoods: ['nested', (cb, page = 1, category = 'all') => { fetch('https://reqres.in/api/products?page=', page, category).then(r => r.json()) .then(res => cb('success', res)) .catch(e => cb('error', e)) }, 'page', 'selectCategory', '$start'], goods: [res => res.data, 'requestGoods.success'], page: ['merge', 'goToPage', [(a, prev) => a < prev ? a : prev, 'totalPages', '-page']], totalPages: [res => res.total_pages, 'requestGoods.success'], category: 'selectCategory', cart: ['merge', [(item, arr) => [...arr, item], 'addToCart', '-cart'], [(id, arr) => arr.filter(item => item.id !== id), 'removeFromCart', '-cart'], ], }, (state, props, $) => { return (<section> <h2>Shop</h2> <div> Category: <select onChange={$('selectCategory')}> <option>All</option> <option>Electronics</option> <option>Photo</option> <option>Cars</option> </select> </div> <ul className="goods"> { state.goods.map((item, i) => { const cartI = state.cart.findIndex(a => a.id === item.id); return (<li key={i}> { item.name } <div> { cartI === -1 && <button onClick={$("addToCart", item)}>Add to cart</button> } { cartI !== -1 && <button onClick={$("removeFromCart", item.id)}>Remove from cart</button> } </div> </li>); }) } </ul> <ul className="pages"> { new Array(state.totalPages).fill(true).map((_, p) => { const page = Number(p) + 1; return ( <li className="page" onClick={$('goToPage', page)} key={p}> { page } </li> ); }) } </ul> </section> <section> <h2>Cart</h2> <ul> { state.cart.map((item, i) => { return (<li key={i}> { item.name } <div> <button onClick={$("removeFromCart", item.id)}>Remove from cart</button> </div> </li>); }) } </ul> </section>); }); export default App;
Construcción
<select onChange={$('selectCategory')}>
significa que cuando se cambia el campo, el valor se "insertará" en la secuencia selectCategory. Pero cual es el significado? Por defecto, esto es event.target.value, pero si necesitamos empujar algo más, lo especificamos con el segundo argumento, como aquí:
<button onClick={$("addToCart", item)}>
Todo aquí, eventos, datos y procesos, son flujos. La activación de un evento provoca un recálculo de datos o eventos dependiendo de él, y así sucesivamente a lo largo de la cadena. El valor de la secuencia dependiente se calcula utilizando una fórmula que puede devolver un valor, o una promesa (entonces MRR esperará su resolución).
La API de MRR es muy concisa y concisa: para la mayoría de los casos, solo necesitamos de 3 a 4 operadores básicos y se pueden hacer muchas cosas sin ellos. Agregue un mensaje de error cuando la lista de productos no se cargue correctamente, que se mostrará durante un segundo:
hideErrorMessage: [() => new Promise(res => setTimeout(res, 1000)), 'requestGoods.error'], errorMessageShown: [ 'merge', [() => true, 'requestGoods.error'], [() => false, 'hideErrorMessage'], ],
Sal, pimienta azúcar al gusto
También hay azúcar sintáctica en mrr, que es opcional para el desarrollo, pero puede acelerarlo. Por ejemplo, el operador de alternar:
errorMessageShown: ['toggle', 'requestGoods.error', [() => new Promise(res => setTimeout(res, 1000)), 'showErrorMessage']],
Un cambio en el primer argumento establecerá la celda en verdadero, y en el segundo en falso.
El enfoque de "descomponer" los resultados de una tarea asincrónica en subcélulas de éxito y error también está tan extendido que puede usar el operador de promesa especial (que elimina automáticamente la condición de carrera):
requestGoods: [ 'promise', (page = 1, category = 'all') => fetch('https://reqres.in/api/products?page=', page, category).then(r => r.json()), 'page', 'selectCategory', '$start' ],
Una gran cantidad de funcionalidades caben en solo un par de docenas de líneas. Nuestro junio condicional está satisfecho: logró escribir código de trabajo, que resultó ser bastante compacto: toda la lógica cabe en un archivo y en una pantalla. Pero el firmante entrecierra los ojos con incredulidad: Eka no se ve ... puedes escribir esto en ganchos / recomponer / etc.
Sí, de hecho lo es! El código, por supuesto, es poco probable que sea aún más compacto y estructurado, pero ese no es el punto. Imaginemos que el proyecto se está desarrollando y necesitamos dividir la funcionalidad en dos páginas separadas: una lista de productos y una cesta. Además, los datos de la cesta, obviamente, deben almacenarse globalmente para ambas páginas.
Un enfoque, una interfaz
Aquí llegamos a otro problema de desarrollo de reacción: la existencia de enfoques heterogéneos para administrar el estado localmente (dentro de un componente) y globalmente a nivel de toda la aplicación. Estoy seguro de que muchos se enfrentaron a un dilema: ¿implementar alguna lógica a nivel local o global? O otra situación: resultó que algunos datos locales deben guardarse globalmente, y hay que reescribir parte de la funcionalidad, por ejemplo, desde recomposiciones hasta editor ...
El contraste es, por supuesto, artificial, y en mrr no lo es: es igual de bueno, y lo más importante: ¡uniforme! - Adecuado tanto para la gestión estatal local como global. En general, no necesitamos ningún estado global, solo tenemos la capacidad de intercambiar datos entre los componentes, por lo que el estado del componente raíz será "global".
El esquema de nuestra aplicación ahora es el siguiente: el componente raíz que contiene la lista de productos en la cesta, y dos subcomponentes: los productos y la cesta, y el componente global "escucha" los flujos "agregar a la cesta" y "eliminar de la cesta" de los componentes secundarios.
const App = withMrr({ $init: { cart: [], currentPage: 'goods', }, cart: ['merge', [(item, arr) => [...arr, item], 'addToCart', '-cart'], [(id, arr) => arr.filter(item => item.id !== id), 'removeFromCart', '-cart'], ], }, (state, props, $, connectAs) => { return ( <div> <menu> <li onClick={$('currentPage', 'goods')}>Goods</li> <li onClick={$('currentPage', 'cart')}>Cart{ state.cart && state.cart.length ? '(' + state.cart.length + ')' : '' }</li> </menu> <div> { state.currentPage === 'goods' && <Goods {...connectAs('goods', ['addToCart', 'removeFromCart'], ['cart'])}/> } { state.currentPage === 'cart' && <Cart {...connectAs('cart', { 'removeFromCart': 'remove' }, ['cart'])}/> } </div> </div> ); })
const Goods = withMrr({ $init: { goods: [], page: 1, }, goods: [res => res.data, 'requestGoods.success'], requestGoods: [ 'promise', (page = 1, category = 'all') => fetch('https://reqres.in/api/products?page=', page, category).then(r => r.json()), 'page', 'selectCategory', '$start' ], page: ['merge', 'goToPage', [(a, prev) => a < prev ? a : prev, 'totalPages', '-page']], totalPages: [res => res.total, 'requestGoods.success'], category: 'selectCategory', errorShown: ['toggle', 'requestGoods.error', [cb => new Promise(res => setTimeout(res, 1000)), 'requestGoods.error']], }, (state, props, $) => { return (<div> ... </div>); });
const Cart = withMrr({}, (state, props, $) => { return (<div> <h2>Cart</h2> <ul> { state.cart.map((item, i) => { return (<div> { item.name } <div> <button onClick={$('remove', item.id)}>Remove from cart</button> </div> </div>); }) } </ul> </div>); });
¡Es increíble lo poco que ha cambiado! ¡Simplemente presentamos los flujos en los componentes correspondientes y colocamos "puentes" entre ellos! Al conectar los componentes utilizando la función mrrConnect, especificamos la asignación para flujos descendentes y ascendentes:
connectAs( 'goods', ['addToCart', 'removeFromCart'], ['cart'] )
Aquí, las secuencias addToCart y removeFromCart del componente secundario irán al elemento primario y la secuencia del carrito volverá. No estamos obligados a usar los mismos nombres de flujo; si no coinciden, usamos el mapeo:
connectAs('cart', { 'removeFromCart': 'remove' })
La secuencia de eliminación del componente secundario será la fuente de la secuencia removeFromCart en el elemento primario.
Como puede ver, el problema de elegir una ubicación de almacenamiento de datos en el caso de mrr se elimina por completo: almacena los datos donde se determina lógicamente.
Aquí nuevamente, uno no puede dejar de notar el inconveniente del Editor: en él debe guardar todos los datos en un único repositorio central. ¡Incluso los datos que pueden ser solicitados y utilizados por un solo componente separado o su subárbol! Si escribiéramos en el "estilo editorial", también llevaríamos la carga y la paginación de bienes al nivel global (para ser justos, este enfoque, gracias a la flexibilidad de MRR, también es posible y tiene derecho a la vida, código fuente ).
Sin embargo, esto no es necesario. Los bienes cargados se usan solo en el componente de bienes, por lo tanto, llevándolos al nivel global, solo obstruimos e inflamos el estado global. Además, tendremos que borrar los datos obsoletos (por ejemplo, una página de paginación) cuando el usuario regrese a la página del producto nuevamente. Al elegir el nivel correcto de almacenamiento de datos, evitamos automáticamente tales problemas.
Otra ventaja de este enfoque es que la lógica de la aplicación se combina con la presentación, lo que nos permite reutilizar componentes individuales de React como widgets totalmente funcionales, en lugar de como plantillas "tontas". Además, manteniendo un mínimo de información a nivel global (idealmente, esto es solo datos de sesión) y sacando la mayor parte de la lógica en componentes de página separados, reducimos en gran medida la coherencia del código. Por supuesto, este enfoque no siempre es aplicable, pero hay una gran cantidad de tareas en las que el estado global es extremadamente pequeño y las "pantallas" individuales son casi completamente independientes entre sí: por ejemplo, varios tipos de administradores, etc. A diferencia del Editor, que nos provoca llevar todo lo que se necesita y no se necesita al nivel global, MRR le permite almacenar datos en subárboles separados, fomentando y haciendo posible la encapsulación, por lo que nuestra aplicación pasa de ser un "pastel" monolítico a un "pastel" en capas.
Vale la pena mencionar: por supuesto, ¡no hay nada revolucionario nuevo en el enfoque propuesto! Los componentes, widgets autónomos, han sido uno de los enfoques básicos utilizados desde la llegada de los frameworks js. La única diferencia significativa es que mrr sigue el principio declarativo: los componentes solo pueden escuchar los flujos de otros componentes, pero no pueden influir en ellos (¿qué tiene que hacer desde la dirección de abajo hacia arriba o desde la dirección de arriba hacia abajo, que difiere del flujo? enfoque). Los componentes inteligentes que solo pueden intercambiar mensajes con los componentes subyacentes y principales corresponden al modelo de actor popular pero poco conocido en el desarrollo front-end (el tema del uso de actores e hilos en el front-end está bien cubierto en el artículo Introducción a la programación reactiva ).
Por supuesto, esto está lejos de ser la implementación canónica de los actores, pero la esencia es exactamente esto: el papel de los actores es desempeñado por los componentes que intercambian mensajes a través de flujos MPP; un componente puede (¡declarativamente!) crear y eliminar actores componentes secundarios gracias al DOM virtual y React: la función de representación, en esencia, determina la estructura de los actores secundarios.
En lugar de la situación estándar de React, cuando "soltamos" el componente principal en una determinada devolución de llamada mediante accesorios, deberíamos escuchar el flujo del componente secundario desde el principal. Lo mismo está en la dirección opuesta, de padres a hijos. Por ejemplo, puede preguntar: ¿por qué transferir los datos de la cesta del carrito al componente Carrito como una secuencia, si podemos, sin más preámbulos, simplemente pasarlos como accesorios? Cual es la diferencia De hecho, este enfoque también se puede utilizar, pero solo hasta que sea necesario responder a los cambios en los accesorios. Si alguna vez ha utilizado el método componentWillReceiveProps, entonces sabe de qué se trata. Este es un tipo de "reactividad para los pobres": escucha absolutamente todos los cambios de utilería, determina qué ha cambiado y reacciona. Pero este método pronto desaparecerá de React, y puede surgir la necesidad de una reacción a las "señales de arriba".
En mrr, los flujos “fluyen” no solo hacia arriba, sino también hacia abajo en la jerarquía de componentes, para que los componentes puedan responder independientemente a los cambios de estado. Al hacerlo, puede utilizar toda la potencia de las herramientas reactivas de MRR.
const Cart = withMrr({ foo: [items => {
Añade un poco de burocracia
El proyecto está creciendo, se hace difícil hacer un seguimiento de los nombres de los flujos, que son ... ¡oh, horror! - Se almacenan en filas. Bueno, podemos usar constantes para nombres de flujo, así como para declaraciones mrr. Ahora, romper una aplicación haciendo un pequeño error tipográfico se vuelve más difícil.
import { withMrr } from 'mrr'; import { merge, toggle, promise } from 'mrr/operators'; import { cell, nested, $start$, passive } from 'mrr/cell'; const goods$ = cell('goods'); const page$ = cell('page'); const totalPages$ = cell('totalPages'); const category$ = cell('category'); const errorShown$ = cell('errorShown'); const addToCart$ = cell('addToCart'); const removeFromCart$ = cell('removeFromCart'); const selectCategory$ = cell('selectCategory'); const goToPage$ = cell('goToPage'); const Goods = withMrr({ $init: { [goods$]: [], [page$]: 1, }, [goods$]: [res => res.data, requestGoods$.success], [requestGoods$]: promise((page, category) => fetch('https://reqres.in/api/products?page=', page).then(r => r.json()), page$, category$, $start$), [page$]: merge(goToPage$, [(a, prev) => a < prev ? a : prev, totalPages$, passive(page$)]), [totalPages$]: [res => res.total, requestGoods$.success], [category$]: selectCategory$, [errorShown$]: toggle(requestGoods$.error, [cb => new Promise(res => setTimeout(res, 1000)), requestGoods$.error]), }, ...);
¿Qué hay en la caja negra?
¿Qué hay de las pruebas? La lógica descrita en el componente mrr es fácil de separar de la plantilla y luego probar.
Hagamos la estructura mrr por separado de nuestro archivo.
const GoodsStruct = { $init: { [goods$]: [], [page$]: 1, }, ... } const Goods = withMrr(GoodsStruct, (state, props, $) => { ... }); export { GoodsStruct }
y luego lo importamos en nuestras pruebas. Con un simple envoltorio podemos
coloque el valor en la secuencia (como si se hiciera desde el DOM) y luego verifique los valores de otros hilos que dependen de él.
import { simpleWrapper} from 'mrr'; import { GoodsStruct } from '../src/components/Goods'; describe('Testing Goods component', () => { it('should update page if it\'s out of limit ', () => { const a = simpleWrapper(GoodsStruct); a.set('page', 10); assert.equal(a.get('page'), 10); a.set('requestGoods.success', {data: [], total: 5}); assert.equal(a.get('page'), 5); a.set('requestGoods.success', {data: [], total: 10}); assert.equal(a.get('page'), 5); }) })
Brillo y pobreza de reactividad
Vale la pena señalar que la reactividad es una abstracción de un nivel superior en comparación con la formación "manual" de un estado basado en eventos en el Editor. Facilitar el desarrollo, por un lado, crea oportunidades para dispararte en el pie. Considere este escenario: el usuario va a la página número 5, luego cambia la "categoría" del filtro. Debemos cargar la lista de productos de la categoría seleccionada en la quinta página, pero puede resultar que los productos en esta categoría tengan solo tres páginas. En el caso del backend "estúpido", el algoritmo de nuestras acciones es el siguiente:
- página de datos de solicitud = 5 & category =% category%
- tomar de la respuesta el valor del número de páginas
- si devuelve un número cero de registros, solicite la página más grande disponible
Si tuviéramos que implementar esto en el Editor, tendríamos que crear una gran acción asincrónica con la lógica descrita. En el caso de la reactividad en mrr, no hay necesidad de describir este escenario por separado. Todo ya está contenido en estas líneas:
[requestGoods$]: ['nested', (cb, page, category) => { fetch('https://reqres.in/api/products?page=', page).then(r => r.json()) .then(res => cb('success', res)) .catch(e => cb('error', e)) }, page$, category$, $start$], [totalPages$]: [res => res.total, requestGoods$.success], [page$]: merge(goToPage$, [(a, prev) => a < prev ? a : prev, totalPages$, passive(page$)]),
Si el nuevo valor de totalPages es menor que la página actual, actualizaremos el valor de la página y así iniciaremos una segunda solicitud al servidor.
Pero si nuestra función devuelve el mismo valor, todavía se percibirá como un cambio en la secuencia de la página, seguido de la reculcación de todas las secuencias dependientes. Para evitar esto, mrr tiene un significado especial: omitir. Al devolverlo, señalamos: no se han producido cambios, no es necesario actualizar nada.
import { withMrr, skip } from 'mrr'; [requestGoods$]: nested((cb, page, category) => { fetch('https://reqres.in/api/products?page=', page).then(r => r.json()) .then(res => cb('success', res)) .catch(e => cb('error', e)) }, page$, category$, $start$), [totalPages$]: [res => res.total, requestGoods$.success], [page$]: merge(goToPage$, [(a, prev) => a < prev ? a : skip, totalPages$, passive(page$)]),
Por lo tanto, un pequeño error puede llevarnos a un bucle infinito: si devolvemos no "skip" sino "prev", la celda de la página cambiará y se producirá una segunda solicitud, y así sucesivamente en un círculo. La posibilidad misma de tal situación, por supuesto, no es un "inconveniente defectuoso" de FRP o mrr, ya que la posibilidad de una recursión infinita o un bucle no indica las ideas defectuosas de la programación estructural. Sin embargo, debe entenderse que MRR todavía requiere cierta comprensión del mecanismo de reactividad. Volviendo a la conocida metáfora de los cuchillos, mrr es un cuchillo muy afilado que mejora la eficiencia del trabajo, pero también puede dañar a un trabajador inepto.
Por cierto, debitar mrr es muy fácil sin instalar extensiones:
const GoodsStruct = { $init: { ... }, $log: true, ... }
Simplemente agregue $ log: true a la estructura mrr, y todos los cambios en las celdas se enviarán a la consola, para que pueda ver qué cambios y cómo.
Conceptos como la escucha pasiva o el significado de saltar no son "muletas" específicas: amplían las posibilidades de reactividad para que pueda describir fácilmente toda la lógica de la aplicación sin recurrir a enfoques imperativos. Mecanismos similares están, por ejemplo, en Rx.js, pero su interfaz allí es menos conveniente. : Mrr: FRP
.
Resumen
- FRP, mrr ,
- : ,
- , ,
- , , - ( - !)
- mrr : " , !"
- ,
- , , ( ). !
- : , , , TMTOWTDI: , - .
PS
. , mrr , , :
import useMrr from 'mrr/hooks'; function Foo(props){ const [state, $, connectAs] = useMrr(props, { $init: { counter: 0, }, counter: ['merge', [a => a + 1, '-counter', 'incr'], [a => a - 1, '-counter', 'decr'] ], }); return ( <div> Counter: { state.counter } <button onClick={ $('incr') }>increment</button> <button onClick={ $('decr') }>decrement</button> <Bar {...connectAs('bar')} /> </div> ); }