Executando verificação de segurança...
13

As aplicações reativas atuais são um absurdo completo

capa-post-1

Já faz um tempo que venho pensando em falar sobre o quanto é complicado desenvolver aplicações reativas e hoje tive a coragem de começar a escrever sobre esse assunto tão delicado.

Desde 2013 trabalho com desenvolvimento de sistemas. Tenho desenvolvido principalmente aplicações web e apesar de bastante experiente, acho que ficou muito mais dificil construir aplicações com as novas tecnologias que surgiram na última década.

Quando comecei a desenvolver em 2013, bastava conhecer um pouco de orientação a objetos, PHP, MySQL, HTML, CSS e JQuery. Podiamos criar aplicações rapidamente, era muito produtivo.

Hoje em dia gastamos um tempo enorme lendo documentações, lidando com centenas de problemas criados muitas vezes por adortarmos excessivamente recursos como bibliotecas de terceiros, que depois de somadas, não nos torna mais produtivos.

Nesse post, vou focar num ponto que considero crucial para qualquer aplicação, o gerenciamento de estado.

Considero muito burocrático, complicado e as vezes até desnecessário em alguns casos mais graves. Adotar gerenciadores de estados de terceiros para a maioria dos projetos. Por isso, para não ficar apenas criticando, demonstro na prática como poderia ser uma solução eficaz diante das percepções que compartilho com você nesse momento.

Falando mal dos gerenciadores de estado atuais

Os principais gerenciadores de estado que conheço são:

Pensando no React ainda existem os hooks que "ajudam" o desenvolvedor a dizer ao React quando a aplicação deve reagir a mudanças no estado.

Sinceramente, do meu ponto de vista, todas essas soluções resolvem o problema do gerenciamento de estado, mas, implicando em complexidade extra e totalmente desnecessária.

Eu sinto que o Redux é extremamente verboso e percebi em alguns casos que ele complica muito o contexto das aplicações ao necessitar de vários arquivos para manter tudo organizado. Também acredito que o Vuex e o Pinia estão na mesma direção.

As vezes refletindo me pergunto o que pensavam essas pessoas quando criaram essas ferramentas. Será que pensavam "Por que facilitar se podemos dificultar!"?

Outro alvo das minhas críticas, o NgRx traz uma solução menos árdua, ainda acho complicado já que o RxJS tem um trilhão de métodos com comportamentos diferentes para resolver várias situações. Por isso, penso que tudo poderia ser muito mais simples do que é atualmente.

Bibliotecas de terceiros não deveriam ser o padrão

Me diga você, quem conhece melhor os problemas que o software que você está criando resolve?

Se sua resposta foi "Ninguém conhece melhor que meus clientes, minha equipe, minha empresa ou meus colaboradores", estou alinhado com você nessa forma de pensar. No entanto, isso me faz levantar outra questão "porque praticamente todo mundo usa bibliotecas de terceiros como um padrão para solucionar problemas de gerenciamento de estado?".

A pergunta feita no parágrafo anterior exige muito cuidado na formulação de uma resposta. Mas, sem pensar muito, uma resposta pode ser "não reinventar a roda" ou talvez "poupar tempo usando a sabedoria empregada nas bibliotecas que já estão validadas para uso".

Siceramente, não sinto que essas respostas são solidas o suficiente para justificar a adoção de bibliotecas de terceiros super complexas em detrimento de padrões simples que poderiam ser muito menos burocráticos.

Não me considero nem perto de ser o melhor desenvolvedor do mundo, mesmo assim, tenho construido software de boa qualidade ao longo da última década. Por isso, creio que desenvolver uma solução própria, baseada em patterns poderia ser melhor que usar bibliotecas de terceiros para esse fim.

Quem critica deve propor uma solução melhor

Sempre considerei que saber muito sobre algo é menos importante que conseguir fazer mais com o pouco que se sabe.

Me sinto muito alinhado com pensamentos que vão diretamente ao encontro de KISS, YAGNI e DRY,conceitos que me ajudam a manter as coisas simples.

