De nos jours, peu de gens écrivent en Perl, mais la célèbre maxime de Larry Wall "Garder les choses simples faciles et difficiles possible" est devenue la formule généralement acceptée pour une technologie efficace. Elle peut être interprétée sous l'aspect non seulement de la complexité des tâches, mais aussi de l'approche: une technologie idéale devrait, d'une part, permettre le développement rapide d'applications moyennes et petites (y compris "en écriture seule"), d'autre part, fournir des outils pour un développement réfléchi applications complexes, où la fiabilité, la maintenabilité et la structure sont primordiales. Ou même, se traduisant dans le plan humain: être accessible aux Jones, et en même temps satisfaire les demandes des Signyors.
Les éditeurs désormais populaires peuvent être critiqués des deux côtés - prenez au moins le fait que l'écriture d'une fonctionnalité même élémentaire peut entraîner de nombreuses lignes espacées sur plusieurs fichiers - mais nous n'irons pas en profondeur, car beaucoup a déjà été dit à ce sujet.
"Vous êtes censé garder toutes les tables dans une pièce et les chaises dans une autre"
- Juha Paananen, créateur de la bibliothèque Bacon.js, à propos de l'éditeur
La technologie qui sera discutée aujourd'hui n'est pas une solution miracle, mais prétend être plus cohérente avec ces critères.
Mrr est une bibliothèque réactive fonctionnelle qui professe le principe de "tout est flux". Les principaux avantages fournis par l'approche fonctionnelle-réactive dans mrr sont la concision, l'expressivité du code, ainsi qu'une approche unifiée pour les transformations de données synchrones et asynchrones.
À première vue, cela ne ressemble pas à une technologie qui sera facilement accessible aux débutants: le concept de flux peut être difficile à comprendre, il n'est pas si répandu sur le front-end, associé principalement à des bibliothèques aussi stupides que Rx. Et surtout, il n'est pas entièrement clair comment expliquer les flux basés sur le schéma de base «action-réaction-mise à jour DOM». Mais ... nous ne parlerons pas abstraitement des flux! Parlons de choses plus compréhensibles: événements, condition.
Cuisine selon la recette
Sans entrer dans la nature du FRP, nous suivrons un schéma simple pour formaliser le domaine:
- faire une liste de données qui décrit l'état de la page et seront utilisées dans l'interface utilisateur, ainsi que leurs types.
- faire une liste des événements qui se produisent ou sont générés par l'utilisateur sur la page, et les types de données qui seront transmises avec eux
- faire une liste des processus qui se produiront sur la page
- déterminer les interdépendances entre eux.
- Décrire les interdépendances à l'aide d'opérateurs appropriés.
En même temps, nous n'avons besoin de connaître la bibliothèque qu'à la toute dernière étape.
Prenons donc un exemple simplifié d'une boutique en ligne, dans laquelle il existe une liste de produits avec pagination et filtrage par catégorie, ainsi qu'un panier.
Données sur la base desquelles l'interface sera construite:
- liste de produits (tableau)
- catégorie sélectionnée (ligne)
- nombre de pages avec des marchandises (nombre)
- liste des produits qui sont dans le panier (tableau)
- page actuelle (numéro)
- le nombre de produits dans le panier (nombre)
Événements (par «événements», ils signifient uniquement des événements momentanés. Les actions qui se produisent pendant un certain temps - les processus - doivent être décomposées en événements distincts):
- page d'ouverture (vide)
- sélection de catégorie (chaîne)
- ajout de marchandises au panier (objet "marchandises")
- retrait des marchandises du panier (identifiant des marchandises à supprimer)
- aller à la page suivante de la liste des produits (numéro - numéro de page)
Processus: ce sont des actions qui commencent et peuvent se terminer par différents événements à la fois ou après un certain temps. Dans notre cas, ce sera le chargement des données produit depuis le serveur, ce qui peut conduire à deux événements: l'achèvement réussi et l'achèvement avec une erreur.
Interdépendances entre les événements et les données. Par exemple, la liste des produits dépendra de l'événement: "chargement réussi de la liste des produits". Et «commencez à charger la liste des marchandises» - depuis «ouvrir la page», «sélectionner la page actuelle», «sélectionner une catégorie». Faites une liste du formulaire [élément]: [... dépendances]:
{ requestGoods: ['page', 'category', 'pageLoaded'], goods: ['requestGoods.success'], page: ['goToPage', 'totalPages'], totalPages: ['requestGoods.success'], cart: ['addToCart', 'removeFromCart'], goodsInCart: ['cart'], category: ['selectCategory'] }
Oh ... mais c'est presque le code pour mrr!

