Atualmente, poucas pessoas escrevem em Perl, mas a famosa máxima de Larry Wall, "Mantenha as coisas simples, fáceis e difíceis possíveis" se tornou a fórmula geralmente aceita para uma tecnologia eficaz. Ela pode ser interpretada no aspecto não apenas da complexidade das tarefas, mas também da abordagem: uma tecnologia ideal deve, por um lado, permitir o rápido desenvolvimento de aplicativos de médio e pequeno porte (incluindo "somente gravação"), por outro lado, fornecer ferramentas para um desenvolvimento criterioso aplicações complexas, onde confiabilidade, manutenção e estrutura são fundamentais. Ou ainda, traduzindo para o plano humano: estar acessível aos Jones e, ao mesmo tempo, satisfazer os pedidos dos Signatários.
Os editores agora populares podem ser criticados de ambos os lados - pelo menos o fato de que escrever funcionalidades elementares pode resultar em muitas linhas espaçadas em vários arquivos - mas não vamos nos aprofundar, porque muito já foi dito sobre isso.
"Você deveria manter todas as mesas em uma sala e as cadeiras em outra"
- Juha Paananen, criadora da biblioteca Bacon.js, sobre o Editor
A tecnologia que será discutida hoje não é uma bala de prata, mas afirma ser mais consistente com esses critérios.
Mrr é uma biblioteca reativa funcional que professa o princípio de "tudo é fluxo". As principais vantagens oferecidas pela abordagem funcional-reativa no mrr são concisão, expressividade do código, bem como uma abordagem unificada para transformações de dados síncronos e assíncronos.
À primeira vista, isso não soa como uma tecnologia que será facilmente acessível para iniciantes: o conceito de fluxo pode ser difícil de entender, não é tão difundido no front-end, associado principalmente a bibliotecas idiotas como Rx. E o mais importante, não está totalmente claro como explicar os fluxos com base no esquema básico "DOM de ação-reação-atualização". Mas ... não falaremos abstratamente sobre fluxos! Vamos falar de coisas mais compreensíveis: eventos, condição.
Cozinhando de acordo com a receita
Sem entrar na natureza do FRP, seguiremos um esquema simples para formalizar a área de assunto:
- faça uma lista dos dados que descrevem o estado da página e serão usados na interface do usuário, bem como seus tipos.
- faça uma lista dos eventos que ocorrem ou são gerados pelo usuário na página e os tipos de dados que serão transmitidos com eles
- faça uma lista dos processos que ocorrerão na página
- determinar as interdependências entre eles.
- Descreva interdependências usando operadores apropriados.
Ao mesmo tempo, precisamos conhecer a biblioteca apenas no último estágio.
Então, vamos dar um exemplo simplificado de uma loja virtual, na qual há uma lista de produtos com paginação e filtragem por categoria, além de uma cesta.
Dados com base nos quais a interface será construída:
- lista de mercadorias (matriz)
- categoria selecionada (linha)
- número de páginas com mercadorias (número)
- lista de produtos que estão na cesta (matriz)
- página atual (número)
- o número de produtos na cesta (número)
Eventos (por "eventos", eles significam apenas eventos momentâneos. Ações que ocorrem por um tempo - processos - precisam ser decompostas em eventos separados):
- página de abertura (nula)
- seleção de categoria (string)
- adicionando mercadorias à cesta (objeto "mercadorias")
- remoção de mercadorias da cesta (identificação das mercadorias a serem excluídas)
- vá para a próxima página da lista de produtos (número - número da página)
Processos: são ações que começam e podem terminar com diferentes eventos de uma só vez ou depois de algum tempo. No nosso caso, esse será o carregamento dos dados do produto do servidor, o que pode acarretar dois eventos: conclusão bem-sucedida e conclusão com erro.
Interdependências entre eventos e dados. Por exemplo, a lista de produtos dependerá do evento: "carregamento bem-sucedido da lista de produtos". E “comece a carregar a lista de mercadorias” - de “abrir a página”, “selecionar a página atual”, “selecionar uma categoria”. Faça uma lista do formulário [elemento]: [... dependências]:
{ requestGoods: ['page', 'category', 'pageLoaded'], goods: ['requestGoods.success'], page: ['goToPage', 'totalPages'], totalPages: ['requestGoods.success'], cart: ['addToCart', 'removeFromCart'], goodsInCart: ['cart'], category: ['selectCategory'] }
Ah ... mas esse é quase o código para o sr.