A partir desse ponto, pretendo apresentar uma solução de gerenciamento de estado que considero eficaz, concisa e fácil de aprender a usar mesmo para desenvolvedores iniciantes.

Observer Pattern

O padrão observador é uma forma de fazer com que uma função seja executada no futuro, quando uma mudança ocorrer em um objeto observado.

Exemplo:


import { createState, render, html } from "./lib";

type TotalModel  = { 
  total: number 
}

type Params = { 
  state: TotalModel;
}

const template = ({state}: Params) => html`
  <p><strong>Total</strong>: ${state.total}</p>
`

const AppTotal = () => {
  const price = 20;
  const operationTaxValue = 10;
  const state = createState<TotalModel>({ total: 0 });
  
  state.watchState( newState => render(template))

  setTimeout(() => state.set({total: 5}), 3000)  

  const getTotal = () => {
    state.setState({ total: price + operationTaxValue })
  }  

  return { state, template }
}

Note no exemplo acima a importação dos serviços createState e render. O serviço createState é responsável por criar um objeto que informará ao render para atualizar a view novamente assim que o estado for atualizado.

Essas são operações que definem o comportamento do pardão observador e agora que a responsabilidade de cada serviço está clara, podemos seguir para a implementação de cada um deles.

Renderizador reativo

Um renderizador reativo é um serviço que pode construir uma árvore de elementos HTML e atualizar o DOM sempre que ocorrer alterações nos estados de um ou mais componentes.

Para começar, serão definidos os tipos usados para construir o renderizador reativo:


//lib/render/types.ts

import { TState } from "../state/types"

type Model = Object & {}

type TemplateParams<TModel> = {
  state: TModel
}

export type Template<TModel> = {
  (params: TemplateParams<TModel>): string
}

export type Component<TModel> = {
  template: Template<TModel>,
  state: TState<TModel>
}

export type ComponentFactory<TModel> = {
  (): Component<TModel>
}

Cada um dos tipos criados, será utilizado para definir os parâmetros do renderizador que será construído logo adiante. Veja:


//lib/render/index.ts

import { ComponentFactory } from "./types";

const taggedFunction = (tags: any, ...values: any[]): string => {
  return tags
    .map((tag: string, index: number) => {
      return `${tag}${values[index] || ""}`;
    })
    .join("");
};

export const html = taggedFunction;
export const css = taggedFunction;

const createSelector = (text: string) =>
	text.split(/(?=[A-Z])/).join("-").toLowerCase();

const throwError = (message: string) => {
  throw new Error(message);
}

const createComponentElement = (componentFactoryName: string): HTMLElement => {
  if(!componentFactoryName) throwError('componentFactory name is not defined and must be.');
  const selector = createSelector(componentFactoryName);
  return document.createElement(selector);
}

export const render = <TModel>(
  componentFactory: ComponentFactory<TModel>, 
  parentElement = document.body
): void => {
  const componentElement = createComponentElement(componentFactory?.name) || null;
  const component = componentFactory()
  const state = component.state.state
  !componentElement && throwError('Component element is not defined and must be.')
  
  parentElement.innerHTML = '';
  componentElement.insertAdjacentHTML('beforeend', component.template({ state }));
  parentElement.insertAdjacentElement('beforeend', componentElement);
}

Observe que no arquivo render.ts declara 4 funções.

  • createSelector: reponsável por criar o seletor do componente
  • createComponentElement: responsável por criar o elemento wrapper do componente
  • render: responsável por renderizar o componente
  • taggedFunction: responsável pelo parse de tags e por ajudar com recursos de sintaxe HTML e CSS no VSCode

Das quatro funções citadas acima, a mais complexa é a função render, que deve receber outra função como componente e um elemento qualquer como contexto. Por padrão, essa função recebe como contexto o element body da página HTML.

Dentro da função render foram usadas outras duas funções, para criar os recursos dos quais depende a função render para injetar o componente na view através dos métodos insertAdjacentHTML e insertAdjacentElement.

