You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Web-Dev-For-Beginners/translations/pt/7-bank-project/4-state-management/README.md

16 KiB

Construir uma App Bancária Parte 4: Conceitos de Gestão de Estado

Questionário Pré-Aula

Questionário pré-aula

Introdução

À medida que uma aplicação web cresce, torna-se um desafio acompanhar todos os fluxos de dados. Qual código obtém os dados, qual página os consome, onde e quando precisam ser atualizados... é fácil acabar com um código confuso e difícil de manter. Isso é especialmente verdadeiro quando é necessário compartilhar dados entre diferentes páginas da aplicação, como os dados do utilizador. O conceito de gestão de estado sempre existiu em todos os tipos de programas, mas à medida que as aplicações web continuam a crescer em complexidade, tornou-se um ponto-chave a considerar durante o desenvolvimento.

Nesta última parte, vamos rever a aplicação que construímos para repensar como o estado é gerido, permitindo suporte para atualizações do navegador a qualquer momento e persistindo os dados entre sessões do utilizador.

Pré-requisitos

É necessário ter concluído a parte de obtenção de dados da aplicação web para esta lição. Também é necessário instalar o Node.js e executar a API do servidor localmente para poder gerir os dados da conta.

Pode testar se o servidor está a funcionar corretamente executando este comando num terminal:

curl http://localhost:5000/api
# -> should return "Bank API v1.0.0" as a result

Repensar a gestão de estado

Na lição anterior, introduzimos um conceito básico de estado na nossa aplicação com a variável global account, que contém os dados bancários do utilizador atualmente autenticado. No entanto, a nossa implementação atual tem algumas falhas. Experimente atualizar a página enquanto está no painel. O que acontece?

Há 3 problemas com o código atual:

  • O estado não é persistido, pois uma atualização do navegador leva-o de volta à página de login.
  • Existem várias funções que modificam o estado. À medida que a aplicação cresce, isso pode dificultar o acompanhamento das alterações e é fácil esquecer de atualizar algo.
  • O estado não é limpo, então, quando clica em Logout, os dados da conta ainda estão lá, mesmo estando na página de login.

Poderíamos atualizar o nosso código para resolver esses problemas um por um, mas isso criaria mais duplicação de código e tornaria a aplicação mais complexa e difícil de manter. Ou poderíamos parar por alguns minutos e repensar a nossa estratégia.

Que problemas estamos realmente a tentar resolver aqui?

A gestão de estado trata de encontrar uma abordagem eficaz para resolver estes dois problemas específicos:

  • Como manter os fluxos de dados numa aplicação compreensíveis?
  • Como garantir que os dados do estado estejam sempre sincronizados com a interface do utilizador (e vice-versa)?

Depois de resolver esses problemas, quaisquer outros problemas que possa ter podem já estar resolvidos ou tornarem-se mais fáceis de resolver. Existem muitas abordagens possíveis para resolver esses problemas, mas vamos optar por uma solução comum que consiste em centralizar os dados e as formas de os alterar. Os fluxos de dados seriam assim:

Esquema mostrando os fluxos de dados entre o HTML, ações do utilizador e estado

Não vamos abordar aqui a parte em que os dados automaticamente desencadeiam a atualização da vista, pois está ligada a conceitos mais avançados de Programação Reativa. É um bom tema para explorar mais a fundo.

Existem muitas bibliotecas com diferentes abordagens para gestão de estado, sendo o Redux uma opção popular. Dê uma olhada nos conceitos e padrões utilizados, pois muitas vezes é uma boa forma de aprender sobre os potenciais problemas que pode enfrentar em grandes aplicações web e como podem ser resolvidos.

Tarefa

Vamos começar com um pouco de refatoração. Substitua a declaração de account:

let account = null;

Por:

let state = {
  account: null
};

A ideia é centralizar todos os dados da aplicação num único objeto de estado. Por enquanto, só temos account no estado, então não muda muito, mas cria um caminho para evoluções futuras.

Também temos de atualizar as funções que o utilizam. Nas funções register() e login(), substitua account = ... por state.account = ...;

No início da função updateDashboard(), adicione esta linha:

const account = state.account;

Esta refatoração por si só não trouxe muitas melhorias, mas a ideia era preparar o terreno para as próximas alterações.

Acompanhar alterações nos dados

Agora que implementámos o objeto state para armazenar os dados, o próximo passo é centralizar as atualizações. O objetivo é facilitar o acompanhamento de quaisquer alterações e quando elas ocorrem.

