Nós bombeamos ganchos React usando FRP

Tendo dominado os ganchos, muitos desenvolvedores do React experimentaram euforia, obtendo finalmente um kit de ferramentas simples e conveniente que permite implementar tarefas com significativamente menos código. Mas isso significa que os ganchos useState e useReducer padrão oferecidos imediatamente são tudo o que precisamos para gerenciar o estado?


Na minha opinião, em sua forma bruta, seu uso não é muito conveniente, é mais provável que eles sejam considerados a base para a criação de ganchos de gerenciamento de estado realmente convenientes. Os próprios desenvolvedores de reação incentivam fortemente o desenvolvimento de ganchos personalizados, então por que não fazê-lo? Sob o corte, veremos um exemplo muito simples e compreensível, o que há de errado com os ganchos comuns e como eles podem ser melhorados, tanto que eles se recusam completamente a usá-los em sua forma pura.


Existe um determinado campo para entrada, condicionalmente, de um nome. E há um botão clicando no qual devemos fazer uma solicitação ao servidor com o nome digitado (uma determinada pesquisa). Parece que poderia ser mais fácil? No entanto, a solução está longe de ser óbvia. A primeira implementação ingênua:


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>; } 

O que há de errado aqui? Se o usuário, digitando algo no campo, enviar o formulário duas vezes, apenas a primeira solicitação funcionará para nós, porque no segundo clique, a solicitação não será alterada e useEffect não funcionará. Se imaginarmos que nosso aplicativo é um serviço de pesquisa de tickets, e o usuário poderá, em alguns intervalos, enviar o formulário repetidamente sem fazer alterações, essa implementação não funcionará para nós! Usar o nome como uma dependência para useEffect também é inaceitável; caso contrário, o formulário será enviado imediatamente quando o texto for alterado. Bem, você tem que mostrar engenhosidade.


 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>; } 

Agora, a cada clique, mudaremos o significado da solicitação para o oposto, o que atingirá o comportamento desejado. É uma muleta muito pequena e inocente, mas torna o código um pouco confuso de entender. Talvez agora pareça a você que sugo o problema do meu dedo e inflo sua escala. Bem, para responder se é verdade ou não, você precisa comparar esse código com outras implementações que oferecem uma abordagem mais expressiva.


Vejamos este exemplo em um nível teórico usando a abstração de threads. É muito conveniente para descrever o status das interfaces do usuário. Portanto, temos dois fluxos: dados inseridos no campo de texto (nome $) e um fluxo de cliques no botão enviar do formulário (clique em $). A partir deles, precisamos criar um terceiro fluxo combinado de solicitações para o servidor.


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

Aqui está o comportamento que precisamos alcançar. Cada fluxo possui dois aspectos: o valor que possui e o momento em que os valores fluem através dele. Em várias situações, podemos precisar de um ou outro aspecto, ou ambos. Você pode comparar isso com o ritmo e a harmonia da música. Fluxos para os quais apenas o tempo de resposta é essencial também são chamados de sinais.


No nosso caso, clicar em $ é um sinal puro: não importa qual valor flui através dele (indefinido / verdadeiro / Evento / o que for), só é importante quando isso acontece. Nome do caso $
o oposto: suas mudanças de modo algum implicam mudanças no sistema, mas podemos precisar do seu significado em algum momento. E a partir desses dois fluxos, precisamos criar o terceiro, tirando da primeira vez, do segundo valor.


No caso do Rxjs, temos um operador quase pronto para isso:


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

No entanto, o uso prático de Rx no React pode ser bastante inconveniente. Uma opção mais adequada é a biblioteca mrr , construída com os mesmos princípios reativos funcionais que o Rx, mas especialmente adaptada para uso com o React no princípio de "reatividade total" e conectado como um 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>; } 

A interface useMrr é semelhante a useState ou useReducer: retorna um objeto de estado (valores de todos os threads) e um setter para inserir valores em threads. Mas por dentro, tudo é um pouco diferente: cada campo de estado (= fluxo), exceto aqueles em que colocamos valores diretamente dos eventos DOM, é descrito por uma função e uma lista de threads pai, cuja alteração fará com que o filho seja recalculado. Nesse caso, os valores dos encadeamentos pai serão substituídos na função Se queremos apenas obter o valor do fluxo, mas não responder à sua alteração, escrevemos um "menos" na frente do nome, como no caso do nome.