Seria totamente possível usar apenas innerHTML, mas, as funções insertAdjacentHTML e insertAdjacentElement produzem o resultado esperado e ainda otimizam a performance das operações de manipulação do DOM.

Uma vez "pronto" o renderizador pode ser usado multiplas vezes, para tal, basta apenas importar o módulo.


  import { render } from '@/services'

Gerenciador de estado

Seguindo em frente vou demonstrar como é possível estruturar um gerenciador de estado bem simples.

Em poucas linhas de código, será definido um gerenciador de estado bem completo. Que pode ser extendido para realizar todas as atividades que o padrão FLUX é capaz de operar, mas, como simplicidade muito mais efetiva.

Primeiro é necessário que os tipos que serão usados logo em seguida sejam definidos.


//lib/state/types.ts

export type TEmpty = null | undefined;

export type TGenericObject<T> = {
  [key: string]: T;
};

export type TStateValue<T> = TGenericObject<T>;

export type TStateHandler<A> = <T extends A>(payload: T) => void;

export type TStateHandlerRemove = () => boolean;

export type TState<T> = {
  state: T;
  setState: (payload: T) => void;
  watchState: (handler: TStateHandler<T>) => TStateHandlerRemove;
};

Os tipos acima podem parecer um pouco complexos, então, se você não estiver acostumado a trabalhar com Typescript, não se preocupe tanto. Vou tentar exclarecer alguns pontos abaixo para facilitar um pouco mais sua vida.

Nos trechos onde há algo como TStateValue<T> & TStateHandler<A> foram definidos tipos genéricos. Os tipos genéricos <T> e <A> são como os parâmetros de funções que permitem receber valores "dinamicamente".

No caso da tipagem, os tipos genéricos podem ser usados para tipar algum valor desconhecido no momento da declaração da tipagem. Dessa forma, o gerenciador de estado poderá tipar qualquer estrutura de dados no futuro.

Tomando como exemplo o código abaixo, você poderá perceber que o tipo TotalModel foi passado como tipo genérico para a função createState. Esse método faz uso de tipos genéricos para definir o tipo de dados de um objeto externo.

O objeto {total:0} está sendo tipado através de um tipo genérico.

type TotalModel  = { 
  total: number 
}

const state = createState<TotalModel>({ total: 0 });

Observe a definição da fábrica de estados no código abaixo:


//lib/state/index.ts

import {
  TState,
  TStateHandler,
  TStateHandlerRemove,
} from "./types";

export const createState = <T extends Object>(value: T): TState<T> => {
  const state: T = value;
  const handlers = new Set<TStateHandler<T>>();

  const _notifyHandlers = (value: T): void => {
    handlers.forEach((handler) => handler<T>(value));
  };

  const setState = (payload: T): void => {
    const payloadCopy = JSON.parse(JSON.stringify(payload));
    const stateCopy = JSON.parse(JSON.stringify(state));
    const newState = { ...stateCopy, ...payloadCopy };
    Object.assign(state, newState);
    _notifyHandlers(newState);
  };

  const watchState = (handler: TStateHandler<T>): TStateHandlerRemove => {
    handlers.add(handler);
    return () => handlers.delete(handler);
  };

  return { state, setState, watchState };
};

A função createState declarada logo acima, aceita um tipo genérico derivado de Object e o usa para definir a estrutura de dados do objeto value e para definir o tipo de objeto a ser retornado por essa função.

O retorno da fábrica de estados acima, deve ser um objeto contento as funções setState, watchState e um objeto que represente o estado. Cada uma dessas funções responde por uma única responsabilidade, mas, juntas formam o Observer pattern.

O padrão observer pode ser usado para inscrever funções que devem ser executadas mediante alterações na estrutura de dados de um objeto, por isso, esse padrão pode ser muito útil para criar gerenciadores de estados.

Na solução que apresento, sempre que for necessário modificar o estado atual, a função setState deve ser usada e para os casos em que for necessário definir um observador para reagir a mudanças no estado, a função watchState é que deverá ser usada.

