Criei o mesmo app em React e Vue. Eis as diferenças.

Tradução do texto original de Sunil Sandhu "I created the exact same app in React and Vue. Here are the differences."

Usando Vue no meu atual ambiente de trabalho, eu já tinha um sólido conhecimento de como isso tudo funcionava. No entanto, estava curioso sobre como era a grama do outro lado da cerca — a grama neste cenário sendo React.

Nota de tradução! Ao contrário do autor original, eu, Marcell, trabalho majoritariamente em equipes que usam React. Mantive a mesma semântica textual com poucas modificações linguísticas e algumas adequações técnicas.

Li a documentação do React e assisti a alguns vídeos tutoriais e, enquanto eles eram ótimos e tudo mais, o que eu realmente queria era saber o quão diferente React era de Vue. Por "diferente", não quis dizer coisas como se os dois tinham virtual DOM ou como eles eram ao renderizar páginas. Eu queria alguém para tomar tempo explicando o código e me dizer o que estava acontecendo! Eu queria encontrar um artigo que explicasse essas diferenças para que alguém novo ao Vue ou React (ou desenvolvimento web como um todo) pudesse entender melhor as diferenças entre ambos.

Quem se saiu melhor?

Decidi tentar e razoavelmente construir uma aplicação padrão de "To Do" ("lista de coisas a fazer") que permite a um usuário adicionar e deletar itens de uma lista. Ambos os apps foram criados usando suas CLIs padrões (create-react-app para React, e vue-cli para Vue). CLI significa command line interface ("interface de linha de comando"), a propósito. 🤓

Aliás, esta introdução já está mais longa do que eu havia previsto. Então vamos começar dando uma rápida olhada em como os apps se parecem:

Vue vs React: A Força Irresistível encontra O Objeto Imóvel

O código CSS para ambos são exatamente o mesmo, mas há diferenças onde eles estão localizados. Com isso em mente, vamos a seguir dar uma olhada nas estruturas de arquivos:

Quem se saiu melhor?

Você verá que suas estruturas são quase idênticas também. A única diferença aqui foi que a aplicação React tem três arquivos CSS, em contrapartida o aplicativo Vue não tem nenhum. A razão principal para isso é porque, no create-react-app, o componente React terá um arquivo acompanhante para guardar seus estilos, enquanto que o vue-cli adota uma abordagem mais auto-contida onde os estilos são declarados dentro do arquivo do componente.

Por fim, ambos alcançam a mesma coisa, e não há nada mais a dizer além de que você pode ir em frente e estruturar seu CSS diferentemente em React ou Vue. Isso realmente se resume a preferência pessoal — você ouvirá muitas discussões da comunidade sobre como CSS deve ser estruturado. Por ora, iremos apenas seguir a estrutura definida por ambos as CLI.

Mas antes de prosseguirmos, vamos dar uma olhada rápida ao que típicos componentes Vue e React se parecem:

Vue à esquerda. React à direita.

Agora vamos adentrar nos minuciosos detalhes!

Como mutamos dados?

Mas antes, o que sequer significa "mutação de dados"? Parece um pouco técnico, não é? Basicamente significa só alterar o dado que temos armazenado. Então se quisermos alterar o valor do nome de uma pessoa de "John" para "Mark", estaríamos "mutando o dado". Então aqui está uma diferença chave entre React e Vue. Enquanto Vue essencialmente cria um objeto de dados onde dados podem ser livremente atualizados, React cria um objeto de estado onde um pouco mais de trabalho braçal é necessário para fazer as atualizações. React implementa esse trabalho braçal extra por uma boa razão e iremos chegar lá daqui a pouco. Daremos uma olhada no objeto data do Vue e no objeto state do React:

Objeto data do Vue à esquerda. Objeto state do React à direita.

Você pode ver que passamos o mesmo dado em ambos, mas eles são simplesmente nomeados diferentemente. Assim, as formas de passar um dado inicial aos nossos componentes são muito, muito parecidas. Mas como havíamos mencionado, iremos alterar esses dados diferentemente no Vue e no React.