Para evitar alterações feitas diretamente no objeto state, também é uma boa prática considerá-lo imutável, o que significa que não pode ser modificado de forma alguma. Isso também significa que é necessário criar um novo objeto de estado se quiser alterar algo nele. Ao fazer isso, constrói uma proteção contra potenciais efeitos colaterais indesejados e abre possibilidades para novas funcionalidades na aplicação, como implementar desfazer/refazer, além de facilitar a depuração. Por exemplo, poderia registar todas as alterações feitas no estado e manter um histórico das alterações para entender a origem de um erro.

Em JavaScript, pode usar Object.freeze() para criar uma versão imutável de um objeto. Se tentar fazer alterações num objeto imutável, será lançada uma exceção.

Sabe a diferença entre um objeto imutável superficial e profundo? Pode ler sobre isso aqui.

Tarefa

Vamos criar uma nova função updateState():

function updateState(property, newData) {
  state = Object.freeze({
    ...state,
    [property]: newData
  });
}

Nesta função, estamos a criar um novo objeto de estado e a copiar os dados do estado anterior usando o operador de espalhamento (...). Em seguida, substituímos uma propriedade específica do objeto de estado com os novos dados usando a notação de colchetes [property] para atribuição. Por fim, bloqueamos o objeto para evitar modificações usando Object.freeze(). Por enquanto, só temos a propriedade account armazenada no estado, mas com esta abordagem pode adicionar quantas propriedades forem necessárias.

Também vamos atualizar a inicialização do state para garantir que o estado inicial também seja congelado:

let state = Object.freeze({
  account: null
});

Depois disso, atualize a função register substituindo a atribuição state.account = result; por:

updateState('account', result);

Faça o mesmo com a função login, substituindo state.account = data; por:

updateState('account', data);

Agora vamos aproveitar para corrigir o problema dos dados da conta não serem limpos quando o utilizador clica em Logout.

Crie uma nova função logout():

function logout() {
  updateState('account', null);
  navigate('/login');
}

Na função updateDashboard(), substitua o redirecionamento return navigate('/login'); por return logout();;

Experimente registar uma nova conta, fazer logout e login novamente para verificar se tudo ainda funciona corretamente.

Dica: pode verificar todas as alterações de estado adicionando console.log(state) no final de updateState() e abrindo o console nas ferramentas de desenvolvimento do navegador.

Persistir o estado

A maioria das aplicações web precisa de persistir dados para funcionar corretamente. Todos os dados críticos geralmente são armazenados numa base de dados e acessados através de uma API de servidor, como os dados da conta do utilizador no nosso caso. Mas, às vezes, também é interessante persistir alguns dados na aplicação cliente que está a ser executada no navegador, para uma melhor experiência do utilizador ou para melhorar o desempenho de carregamento.

Quando quiser persistir dados no navegador, há algumas perguntas importantes que deve fazer:

  • Os dados são sensíveis? Deve evitar armazenar quaisquer dados sensíveis no cliente, como senhas de utilizador.
  • Por quanto tempo precisa de manter esses dados? Pretende acessar esses dados apenas durante a sessão atual ou quer que sejam armazenados para sempre?

Existem várias formas de armazenar informações numa aplicação web, dependendo do que pretende alcançar. Por exemplo, pode usar os URLs para armazenar uma consulta de pesquisa e torná-la partilhável entre utilizadores. Também pode usar cookies HTTP se os dados precisarem de ser partilhados com o servidor, como informações de autenticação.

Outra opção é usar uma das muitas APIs do navegador para armazenar dados. Duas delas são particularmente interessantes:

  • localStorage: um armazenamento de chave/valor que permite persistir dados específicos do site atual entre diferentes sessões. Os dados guardados nele nunca expiram.
  • sessionStorage: funciona da mesma forma que o localStorage, exceto que os dados armazenados nele são apagados quando a sessão termina (quando o navegador é fechado).

Note que ambas as APIs só permitem armazenar strings. Se quiser armazenar objetos complexos, terá de os serializar para o formato JSON usando JSON.stringify().

Se quiser criar uma aplicação web que não funcione com um servidor, também é possível criar uma base de dados no cliente usando a API IndexedDB. Esta é reservada para casos de uso avançados ou se precisar de armazenar uma quantidade significativa de dados, pois é mais complexa de usar.

Tarefa