Veja abaixo:


  const state = createState<TotalModel>({ total: 0 });

  state.watchState( newState => console.log('Ocorreu alguma alteração no estado:', newState))

  setTimeout(() => state.set({total: 5}), 3000)

No exemplo acima, um estado inicial foi definido como {total:0} e depois de 3 segundos quando state.set for executada, a função watchState será notificada e imediatamente executará a função de log exibindo a mensagem Ocorreu alguma alteração no estado e o Novo estado no console.

O comportamento descrito acima ocorre porque a função state.set é um notificador e state.watchState é responsável por inscrever observadores interesados em reagir a mudanças no estado.

Poderiamos incluir com muita facilidade outras funcões ao gerenciador de estado. Por exemplo, podriamos incluir uma função para desinscrever observadores, mas, creio que por hora esse exemplo basta.

Como os recursos criados devem ser usados na prática

Para fazer uso dos recursos criados anteriormente, não poderia ser mais simples. Basta seguir criando um componente e importando os módulos que foram criados para fabricar estados e definir quais observadores deverão reagir a alterações no estado.

Abaixo demonstro o código completo em um componente bem simples:


import { createState, render, html } from "./lib";

type TotalModel  = { 
  total: number 
}

type Params = { 
  state: TotalModel;
}

const template = ({state}: Params) => html`
  <p><strong>Total</strong>: ${state.total}</p>
`

const AppTotal = () => {
  const price = 20;
  const operationTaxValue = 10;
  const state = createState<TotalModel>({ total: 0 });

  state.watchState( newState => render(template))

  setTimeout(() => state.set({total: 5}), 3000)

  const getTotal = () => {
    state.setState({ total: price + operationTaxValue })
  }  

  return { state, template }
}

No exemplo acima, após 3 segundos, o estado é alterado e o componente reage a mudança ocorrida renderizando o novo estado e o template do componente. Portanto, esse padrão tão simples, poderia ser extendido para gerenciar todo o estado de uma aplicação.

Como o código escrito está de acordo com a sugestão de YAGNI, DRY e KISS, manter e expandir suas capacidades não seria uma tarefa tão complexa. Além do mais, pelo menos algumas das bibliotecas que citei antes, já usam esse pattern sob o capô.

Criticando outras áreas do desenvolvimento de software

Nesse post falei apenas da ponta do Iceberg. A complexidade das aplicações que desenvolvemos hoje em dia são exageradamente grande e precisam ser eliminadas rapidamente do meu ponto de vista.

Apesar de discordar da forma como as coisas estão sendo feitas hoje em dia, não pense que sou da turma que chuta o pau da barraca e se torna radicalmente contra usar frameworks e bibliotecas.

Tenho usado algumas bibliotecas e frameworks de terceiros sempre que considero os ganhos que podem trazer como positivos ou muito positivos eu até criei minha própria biblioteca IARES para construir SPA.

Bem, por hora é só, mas, se você curtiu e quer acompanhar outras postagens, veja esses conteúdos que criei aqui, eles podem te trazer ótimos insights:

1 - Arquitetura para micro-frontend
2 - Metarepo vs Monorepo
3 - Definindo um bom codestyle

2

Compartilho da sua opinião. Boa parte do que tem aí é o que falo.

Não acho que alguma ferramenta popular seja completamente inútil, em geral haverá um cenário que seja bom o seu uso, mas em geral também o uso se dá pelo hype. E as pessoas não conseguem se livrar disso.

Em muitos casos até acho que pode ser necessário fazer uso de tudo isso. Mas só porque a web não é a tecnologia certa para o que a pessoa está fazendo. Não era mais fácil resolver os problemas no nativo? Mas houve uma conjunção de coisas, e houve muito interesse em fazer a web ser o centro do desenvolvimento, e aí tudo vai conspirando a favor. "Tem um problema? vamos fazer uma ferramenta que resolve". Em vez de escolher algo que não tem tanto problema.