Digamos que temos um dado chamado name: 'Sunil'.

Em Vue, referenciamos isso chamando this.name. Nós também podemos atualizar isso chamando por this.name = 'John'. Isso irá mudar meu nome para John. Não sei como eu deveria me sentir sendo chamado de John, mas acontece! 😅

Nota de tradução! E, na verdade, meu nome é Marcell, mas… acontece! 😝

Em React, referenciaríamos esse mesmo dado chamando por this.state.name. Agora a diferença chave é que não podemos simplesmente escrever this.state.name = 'John', porque React tem algumas restrições para prevenir esse tipo fácil e descuidado de mutação. Então em React, nós escreveríamos this.setState({ name: 'John' }).

Enquanto isso essencialmente faz a mesma coisa que em Vue, essa escrita adicional está aí porque Vue realiza sua própria versão de setState por padrão sempre que uma nova parte do dado é atualizada. Resumindo, React requer o setState e o dado atualizado sendo passado como parâmetro, ao passo que Vue assume que você iria querer já iria querer fazer isso ao atualizar os valores dentro do objeto. Então por que React se importa com isso, e por que setState é necessário? Vamos deixar Revanth Kumar explicar:

"Isso é porque React quer reexecutar certos eventos de seu ciclo de vida [como quando o componente está prestes a receber novas propriedades, se deve atualizar, se irá atualizar, se atualizou], sempre que o estado mudar. Ele sabe que você mutou quando você chama a função setState. Se você alterasse o estado diretamente, React teria de ter muito trabalho a mais para ficar observando essas alterações e decidir quais etapas do ciclo de vida seriam invocadas. Então, por simplicidade, React usa setState."
Ele sabia melhor

Agora que temos mutações fora do caminho, vamos adentrar em mais detalhes minuciosos olhando para como adicionaríamos novos itens na nossa lista de tarefas em ambos os apps de "To Do".

Como criamos novos itens na lista de atividades?

Nota de tradução! Os códigos não são traduzidos, então não confunda a expressão inglesa “to do” (no código todo, em negrito ou em fundo cinza, cuja tradução é "para fazer") com a palavra portuguesa "todo" (sinônimo de "inteiro"), nem a palavra "data" (escrito nos códigos significando "dado") que é um falso cognato (ou seja, não significa "data" de unidade de tempo).

React:

createNewToDoItem = () => {
this.setState(oldState => {
return {
list: [ ...oldState.list, { oldState.todo } ],
todo: ''
};
});
};

Como React chega aí?

Em React, nosso campo de input (entrada de dados) possui um atributo de value nele. Esse value é atualizado automaticamente por meio do uso de algumas funções que unem tudo para criar uma ligação bidirecional (se você nunca ouviu de two-way binding antes, há uma explicação mais detalhada na seção "como Vue faz isso" mais abaixo). Criamos essa forma de ligação bidirecional por meio de uma função de onChange que ouvirá os eventos associados a esse campo de input sempre que seu valor for alterado (isto é, o usuário digitar algo nele). Vamos rapidamente dar uma olhada nesse campo para ver o que está acontecendo:

<input type="text" 
value={this.state.todo}
onChange={this.handleInput}/>

A função handleInput é executada sempre que o valor do input muda. Isso atualiza todo que está dentro do objeto de estado, definindo-o como qualquer coisa que esteja dentro desse campo de input. Essa função seria:

handleInput = e => {
this.setState({
todo: e.target.value
});
};

Agora, sempre que o usuário pressiona o botão + na página para adicionar um novo item, a função createNewToDoItem essencialmente executará o this.setState passando uma função anônima a ele. Essa função anônima é encarregada de alterar o estado, e recebe o objeto de estado antigo oldState contendo a lista list atual e o valor todo (que é atualizado pela função handleInput), e depois retorna um novo objeto que contém a list inteira de antes mas desta vez contendo também todo em seu fim. A lista inteira foi adicionada por meio do operador spread (use o Google se nunca tiver visto isso antes — é sintaxe ES6).