Temos o comportamento desejado, em essência, em uma linha. Mas isso não é apenas brevidade. Vamos comparar os resultados obtidos com mais detalhes e, em primeiro lugar, com relação a um parâmetro como legibilidade e clareza do código resultante.


Em mrr, você poderá separar quase completamente a "lógica" do "modelo": não será necessário escrever manipuladores imperativos complexos em JSX. Tudo é extremamente declarativo: simplesmente mapeamos o evento DOM para o fluxo correspondente, praticamente sem conversão (para campos de entrada, o valor e.target.value é extraído automaticamente, a menos que você especifique o contrário), e já na estrutura useMrr, descrevemos como os fluxos base são formados subsidiárias. Assim, no caso de transformações de dados síncronas e assíncronas, sempre podemos rastrear facilmente como nosso valor é formado.


Comparando com Px: nem precisávamos usar operadores adicionais: se, como resultado, as funções de srr receberem uma promessa, ele aguardará automaticamente até que seja resolvido e coloque os dados recebidos no fluxo. Além disso, em vez de withLatestFrom, usamos
escuta passiva (sinal de menos), o que é mais conveniente. Imagine que, além do nome, precisaremos enviar outros campos. Em seguida, no sr. Adicionaremos outro fluxo de escuta passiva:


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

E no Rx, você precisa esculpir mais um com o Último de um mapa ou combinar primeiro o nome e o sobrenome em um fluxo.


Mas voltando aos ganchos e mrr. Um registro mais legível de dependências, que sempre mostra como os dados são formados, é talvez uma das principais vantagens. A interface atual useEffect basicamente não permite responder a fluxos de sinais, e é por isso que
Eu tenho que pensar em diferentes reviravoltas.


Outro ponto é que a opção de ganchos comuns carrega renderizações extras. Se o usuário acabou de clicar no botão, isso ainda não implica nenhuma alteração na interface do usuário que a reação precise desenhar. No entanto, uma renderização será chamada. Na variante com mrr, o estado retornado será atualizado apenas quando uma resposta do servidor já tiver chegado. Economizando nas partidas, você diz? Bem, talvez. Mas para mim, pessoalmente, o princípio de "retribuir-se em qualquer situação incompreensível", que é a base de ganchos básicos, causa rejeição.


Renderizações extras significam uma nova formação de manipuladores de eventos. A propósito, aqui os ganchos comuns são todos ruins. Os manipuladores não apenas são imperativos, mas também precisam ser regenerados toda vez que renderizam. E não será possível usar totalmente o cache aqui, porque muitos manipuladores devem estar bloqueados para variáveis ​​internas do componente. Os manipuladores de mrr são mais declarativos e o cache já está incorporado no mrr: set ('name') será gerado apenas uma vez e será substituído no cache por renderizações subsequentes.


Com um aumento na base de código, os manipuladores imperativos podem se tornar ainda mais pesados. Digamos que também precisamos mostrar o número de envios de formulários feitos pelo usuário.


 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>; } 

Não é muito bonito. Obviamente, você pode renderizar o manipulador como uma função separada dentro do componente. A legibilidade aumentará, mas o problema de regenerar a função com cada renderização permanecerá, assim como o problema de imperatividade. Em essência, esse é um código processual regular, apesar da crença generalizada de que a API do React está gradualmente mudando para uma abordagem funcional.


Para aqueles a quem a escala do problema parece exagerada, posso responder que, por exemplo, os desenvolvedores do React estão conscientes do problema da geração excessiva de manipuladores, oferecendo-nos imediatamente uma muleta na forma de useCallback.


Em 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>; } 

Uma alternativa mais conveniente é useReducer, permitindo que você abandone o imperativo dos manipuladores. Mas outros problemas importantes permanecem: a falta de trabalho com sinais (já que o mesmo useEffect será responsável por efeitos colaterais) e a pior legibilidade durante conversões assíncronas (em outras palavras, é mais difícil rastrear o relacionamento entre os campos da loja, devido ao mesmo useEffect ) Se em mrr o gráfico de dependência entre os campos de estado (threads) for imediatamente visível, em ganchos, você precisará olhar um pouco para cima e para baixo.


Além disso, o compartilhamento de useState e useReducer no mesmo componente não é muito conveniente (novamente haverá manipuladores imperativos complexos que mudarão algo em useState
e ação de despacho), pelo qual, provavelmente, antes de desenvolver o componente, você precisará aceitar uma ou outra opção.