Hoje boa parte das pessoas não conseguem justificar a escolha com fatos reais. Mas como toda "conspiração", vai aparecendo gente para criar argumentos que pareça ser uma escolha plausível.

De toda forma, é uma batalha perdida. Podemos falar sobre, mas não mudará o panorama. Chega uma hora que vira o normal.

Obviamente eu sou mais "radical" ainda.

Faz sentido para você?

Espero ter ajudado.


Farei algo que muitos pedem para aprender a programar corretamente, gratuitamente. Para saber quando, me segue nas suas plataformas preferidas. Quase não as uso, não terá infindas notificações (links aqui).

1

No fim, as aplicações desaparecem, morrem, definham junto com o hype, mas o velho e bom js/css/html e um backend qualquer permanecem.

1
1

Tenho pouca experiencia na área, mas aprendi muito bem React com Redux, Context api e tal.

A minha opinião sobre esses gerenciamentos de estado e sobre essas tecnologias que "Dificultam" o desenvolvimento, é que depende de quem está desenvolvendo e para qual contexto a aplicação está sendo desenvolvida.
E eu acabei formando essa opnião quando estava aprendendo Typescript, em uma equipe pequena, onde pessoas que não tem conhecimento de typescript irão trabalhar com o código, não faz nenhum sentido o typescript, só irá dificultar e adicionar mais carga de trabalho, porém em equipes bem estruturadas e grandes, ele acaba facilitando a manutenção do código, evitando erros e tal.

Lembro que em uma aula sobre Redux a professora disse a seguinte frase, estamos usando uma "BAZUKA pra matar uma formiga", e acho que é bem isso mesmo, temos que escolher as ferramentas certas para os projetos. Mas prefiro mil vezes utilizar Redux do que ter problemas com Prop Drilling por exemplo.

1
1

Dei uma conferida e achei o mithril bem verboso, especialmente pra projetos simples.

E curioso porque no final de semana eu trabalhei num projeto parecido com o do autor aqui, mas funciona como o Mithril sendo VanillaJS, mas utiliza uma sintaxe React-Like.

O link pro repo ta aqui, se te interessar, me fala o que achou?

1

Concordo e compartilho desse sentimento !

Tenho algumas plataformas desenvolvidas em PHP + MySQL + HTML + CSS + JS , rodam perfeitamente, estão em constante evolução e são muito práticas em termos de criar novas ferramentas em manutenções.

Apesar de eu adorar as stacks atuais e estar trabalhando basicamente focado nelas, elas complicam muito o ponto de partida mas em contrapartidafacilitam alguns pontos futuros.

Parabéns pelo artigo e por expor algo, que apesar de polemico para alguns, demonstra claramente experiência profissinal de quem sabe do que fala. Grande abraço. Tmj !

1

Boa noite amigo, também sou adepto ao antigo PHP + MySQL + HTML + CSS + JS, pois desenvolvo um sistema de gestão micro erp. Mas sempre fico me perguntando se estou ficando para trás nas tecnologias atuais de desenvolvimento, por exemplo, possivelmente focar em NodeJs + MongoDB + Angular. Mas ai todo dia aparece um sabor novo de framework JS que promete revolucionar o mercado. Vejo uma curva de aprendizado mais longa nesses novos frameworks para conseguir algo funcional e prático para o dia a dia, é válido esse meu pensamento? Você poderia compartilhar quais stacks utilizou pós PHP? abraços.

2

Realmente John. Levei algum tempo até me sentir confortável em desenvolver na nova stack que tenho trabalhado mais atualmente ( Reactjs / React Native / NextJs / Nodejs / Typescript ) que alias gosto muito. Porém um de minhas plataformas, de e-commerce, é feita em PHP. Nela sempre lancei novas ferramentas e features muito rápido. E mesmo hoje, já trabalhando algum tempo com a nova stack em outros sistemas, ainda sim acho menos prático realizar a mesma tarefa.