Finalmente, nós definimos todo como uma string vazia, o que automaticamente atualiza a propriedade value do elemento input e assim limpa o campo.

Vue:

createNewToDoItem() {
this.list.push(
{
'todo': this.todo
}
);
this.todo = '';
}

Como Vue chega aí?

Em Vue, nosso campo input tem um manipulador nele chamado v-model. Isso nos permite fazer o two-way binding. Vamos só olhar rapidamente ao campo de input, então explicaremos o que está acontecendo:

<input type="text" v-model="todo"/>

O v-model liga a entrada desse campo à chave em que temos no objeto data chamada todo. Quando a página carrega, temos todo definido como uma string vazia, como: todo: ''. Se isso já possuísse algum dado, como todo: 'coloque um texto aqui', nosso campo de entrada seria carregado com "coloque um texto aqui" já dentro do campo. De qualquer forma, voltemos a considerá-lo como iniciando por uma string vazia. Qualquer texto que digitarmos dentro do campo input será vinculado ao valor de todo. Isso é efetivamente a ligação bidirecional (o campo input pode atualizar o objeto data e o data pode atualizar o o campo input).

Então olhando de novo o bloco do código da função em Vue createNewToDoItem, vimos que adicionamos conteúdos de todo no array list e então limpamos todo para uma string vazia.

Como deletamos da lista?

React:

deleteItem = indexToDelete => {
this.setState(({ list }) => ({
list: list.filter((toDo, index) => index !== indexToDelete)
}));
};

Como React chega aí?

Enquanto a função deleteItem está localizada dentro de ToDo.js, facilmente pude fazer referência a ela dentro de ToDoItem.js por antes ter passado a função deleteItem como propriedade:

<ToDoItem deleteItem={this.deleteItem.bind(this, key)}/>

Isso passa o método adiante para fazê-lo acessível ao componente filho. Você verá aqui que também estamos "amarrando" (dando bind no) this passando-o como parâmetro além do parâmetro key para que posteriormente a função saiba diferenciar qual dos ToDoItem está tentando disparar a remoção e disparar o ciclo de vida de atualização do componente agregador. Então, dentro do componente de ToDoItem, fazemos o seguinte:

<div className=”ToDoItem-Delete” onClick={this.props.deleteItem}>-</div>

Tudo o que eu tive de fazer foi referenciar a função passada pelo componente pai como this.props.deleteItem.

Vue:

onDeleteItem(todo){
this.list = this.list.filter(item => item !== todo);
}

Como Vue chega aí?

Uma abordagem levemente diferente é necessária em Vue. Essa onDeleteItem está no escopo do objeto data e essencialmente temos de fazer três coisas aqui:

Primeiramente, no elemento em que queremos chamar a função, fazemos:

<div class=”ToDoItem-Delete” @click=”deleteItem(todo)”>-</div>

Então nós temos de criar uma função emissora como método dentro do componente filho (neste caso, ToDoItem.vue), que se fica assim:

deleteItem(todo) {
this.$emit('delete', todo);
}

Além disso, você irá notar que nós realmente referenciamos uma função ao adicionar ToDoItem.vue dentro de ToDo.vue:

<ToDoItem v-for="todo in list" 
:todo="todo"
@delete="onDeleteItem" // <-- isso aqui :)
:key="todo.id" />

Isso é o conhecido como um "ouvinte de eventos" (ou event-listener). Ele fica atento para qualquer ocasião em que um emissor é disparado com a string de "delete". Se ele ouve isso, dispara nossa função onDeleteItem. Tal função está em ToDo.vue ao invés de ToDoItem.vue e, como mostrado anteriormente, apenas filtra o array list dentro do objeto de dados para remover o item clicado.

É válido notar aqui que, no exemplo em Vue, eu poderia ter simplesmente escrito a parte $emit dentro do listener @click, como:

<div class=”ToDoItem-Delete” @click=”$emit(‘delete’, todo)”>-</div>