Il ne reste plus qu'à ajouter des fonctions qui décriront la relation. Vous pourriez vous attendre à ce que les événements, les données et les processus soient des entités différentes dans mrr - mais non, tout cela est des threads! Notre tâche consiste à les connecter correctement.
Comme vous pouvez le voir, nous avons deux types de dépendances: "données" de "l'événement" (par exemple, la page de goToPage) et "données" de la "données" (goodsInCart du panier). Pour chacun d'eux, il existe des approches appropriées.
La manière la plus simple est avec les "données des données": ici nous ajoutons simplement une fonction pure, la "formule":
goodsInCart: [arr => arr.length, 'cart'],
Chaque fois que le tableau de panier est modifié, la valeur de goodsInCart sera recalculée.
Si nos données dépendent d'un événement, alors tout est aussi assez simple:
category: 'selectCategory', goods: [resp => resp.data, 'requestGoods.success'], totalPages: [resp => resp.totalPages, 'requestGoods.success'],
La conception de la forme [fonction, ... thread-arguments] est la base de mrr. Pour une compréhension intuitive, en dessinant une analogie avec Excel, les flux dans mrr sont également appelés cellules, et les fonctions par lesquelles ils sont calculés sont appelées formules.
Si nos données dépendent de plusieurs événements, nous devons transformer leurs valeurs individuellement, puis les combiner en un seul flux à l'aide de l'opérateur de fusion:
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'], ],
Dans les deux cas, nous nous référons à la valeur précédente de la cellule. Pour éviter une boucle infinie, nous nous référons passivement aux cellules du panier et de la page (signe moins devant le nom de la cellule): leurs valeurs seront substituées dans la formule, mais si elles changent, le recalcul ne démarrera pas.
Tous les threads sont construits sur la base d'autres threads ou émis à partir du DOM. Mais qu'en est-il du flux "page d'ouverture"? Heureusement, vous n'avez pas besoin d'utiliser componentDidMount: dans mrr il y a un flux spécial $ start, qui signale que le composant a été créé et monté.
Les "processus" sont calculés de manière asynchrone, tandis que nous en émettons certains événements, l'opérateur "imbriqué" nous aidera ici:
requestGoods: ['nested', (cb, page, category) => { fetch("...") .then(res => cb('success', res)) .catch(e => cb('error', e)); }, 'page', 'category', '$start'],
Lors de l'utilisation de l'opérateur imbriqué, le premier argument nous sera transmis une fonction de rappel pour l'émission de certains événements. Dans ce cas, de l'extérieur, ils seront accessibles via l'espace de noms de la cellule racine, par exemple,
cb('success', res)
à l'intérieur de la formule requestGoods, la mise à jour de la cellule requestGoods.success en résultera.
Pour afficher correctement la page avant le calcul de nos données, vous pouvez spécifier leurs valeurs initiales:
{ goods: [], page: 1, cart: [], },
Ajoutez du balisage. Nous créons un composant React en utilisant la fonction withMrr, qui accepte un diagramme de lien réactif et une fonction de rendu. Afin de "mettre" une valeur dans un flux, nous utilisons la fonction $, qui crée (et met en cache) les gestionnaires d'événements. Maintenant, notre application entièrement fonctionnelle ressemble à ceci:
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;
La construction
<select onChange={$('selectCategory')}>
signifie que lorsque le champ est modifié, la valeur sera "poussée" dans le flux selectCategory. Mais quel est le sens? Par défaut, c'est event.target.value, mais si nous devons pousser autre chose, nous le spécifions avec le deuxième argument, comme ici:
<button onClick={$("addToCart", item)}>
Tout ici - événements, données et processus - ce sont des flux. Le déclenchement d'un événement entraîne un recalcul des données ou des événements qui en dépendent, et ainsi de suite le long de la chaîne. La valeur du flux dépendant est calculée à l'aide d'une formule qui peut renvoyer une valeur ou une promesse (alors mrr attendra sa résolution).
L'API mrr est très concise et concise - dans la plupart des cas, nous n'avons besoin que de 3-4 opérateurs de base, et beaucoup de choses peuvent être faites sans eux. Ajoutez un message d'erreur lorsque la liste des produits n'est pas chargée avec succès, qui s'affichera pendant une seconde:
hideErrorMessage: [() => new Promise(res => setTimeout(res, 1000)), 'requestGoods.error'], errorMessageShown: [ 'merge', [() => true, 'requestGoods.error'], [() => false, 'hideErrorMessage'], ],
Sel, poivre sucre au goût
Il y a aussi du sucre syntaxique dans mrr, qui est facultatif pour le développement, mais peut l'accélérer. Par exemple, l'opérateur bascule:
errorMessageShown: ['toggle', 'requestGoods.error', [() => new Promise(res => setTimeout(res, 1000)), 'showErrorMessage']],
Un changement dans le premier argument définira la cellule sur true et dans le second sur false.
L'approche de "décomposition" des résultats d'une tâche asynchrone en sous-cellules de réussite et d'erreur est également si répandue que vous pouvez utiliser l'opérateur de promesse spécial (qui élimine automatiquement la condition de concurrence):
requestGoods: [ 'promise', (page = 1, category = 'all') => fetch('https://reqres.in/api/products?page=', page, category).then(r => r.json()), 'page', 'selectCategory', '$start' ],
Beaucoup de fonctionnalités tiennent dans quelques dizaines de lignes. Notre juin conditionnel est satisfait - il a réussi à écrire du code de travail, qui s'est avéré assez compact: toute la logique tient dans un seul fichier et sur un seul écran. Mais le signataire plisse les yeux incrédule: Eka est invisible ... vous pouvez écrire ceci sur des crochets / recomposer / etc.
Oui, bien sûr! Le code, bien sûr, est peu susceptible d'être encore plus compact et structuré, mais ce n'est pas le but. Imaginons que le projet se développe, et nous devons diviser la fonctionnalité en deux pages distinctes: une liste de produits et un panier. De plus, les données du panier doivent évidemment être stockées globalement pour les deux pages.
Une approche, une interface
Nous arrivons ici à un autre problème de développement des réactions: l'existence d'approches hétérogènes pour gérer l'état localement (au sein d'un composant) et globalement au niveau de l'application entière. Beaucoup, j'en suis sûr, étaient confrontés à un dilemme: mettre en œuvre une logique localement ou globalement? Ou une autre situation: il s'est avéré que certaines données locales doivent être enregistrées globalement, et vous devez réécrire une partie de la fonctionnalité, par exemple, de recomposer à l'éditeur ...
Le contraste est, bien sûr, artificiel, et en mrr il ne l'est pas: il est tout aussi bon, et surtout - uniforme! - Convient à la fois pour la gestion de l'état local et mondial. En général, nous n'avons besoin d'aucun état global, nous avons juste la possibilité d'échanger des données entre les composants, donc l'état du composant racine sera "global".
Le schéma de notre application est maintenant le suivant: le composant racine contenant la liste des marchandises dans le panier, et deux sous-composants: les marchandises et le panier, et le composant global "écoute" les flux "ajouter au panier" et "retirer du panier" des composants enfants.
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>); });
C'est incroyable comme peu de choses ont changé! Nous avons simplement disposé les flux dans les composants correspondants et posé des «ponts» entre eux! En connectant les composants à l'aide de la fonction mrrConnect, nous spécifions le mappage pour les flux en aval et en amont:
connectAs( 'goods', ['addToCart', 'removeFromCart'], ['cart'] )
Ici, les flux addToCart et removeFromCart du composant enfant iront au parent et le flux du panier reviendra. Nous ne sommes pas tenus d'utiliser les mêmes noms de flux - s'ils ne correspondent pas, nous utilisons le mappage:
connectAs('cart', { 'removeFromCart': 'remove' })
Le flux de suppression du composant enfant sera la source du flux removeFromCart dans le parent.
Comme vous pouvez le voir, le problème du choix d'un emplacement de stockage de données dans le cas de mrr est complètement supprimé: vous stockez les données là où elles sont déterminées logiquement.
Là encore, on ne peut manquer de noter l'inconvénient de l'éditeur: vous devez y sauvegarder toutes les données dans un seul référentiel central. Même les données qui peuvent être demandées et utilisées par un seul composant distinct ou son sous-arbre! Si nous écrivions dans le "style éditorial", nous porterions également le chargement et la pagination des marchandises au niveau mondial (en toute équité - cette approche, grâce à la flexibilité de mrr, est également possible et a droit à la vie, code source ).
Cependant, ce n'est pas nécessaire. Les marchandises chargées ne sont utilisées que dans la composante des marchandises, par conséquent, en les portant au niveau mondial, nous obstruons et gonflons uniquement l'état mondial. De plus, nous devrons effacer les données obsolètes (par exemple, une page de pagination) lorsque l'utilisateur reviendra sur la page du produit. En choisissant le bon niveau de stockage des données, nous évitons automatiquement ces problèmes.
Un autre avantage de cette approche est que la logique d'application est combinée avec la présentation, ce qui nous permet de réutiliser des composants React individuels en tant que widgets entièrement fonctionnels, plutôt qu'en tant que modèles «stupides». De plus, en gardant un minimum d'informations au niveau global (idéalement, ce ne sont que des données de session) et en retirant la majeure partie de la logique dans des composants de page séparés, nous réduisons considérablement la cohérence du code. Bien sûr, cette approche n'est pas toujours applicable, mais il existe un grand nombre de tâches où l'état global est extrêmement petit et les «écrans» individuels sont presque complètement indépendants les uns des autres: par exemple, différents types d'administrateurs, etc. Contrairement à l'éditeur, qui nous incite à prendre tout ce qui est nécessaire et non nécessaire au niveau mondial, mrr vous permet de stocker des données dans des sous-arbres séparés, encourageant et rendant l'encapsulation possible, transformant ainsi notre application d'un "gâteau" monolithique en un "gâteau" en couches.
Il convient de le mentionner: bien sûr, il n'y a rien de révolutionnaire dans l'approche proposée! Les composants, widgets autonomes, sont l'une des approches de base utilisées depuis l'avènement des frameworks js. La seule différence significative est que mrr suit le principe déclaratif: les composants ne peuvent écouter que les flux des autres composants, mais ne peuvent pas les influencer (que doit-il faire de bas en haut, ou de haut en bas, qui diffère du flux approche). Les composants intelligents qui ne peuvent échanger des messages qu'avec les composants sous-jacents et parents correspondent au modèle d'acteur populaire mais peu connu dans le développement frontal (le sujet de l'utilisation des acteurs et des threads sur le frontal est bien couvert dans l'article Introduction à la programmation réactive ).
Bien sûr, cela est loin de la mise en œuvre canonique des acteurs, mais l'essence est exactement la suivante: le rôle des acteurs est joué par des composants échangeant des messages via des flux MPP; un composant peut (de manière déclarative!) créer et supprimer des acteurs de composants enfants grâce au DOM virtuel et React: la fonction de rendu, par essence, détermine la structure des acteurs enfants.
Au lieu de la situation standard pour React, lorsque nous «déposons» le composant parent dans un certain rappel via des accessoires, nous devons écouter le flux du composant enfant depuis le parent. La même chose va dans le sens opposé, du parent à l'enfant. Par exemple, vous pouvez vous demander: pourquoi transférer les données du panier du panier vers le composant Cart en tant que flux, si nous pouvons, sans plus tarder, simplement les transmettre comme accessoires? Quelle est la différence? En effet, cette approche peut également être utilisée, mais seulement jusqu'à ce qu'il soit nécessaire de répondre aux changements d'accessoires. Si vous avez déjà utilisé la méthode componentWillReceiveProps, vous savez de quoi il s'agit. C'est une sorte de «réactivité pour les pauvres»: vous écoutez absolument tous les changements d'accessoires, déterminez ce qui a changé et réagissez. Mais cette méthode disparaîtra bientôt de React, et le besoin d'une réaction aux «signaux d'en haut» pourrait survenir.
Dans mrr, les flux «coulent» non seulement vers le haut, mais aussi vers le bas de la hiérarchie des composants, de sorte que les composants peuvent répondre indépendamment aux changements d'état. Ce faisant, vous pouvez utiliser toute la puissance des outils réactifs mrr.
const Cart = withMrr({ foo: [items => {
Ajoutez un peu de bureaucratie
Le projet grandit, il devient difficile de garder une trace des noms des flux qui sont - oh, horreur! - sont stockés en rangées. Eh bien, nous pouvons utiliser des constantes pour les noms de flux, ainsi que pour les instructions mrr. Désormais, casser une application en faisant une petite faute de frappe devient plus difficile.
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'y a-t-il dans la boîte noire?
Et les tests? La logique décrite dans le composant mrr est facile à séparer du modèle, puis à tester.
Faisons la structure mrr séparément de notre fichier.
const GoodsStruct = { $init: { [goods$]: [], [page$]: 1, }, ... } const Goods = withMrr(GoodsStruct, (state, props, $) => { ... }); export { GoodsStruct }
puis nous l'importons dans nos tests. Avec un simple emballage, nous pouvons
placez la valeur dans le flux (comme si cela avait été fait à partir du DOM), puis vérifiez les valeurs des autres threads qui en dépendent.
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); }) })
Brillance et pauvreté de réactivité
Il convient de noter que la réactivité est une abstraction d'un niveau supérieur par rapport à la formation d'état "manuelle" basée sur les événements de l'éditeur. Facilitant le développement, d'une part, il crée des opportunités de se tirer une balle dans le pied. Considérez ce scénario: l'utilisateur va à la page numéro 5, puis bascule le filtre "catégorie". Nous devons charger la liste des produits de la catégorie sélectionnée sur la cinquième page, mais il peut s'avérer que les produits de cette catégorie n'auront que trois pages. Dans le cas du backend "stupide", l'algorithme de nos actions est le suivant:
- page de données de demande = 5 & catégorie =% catégorie%
- prendre de la réponse la valeur du nombre de pages
- si renvoyé un nombre zéro d'enregistrements, demandez la plus grande page disponible
Si nous devions l'implémenter sur l'éditeur, nous devrions créer une grande action asynchrone avec la logique décrite. Dans le cas de la réactivité sur mrr, il n'est pas nécessaire de décrire ce scénario séparément. Tout est déjà contenu dans ces lignes:
[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 la nouvelle valeur totalPages est inférieure à la page actuelle, nous mettrons à jour la valeur de la page et lancerons ainsi une deuxième demande au serveur.
Mais si notre fonction retourne la même valeur, elle sera toujours perçue comme un changement dans le flux de page, suivi d'une rétrécissement de tous les flux dépendants. Pour éviter cela, mrr a une signification spéciale - sauter. En le retournant, nous signalons: aucun changement n'est survenu, rien n'a besoin d'être mis à jour.
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$)]),
Ainsi, une petite erreur peut nous conduire à une boucle infinie: si nous retournons non "skip", mais "prev", la cellule de la page changera et une seconde requête se produira, et ainsi de suite dans un cercle. La possibilité même d'une telle situation, bien sûr, n'est pas un «inconvénient imparfait» du FRP ou du mrr, car la possibilité d'une récursion infinie ou d'une boucle n'indique pas les idées erronées de la programmation structurelle. Cependant, il faut comprendre que mrr nécessite encore une certaine compréhension du mécanisme de réactivité. Revenant à la métaphore bien connue des couteaux, mrr est un couteau très tranchant qui améliore l'efficacité du travail, mais peut également blesser un travailleur inepte.
Soit dit en passant, le débit de mrr est très facile sans installer d'extensions:
const GoodsStruct = { $init: { ... }, $log: true, ... }
Ajoutez simplement $ log: true à la structure mrr, et toutes les modifications apportées aux cellules seront sorties sur la console, afin que vous puissiez voir quelles modifications et comment.
Des concepts tels que l'écoute passive ou la signification de skip ne sont pas des «béquilles» spécifiques: ils élargissent les possibilités de réactivité afin qu'il puisse décrire facilement toute la logique de l'application sans recourir à des approches impératives. Des mécanismes similaires sont, par exemple, dans Rx.js, mais leur interface y est moins pratique. : Mrr: FRP
.
Résumé
- 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> ); }