Queremos que os utilizadores permaneçam autenticados até clicarem explicitamente no botão Logout, então vamos usar localStorage para armazenar os dados da conta. Primeiro, vamos definir uma chave que usaremos para armazenar os dados.

const storageKey = 'savedAccount';

Depois, adicione esta linha no final da função updateState():

localStorage.setItem(storageKey, JSON.stringify(state.account));

Com isso, os dados da conta do utilizador serão persistidos e sempre atualizados, já que centralizámos anteriormente todas as atualizações de estado. É aqui que começamos a beneficiar de todas as refatorações anteriores 🙂.

Como os dados são guardados, também temos de cuidar da sua restauração quando a aplicação é carregada. Como começaremos a ter mais código de inicialização, pode ser uma boa ideia criar uma nova função init, que também inclui o nosso código anterior no final de app.js:

function init() {
  const savedAccount = localStorage.getItem(storageKey);
  if (savedAccount) {
    updateState('account', JSON.parse(savedAccount));
  }

  // Our previous initialization code
  window.onpopstate = () => updateRoute();
  updateRoute();
}

init();

Aqui recuperamos os dados guardados e, se houver algum, atualizamos o estado de acordo. É importante fazer isso antes de atualizar a rota, pois pode haver código que depende do estado durante a atualização da página.

Também podemos tornar a página Dashboard a página padrão da nossa aplicação, já que agora estamos a persistir os dados da conta. Se nenhum dado for encontrado, o painel cuida de redirecionar para a página de Login de qualquer forma. Na função updateRoute(), substitua o fallback return navigate('/login'); por return navigate('/dashboard');.

Agora faça login na aplicação e experimente atualizar a página. Deve permanecer no painel. Com essa atualização, resolvemos todos os problemas iniciais...

Atualizar os dados

...Mas também podemos ter criado um novo problema. Ups!

Vá ao painel usando a conta test, depois execute este comando num terminal para criar uma nova transação:

curl --request POST \
     --header "Content-Type: application/json" \
     --data "{ \"date\": \"2020-07-24\", \"object\": \"Bought book\", \"amount\": -20 }" \
     http://localhost:5000/api/accounts/test/transactions

Experimente atualizar o painel no navegador agora. O que acontece? Vê a nova transação?

O estado é persistido indefinidamente graças ao localStorage, mas isso também significa que nunca é atualizado até sair da aplicação e entrar novamente!

Uma possível estratégia para corrigir isso é recarregar os dados da conta sempre que o painel for carregado, para evitar dados desatualizados.

Tarefa

Crie uma nova função updateAccountData:

async function updateAccountData() {
  const account = state.account;
  if (!account) {
    return logout();
  }

  const data = await getAccount(account.user);
  if (data.error) {
    return logout();
  }

  updateState('account', data);
}

Este método verifica se estamos atualmente autenticados e, em seguida, recarrega os dados da conta a partir do servidor.

Crie outra função chamada refresh:

async function refresh() {
  await updateAccountData();
  updateDashboard();
}

Esta função atualiza os dados da conta e, em seguida, cuida de atualizar o HTML da página do painel. É o que precisamos de chamar quando a rota do painel for carregada. Atualize a definição da rota com:

const routes = {
  '/login': { templateId: 'login' },
  '/dashboard': { templateId: 'dashboard', init: refresh }
};

Experimente recarregar o painel agora, deve exibir os dados da conta atualizados.


🚀 Desafio

Agora que recarregamos os dados da conta sempre que o painel é carregado, acha que ainda precisamos de persistir todos os dados da conta?

Trabalhe em conjunto para alterar o que é guardado e carregado do localStorage para incluir apenas o que é absolutamente necessário para a aplicação funcionar.

Questionário Pós-Aula

Questionário pós-aula

Tarefa

Implementar o diálogo "Adicionar transação"

Aqui está um exemplo do resultado após concluir a tarefa:

Captura de ecrã mostrando um exemplo de diálogo "Adicionar transação"


Aviso Legal:
Este documento foi traduzido utilizando o serviço de tradução por IA Co-op Translator. Embora nos esforcemos para garantir a precisão, é importante notar que traduções automáticas podem conter erros ou imprecisões. O documento original na sua língua nativa deve ser considerado a fonte autoritária. Para informações críticas, recomenda-se a tradução profissional realizada por humanos. Não nos responsabilizamos por quaisquer mal-entendidos ou interpretações incorretas decorrentes da utilização desta tradução.