Isso teria reduzido o número de passos de 3 para 2, mas é simplesmente preferência pessoal.

Em suma, componentes filhos em React podem ter acesso a funções do componente pai via this.props (desde que os componentes estejam passando as propriedades props adiante, o que é razoavelmente uma prática padrão e você vai se deparar com isso várias vezes em outros exemplos de React); enquanto em Vue você terá de emitir eventos do filho que normalmente serão coletados no componente pai.

Como passamos os event listeners?

React:

Para coisas simples como eventos de clique, podemos ir direto ao ponto. Aqui está um exemplo de como criamos um evento de clique para um botão que cria um novo item de "To Do" em list:

<div className=”ToDo-Add” onClick={this.createNewToDoItem}>+</div>

Super fácil aqui e é basicamente é assim como lidaríamos com um onClick usando JS puro. Leva um pouquinho mais para configurar um event listener para sempre que a tecla "Enter" for pressionada. Isso essencialmente requer um evento onKeyPress para ser lidado pela tag input, como:

<input type=”text” onKeyPress={this.handleKeyPress}/>

Essa função essencialmente dispara a função createNewToDoItem sempre que for reconhecida a tecla Enter, como mais ou menos isso:

handleKeyPress = (e) => {
  if (e.key === ‘Enter’) {
    this.createNewToDoItem();
  }
};

Vue:

Em Vue é mais direto ainda. Simplesmente usamos o símbolo @, e então o tipo de event listener que queremos fazer. Então, por exemplo, para adicionar um ouvinte de evento de clique, poderíamos escrever o seguinte:

<div class=”ToDo-Add” @click=”createNewToDoItem()”>+</div>

Note @click é na verdade uma abreviação para v-on:click. O legal do Vue com event listeners é que há mais um bocado de coisas com que você pode encadeá-los, como .once que previne o esse ouvinte de ser disparado mais de uma vez. Há também outro bocado de abreviações quando se fala de escrever eventos específicos de pressionamento de tecla. Em Vue, eu consegui simplesmente escrever:

<input type=”text” v-on:keyup.enter=”createNewToDoItem”/>

E notei que em React havia levado um pouco mais de tempo.

Como passamos dados a um componente filho?

React:

Em React, passamos o objeto de propriedades props ao componente filho no momento em que ele é instanciado, como:

<ToDoItem key={key} item={todo} />

Aqui nós vemos dois membros de props passados ao componente ToDoItem. Daqui em diante, nós vamos referenciar esses dados dentro do filho por meio de this.props. Então para acessar essa propriedade todo, simplesmente chamamos this.props.item.

Vue:

Em Vue, passamos propriedades ao componente filho de forma semelhante. Como em:

<ToDoItem v-for="todo in list" 
:todo="todo"
:key="todo.id"
@delete="onDeleteItem" />

Esses podem então ser referenciados nas instâncias filhas por seus nomes — assim, em nosso caso, 'todo'.

Como emitimos dados de volta a um componente pai?

React:

Primeiro passamos a função adiante ao filho fazendo dela uma propriedade no objeto props dele. Então chamamos no filho de alguma forma, como delegando a um onClick, referenciando por this.props, assim como pudemos ver na seção "como deletamos da lista" sendo feito com this.props.deleteItem. Isso irá disparar a função declarada no componente pai.

Vue:

Em nosso componente filho, simplesmente escrevemos a função que emite o valor de volta à função do pai. Nesse nosso componente pai, escrevemos uma função que ouve quando aquele valor é emitido, o que pode ser disparado durante a chamada da função. Vimos um exemplo disso na seção "como deletamos da lista" também.

E pronto! 🎉

Vimos como adicionamos, removemos e alteramos dados, passamos dados de formulários às propriedades, de componentes pais aos componentes filhos, vice-versa, e disparamos mutações usando event listeners. Existem, é claro, outras pequenas diferenças entre React e Vue, mas espero que o conteúdo desse artigo tenha ajudado a prover um pouco mais de fundamento à compreensão de como ambos funcionam.