Resta apenas adicionar funções que descrevam o relacionamento. Você pode esperar que eventos, dados e processos sejam entidades diferentes no sr. - mas não, tudo isso são threads! Nossa tarefa é conectá-los corretamente.
Como você pode ver, temos dois tipos de dependências: "dados" do "evento" (por exemplo, página do goToPage) e "dados" dos "dados" (goodsInCart do carrinho). Para cada um deles existem abordagens apropriadas.
A maneira mais fácil é com "dados de dados": aqui simplesmente adicionamos uma função pura, a "fórmula":
goodsInCart: [arr => arr.length, 'cart'],
Cada vez que a matriz do carrinho é alterada, o valor de goodsInCart será recalculado.
Se nossos dados dependem de um evento, tudo também é bem simples:
category: 'selectCategory', goods: [resp => resp.data, 'requestGoods.success'], totalPages: [resp => resp.totalPages, 'requestGoods.success'],
O design do formulário [função, ... argumentos de thread] é a base do sr. Para um entendimento intuitivo, traçando uma analogia com o Excel, os fluxos em mrr também são chamados de células e as funções pelas quais são calculados são chamadas de fórmulas.
Se nossos dados dependem de vários eventos, devemos transformar seus valores individualmente e combiná-los em um fluxo usando o operador de mesclagem:
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'], ],
Nos dois casos, nos referimos ao valor anterior da célula. Para evitar um loop infinito, nos referimos às células do carrinho e da página passivamente (o sinal de menos na frente do nome da célula): seus valores serão substituídos na fórmula, mas se eles mudarem, o recálculo não será iniciado.
Todos os threads são criados com base em outros threads ou emitidos a partir do DOM. Mas e o fluxo da "página de abertura"? Felizmente, você não precisa usar o componentDidMount: no sr. Há um fluxo especial $ start, que indica que o componente foi criado e montado.
Os "processos" são calculados de forma assíncrona, enquanto emitimos certos eventos a partir deles, o operador "aninhado" nos ajudará aqui:
requestGoods: ['nested', (cb, page, category) => { fetch("...") .then(res => cb('success', res)) .catch(e => cb('error', e)); }, 'page', 'category', '$start'],
Ao usar o operador aninhado, o primeiro argumento nos passa uma função de retorno de chamada para a emissão de determinados eventos. Nesse caso, do lado de fora eles estarão acessíveis através do espaço de nomes da célula raiz, por exemplo,
cb('success', res)
dentro da fórmula requestGoods, ocorrerá a atualização da célula requestGoods.success.
Para renderizar a página corretamente antes que nossos dados sejam calculados, você pode especificar seus valores iniciais:
{ goods: [], page: 1, cart: [], },
Adicione marcação. Criamos um componente React usando a função withMrr, que aceita um diagrama de link reativo e a função render. Para "colocar" um valor em um fluxo, usamos a função $, que cria (e armazena em cache) manipuladores de eventos. Agora, nosso aplicativo totalmente funcional fica assim:
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;
Construção civil
<select onChange={$('selectCategory')}>
significa que quando o campo for alterado, o valor será "enviado" ao fluxo selectCategory. Mas qual é o significado? Por padrão, este é event.target.value, mas se precisarmos enviar outra coisa, especificamos com o segundo argumento, como aqui:
<button onClick={$("addToCart", item)}>
Tudo aqui - eventos, dados e processos - são fluxos. O acionamento de um evento causa um recálculo de dados ou eventos, dependendo dele, e assim por diante ao longo da cadeia. O valor do fluxo dependente é calculado usando uma fórmula que pode retornar um valor ou uma promessa (então mrr aguardará sua resolução).
A API do mrr é muito concisa e concisa - na maioria dos casos, precisamos apenas de 3 a 4 operadores básicos, e muitas coisas podem ser feitas sem eles. Adicione uma mensagem de erro quando a lista de produtos não for carregada com sucesso, que será exibida por um segundo:
hideErrorMessage: [() => new Promise(res => setTimeout(res, 1000)), 'requestGoods.error'], errorMessageShown: [ 'merge', [() => true, 'requestGoods.error'], [() => false, 'hideErrorMessage'], ],
Sal, pimenta açúcar a gosto
Há também açúcar sintático no srr, que é opcional para o desenvolvimento, mas pode acelerar. Por exemplo, o operador de alternância:
errorMessageShown: ['toggle', 'requestGoods.error', [() => new Promise(res => setTimeout(res, 1000)), 'showErrorMessage']],
Uma alteração no primeiro argumento definirá a célula como true e no segundo como false.
A abordagem para "decompor" os resultados de uma tarefa assíncrona em subcélulas de sucesso e erro também é tão difundida que você pode usar o operador de promessa especial (que elimina automaticamente a condição de corrida):
requestGoods: [ 'promise', (page = 1, category = 'all') => fetch('https://reqres.in/api/products?page=', page, category).then(r => r.json()), 'page', 'selectCategory', '$start' ],
Muita funcionalidade cabe em apenas algumas dezenas de linhas. Nosso junho condicional está satisfeito - ele conseguiu escrever um código funcional, que acabou sendo bastante compacto: toda a lógica se encaixava em um arquivo e em uma tela. Mas o signor olha com incredulidade: Eka é invisível ... você pode escrever isso em ganchos / recompor / etc.
Sim, de fato é! É improvável que o código seja ainda mais compacto e estruturado, mas esse não é o ponto. Vamos imaginar que o projeto esteja em desenvolvimento e precisamos dividir a funcionalidade em duas páginas separadas: uma lista de produtos e uma cesta. Além disso, a cesta de dados, obviamente, deve ser armazenada globalmente nas duas páginas.
Uma abordagem, uma interface
Aqui chegamos a outro problema do desenvolvimento de reações: a existência de abordagens heterogêneas para gerenciar o estado localmente (dentro de um componente) e globalmente no nível de todo o aplicativo. Muitos, tenho certeza, enfrentaram um dilema: implementar alguma lógica local ou globalmente? Ou outra situação: verificou-se que alguns dados locais precisam ser salvos globalmente, e você deve reescrever parte da funcionalidade, por exemplo, de recompor para editor ...
O contraste é, é claro, artificial, e, no caso, o senhor não é: é igualmente bom e, o mais importante - uniforme! - Adequado para gerenciamento de estado local e global. Em geral, não precisamos de nenhum estado global, apenas temos a capacidade de trocar dados entre os componentes, para que o estado do componente raiz seja "global".
O esquema de nossa aplicação agora é o seguinte: o componente raiz que contém a lista de mercadorias na cesta e dois subcomponentes: as mercadorias e a cesta e o componente global "escutam" os fluxos "adicionar à cesta" e "remover da cesta" dos componentes filhos.
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>); });
É incrível como pouco mudou! Simplesmente colocamos os fluxos nos componentes correspondentes e colocamos "pontes" entre eles! Ao conectar os componentes usando a função mrrConnect, especificamos o mapeamento para fluxos a jusante e a montante:
connectAs( 'goods', ['addToCart', 'removeFromCart'], ['cart'] )
Aqui, os fluxos addToCart e removeFromCart do componente filho irão para o pai e o fluxo do carrinho voltará. Não somos obrigados a usar os mesmos nomes de stream - se eles não corresponderem, usamos o mapeamento:
connectAs('cart', { 'removeFromCart': 'remove' })
O fluxo de remoção do componente filho será a fonte do fluxo removeFromCart no pai.
Como você pode ver, o problema de escolher um local de armazenamento de dados no caso de mrr é completamente removido: você armazena dados onde eles são determinados logicamente.
Aqui, novamente, não se pode deixar de notar a desvantagem do Editor: nele você deve salvar todos os dados em um único repositório central. Até os dados que podem ser solicitados e usados por apenas um componente separado ou sua subárvore! Se escrevêssemos no "estilo editorial", também levaríamos o carregamento e a paginação de mercadorias para o nível global (para ser justo - essa abordagem, graças à flexibilidade do srr, também é possível e tem direito à vida, código fonte ).
No entanto, isso não é necessário. As mercadorias carregadas são usadas apenas no componente de mercadorias, portanto, levando-as ao nível global, apenas entupimos e inflamos o estado global. Além disso, teremos que limpar dados desatualizados (por exemplo, uma página de paginação) quando o usuário retornar à página do produto novamente. Escolhendo o nível certo de armazenamento de dados, evitamos automaticamente esses problemas.
Outra vantagem dessa abordagem é que a lógica do aplicativo é combinada com a apresentação, o que nos permite reutilizar componentes individuais do React como widgets totalmente funcionais, em vez de modelos "burros". Além disso, mantendo um mínimo de informações em nível global (idealmente, são apenas dados da sessão) e retirando a maior parte da lógica em componentes de página separados, reduzimos bastante a coerência do código. Obviamente, essa abordagem nem sempre é aplicável, mas há um grande número de tarefas em que o estado global é extremamente pequeno e as “telas” individuais são quase completamente independentes uma da outra: por exemplo, vários tipos de administradores, etc. Ao contrário do Editor, que nos leva a levar tudo o que é necessário e não necessário para o nível global, o srr permite armazenar dados em subárvores separadas, incentivando e possibilitando o encapsulamento, de modo que nosso aplicativo passa de um "bolo" monolítico para um "bolo" em camadas.
Vale ressaltar: é claro que não há nada de novo e revolucionário na abordagem proposta! Componentes, widgets independentes, têm sido uma das abordagens básicas usadas desde o advento das estruturas js. A única diferença significativa é que o srr segue o princípio declarativo: os componentes só podem ouvir os fluxos de outros componentes, mas não podem influenciá-los (o que é necessário fazer a partir da direção de baixo para cima ou da direção de cima para baixo, que difere do fluxo abordagem). Componentes inteligentes que só podem trocar mensagens com os componentes subjacente e pai correspondem ao modelo de ator popular, porém pouco conhecido, no desenvolvimento do front-end (o tópico sobre o uso de atores e encadeamentos no front-end é bem abordado no artigo Introdução à programação reativa ).
Obviamente, isso está longe de ser a implementação canônica dos atores, mas a essência é exatamente isso: o papel dos atores é desempenhado pelos componentes que trocam mensagens através dos fluxos MPP; um componente pode (declarativamente!) criar e excluir atores componentes filhos, graças ao DOM virtual e React: a função render, em essência, determina a estrutura dos atores filhos.
Em vez da situação padrão para o React, quando "soltamos" o componente pai em um determinado retorno de chamada por meio de adereços, devemos ouvir o fluxo do componente filho do pai. A mesma coisa está na direção oposta, de pai para filho. Por exemplo, você pode perguntar: por que transferir os dados do carrinho de compras para o componente Carrinho como um fluxo, se podemos, sem mais delongas, simplesmente passá-los como acessórios? Qual a diferença? De fato, essa abordagem também pode ser usada, mas apenas até que haja uma necessidade de responder a alterações nos adereços. Se você já usou o método componentWillReceiveProps, sabe do que se trata. Esse é um tipo de "reatividade para os pobres": você ouve absolutamente todas as alterações de adereços, determina o que mudou e reage. Mas esse método logo desaparecerá do React, e a necessidade de uma reação aos "sinais de cima" pode surgir.
Em mrr, os fluxos “fluem” não apenas para cima, mas também para baixo na hierarquia de componentes, para que os componentes possam responder independentemente às mudanças de estado. Ao fazer isso, você pode usar todo o poder das ferramentas reativas de mrr.
const Cart = withMrr({ foo: [items => {
Adicione um pouco de burocracia
O projeto está crescendo, fica difícil acompanhar os nomes dos fluxos, que são - ah, horror! - são armazenados em linhas. Bem, podemos usar constantes para nomes de fluxos, bem como para instruções mrr. Agora, a quebra de um aplicativo com pequenos erros de digitação se torna mais 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]), }, ...);
O que há na caixa preta?
E quanto a testes? A lógica descrita no componente mrr é fácil de separar do modelo e depois testar.
Vamos criar a estrutura do srr separadamente do nosso arquivo.
const GoodsStruct = { $init: { [goods$]: [], [page$]: 1, }, ... } const Goods = withMrr(GoodsStruct, (state, props, $) => { ... }); export { GoodsStruct }
e depois a importamos em nossos testes. Com um invólucro simples, podemos
coloque o valor no fluxo (como se tivesse sido feito no DOM) e verifique os valores de outros segmentos dependentes dele.
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); }) })
Brilho e pobreza de reatividade
Vale ressaltar que a reatividade é uma abstração de um nível mais alto em comparação com a formação “manual” de um estado com base em eventos no Editor. Facilitar o desenvolvimento, por um lado, cria oportunidades para dar um tiro no próprio pé. Considere este cenário: o usuário acessa a página número 5 e depois muda o filtro "categoria". É necessário carregar a lista de produtos da categoria selecionada na quinta página, mas pode ser que os produtos dessa categoria tenham apenas três páginas. No caso do back-end "estúpido", o algoritmo de nossas ações é o seguinte:
- página de dados da solicitação = 5 & category =% category%
- retire da resposta o valor do número de páginas
- se retornou um número zero de registros, solicite a maior página disponível
Se implementássemos isso no Editor, teríamos que criar uma grande ação assíncrona com a lógica descrita. No caso de reatividade em mrr, não há necessidade de descrever esse cenário separadamente. Tudo já está contido nestas linhas:
[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$)]),
Se o novo valor totalPages for menor que a página atual, atualizaremos o valor da página e, assim, iniciaremos uma segunda solicitação para o servidor.
Porém, se nossa função retornar o mesmo valor, ainda será percebida como uma alteração no fluxo de páginas, seguida pela reculcação de todos os fluxos dependentes. Para evitar isso, o sr. Tem um significado especial - pular. Retornando, sinalizamos: nenhuma alteração ocorreu, nada precisa ser atualizado.
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$)]),
Assim, um pequeno erro pode nos levar a um loop infinito: se retornarmos não "pular", mas "prev", a célula da página mudará e uma segunda solicitação ocorrerá, e assim por diante em um círculo. A própria possibilidade de tal situação, é claro, não é uma “desvantagem defeituosa” do FRP ou mrr, pois a possibilidade de recursão infinita ou um loop não indica as idéias defeituosas da programação estrutural. No entanto, deve-se entender que o srr ainda requer alguma compreensão do mecanismo de reatividade. Voltando à conhecida metáfora das facas, o sr. É uma faca muito afiada que melhora a eficiência do trabalho, mas também pode ferir um trabalhador inepto.
A propósito, debitar mrr é muito fácil sem instalar nenhuma extensão:
const GoodsStruct = { $init: { ... }, $log: true, ... }
Basta adicionar $ log: true à estrutura mrr e todas as alterações nas células serão exibidas no console, para que você possa ver quais alterações e como.
Conceitos como escuta passiva ou o significado de pular não são “muletas” específicas: eles expandem as possibilidades de reatividade para que ele possa descrever facilmente toda a lógica do aplicativo sem recorrer a abordagens imperativas. Mecanismos semelhantes estão, por exemplo, no Rx.js, mas sua interface é menos conveniente. : Mrr: FRP
.
Sumário
- 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> ); }