Obviamente, a consideração de todos os aspectos ainda pode ser continuada. Para não ir além do escopo do artigo, abordarei alguns pontos menos importantes na íntegra.


Registro centralizado, depuração. Como no mrr todos os fluxos estão contidos em um hub, para depuração é suficiente adicionar um sinalizador:


 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'], }); ... 

Depois disso, todas as alterações nos threads serão exibidas no console. Para acessar todo o estado (ou seja, os valores atuais de todos os segmentos), existe um pseudo-stream $ state:


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

Portanto, se você precisar ou estiver muito acostumado ao estilo editorial, poderá escrever no estilo editor em mrr, retornando um novo valor de campo com base no evento e em todo o estado anterior. Mas o oposto (escrever sobre useReducer ou um editor no estilo mrr) não funcionará, devido à falta de reatividade neles.


Trabalhe com o tempo. Lembra de dois aspectos dos fluxos: significado e tempo de resposta, harmonia e ritmo? Portanto, trabalhar com o primeiro em ganchos comuns é bastante simples e conveniente, mas com o segundo - não. Ao trabalhar com o tempo, quero dizer a formação de fluxos filho, cujo "ritmo" é diferente do pai. Isso é basicamente todos os tipos de filtros, debowns, trotl etc. Tudo isso você provavelmente terá que se implementar. No srr, você pode usar instruções prontas prontas para uso. O conjunto de cavalheiros mrr é inferior à variedade de operadores Rx, mas possui nomes mais intuitivos.


Interação entre componentes. Lembro que no Editor foi considerado uma boa prática criar apenas uma história. Se usarmos useReducer em muitos componentes,
Pode haver um problema com a organização da interação entre as partes. No mrr, os fluxos podem "fluir" livremente de um componente para outro, para cima ou para baixo na hierarquia, mas isso não criará problemas devido à abordagem declarativa. Mais detalhes
Este tópico, bem como outros recursos da API mrr, são descritos no artigo Atores + FRP no React


Conclusões


Os novos ganchos de reação são ótimos e simplificam nossas vidas, mas eles têm algumas falhas que um gancho de uso geral de nível superior (gerenciamento de estado) pode corrigir. O UseMrr da biblioteca de mrr funcional-reativo foi proposto e considerado como tal.


Problemas e suas soluções:


  • recontagens desnecessárias de dados em cada renderização (em mrr estão ausentes devido à reatividade baseada em push)
  • renderizações extras quando uma mudança de estado não implica uma mudança na interface do usuário
  • baixa legibilidade do código com conversões assíncronas (comparadas às síncronas). Em mrr, o código assíncrono não é inferior ao síncrono em legibilidade e expressividade. A maioria dos problemas discutidos em um artigo recente sobre useEffect no mrr é basicamente impossível
  • manipuladores imperativos que nem sempre são armazenados em cache (em mrr, eles são armazenados automaticamente em cache, quase sempre podem ser armazenados em cache, declarativos)
  • usar useState e useReducer ao mesmo tempo pode criar código estranho
  • falta de ferramentas para converter fluxos ao longo do tempo (desaceleração, acelerador, condição de corrida)

Em muitos pontos, pode-se argumentar que eles podem ser resolvidos por ganchos personalizados. Mas é exatamente isso que está sendo proposto, mas, em vez de implementações díspares, para cada tarefa separada, é proposta uma solução holística e consistente.


Muitos problemas tornaram-se familiares demais para que possamos ser claramente reconhecidos. Por exemplo, as conversões assíncronas sempre pareciam mais complicadas e confusas do que as síncronas, e os ganchos nesse sentido não são piores do que as abordagens anteriores (eds, etc.). Para perceber isso como um problema, você deve primeiro ver outras abordagens que ofereçam uma solução melhor.


Este artigo não pretende impor nenhuma visão específica, mas chamar a atenção para o problema. Estou certo de que existem ou estão sendo criadas outras soluções que podem se tornar uma alternativa válida, mas ainda não se tornaram amplamente conhecidas. A próxima API do React Cache também pode fazer uma grande diferença. Ficarei feliz em receber críticas e discussões nos comentários.


Os interessados ​​também podem assistir a uma apresentação sobre este tópico no kyivjs em 28 de março.

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


All Articles