Mas posso te dizer que tenho visto também uma procura grande de profissionais GOLang, Python e PHP+Laravel ao menos por aqui no Canadá e países vizinhos. E um tech recruiter me disse que Angular tem tido uma grande procura também por aqui e Europa. Por outro lado um conhecido recrutador no Brasil me disse que o VUE está no top de solicitações recentemente.

E sabendo que o maior erro, que todos nós devs provavelmente já praticamos, é pular de stack para stack ou linguagem para linguagem, vivemos tempos sombrios kkkkk

Outro ponto de vista de quem é muito fã de PHP. A linguagem tem evoluído para tentar voltar ao topo. Talvez ao invés de migrar você se sinta mais confortável em evoluir na própria linguagem e trabalhar com frameworks como Laravel caso ainda não trabalhe.

Espero ter ajudado com meu ponto de vista meu amigo. Tmj !

1
0
1

Acredito com firmeza que você deve apostar em tecnologias que reduzem teu custo e beneficiam tua produtividade se está empreendendo.

Tua stack atual provê boa segurança e te ajuda a ser produtivo? Se sim, mantém.

Se por outro lado, você vende sua força de trabalho, precisa estar alinhado com as tecnologias de mercado.

Não existe uma única resposta, mas, dentre tantas tecnologias você pode optar por uma Stack que satisfaça produtividade, estabilidade e oportunidades de trabalho e pronto.

1
1

Eu concordo que os novos frameworks acabam sendo muito mais dificeis de usar por ser necessário utilizar todas essas libs de terceiros e ter que ler um milhão de docs so para começar a desenvolver. Mas isso ocorre justamente por esses frameworks não se apegarem a um método especifico e darem liberdade para o dev escolher sua solução os exemplos citados como Redux ou RxJS não são obrigatórios para o uso da ferramenta, voce pode escolher uma outra lib de dev ou criar sua própria solução como de certa forma voce fez em seu post.

Muitas dessas libs que são complexas de mais ou tem muitas coisas como o RxJS citado são assim pois nasceram para suprir uma necessidade e foram criando mais soluções para suprir outros contextos. Seguindo no exemplo do RxJS mesmo voce não precisa e nem vai usar todos os métodos que ele disponibiliza a maioria voce nunca nem vai ver sendo aplicado e sabendo somente o básico voce consegue desenvolver tranquilamente (apesar de "defender" o RxJS eu mesmo não sou fã e sempre bem pouco)

E essa maneira de criar projetos com HTML, CSS, PHP, JS é sim mais simples porem quando voce escala para projetos muito grandes ou tem um time de devs em expansão ele se torna um caos pois beira o impossível manter um padrão pelo projeto para que seja mais facil manter aquele código principalmente com devs novos no time, assim como criar sua própria solução, manter uma doc disso e manter a mesma atualizada é muito trabalhoso e demanda um tempo que muitas vezes voce nao vai ter dentro de uma empresa, por isso a adoção de soluções de terceiros é sempre mais simples e preferida.

1

Discordo de você sobre manter um lib própria.

Tive a oportunidade de publicar e manter algumas bibliotecas no npm e não foi nada complicado.

Você já publicou alguma biblioteca? Teve que manter e atualizar por algum tempo? como foi sua experiência com isso?

Essa afirmação de que é melhor usar a biblioteca que já está pronta para economizar tempo e esforço, considero inválida, pois, as pessoas gastarão tempo e esforço resolvendo problemas criados por usar essas libs.

Também é falso que frameworks ajudam a padronizar código. Afinal, podemos escrever o código da pior forma possível e o framework não vai impedir o uso desse código.

O que os frameworks fornecem são estruturas básicas para resolver problemas de um determinado ponto de vista e esse ponto de vista com toda certeza não é o melhor para todos os casos.

Acredito que desenvolver suas próprias bibliotecas vão te levar a uma visão abrangente sobre o assunto e enriquecer teu conhecimento.

Fica a critério da equipe ou do dev solo escolher ou criar suas ferramentas e se os envolvidos estiverem satisfeitos, basta.

Muito obrigado por partilhar sua visão comigo.