Elementos Fundamentais do NgRx: Loja, Ações, Redutores, Selecionadores, Efeitos

A loja é o elemento chave em todo o processo de gestão do estado. Ela mantém o estado e facilita a interação entre os componentes e o estado. Você pode obter uma referência para a loja via injeção de dependência angular, como mostrado abaixo.

constructor(private store: Store<AppState>) {}

Esta referência de loja pode ser utilizada posteriormente para duas operações primárias:

  • Para enviar ações para a loja através do método store.dispatch(…), que por sua vez acionará redutores e efeitos
  • Para recuperar o estado da aplicação através de seletores

Estrutura de uma árvore de objetos de estado

Suponha que sua aplicação consiste em dois módulos de recursos chamados Usuário e Produto. Cada um destes módulos lida com diferentes partes do estado geral. As informações do produto serão sempre mantidas na seção products no estado. As informações do usuário serão sempre mantidas na seção user do estado. Estas seções também são chamadas fatias.

Ações

Uma ação é uma instrução que você envia para a loja, opcionalmente com alguns metadados (carga útil). Com base no tipo de ação, a loja decide quais operações devem ser executadas. Em código, uma action é representada por um objeto JavaScript simples e antigo com dois atributos principais, a saber type e payload. payload é um atributo opcional que será usado por redutores para modificar o estado. O seguinte trecho de código e figura ilustra este conceito.

{ "type": "Login Action", "payload": { userProfile: user }}

NgRx versão 8 fornece uma função utilitária chamada createAction para definir os criadores de ações (não ações, mas criadores de ações). A seguir um exemplo de código para isto.

Você pode então usar o login action creator (que é uma função) para construir ações e enviá-las para a loja, como mostrado abaixo. user é o objeto payload que você passa para a action.

this.store.dispatch(login({user}));

Reducers

Reducers são responsáveis por modificar o estado e retornar um novo objeto de estado com as modificações. Os redutores tomam em dois parâmetros, o estado atual e a ação. Com base no tipo de ação recebida, os redutores farão certas modificações no estado atual e produzirão um novo estado. Este conceito é mostrado no diagrama abaixo.

Similar às ações, NgRx fornece uma função utilitária chamada createReducer para criar redutores. Uma típica chamada de função createReducer gostaria do seguinte.

Como você pode ver, ela leva no estado inicial (o estado na inicialização da aplicação) e funções de alteração de um para muitos estados que definem como reagir a diferentes ações. Cada uma destas funções de mudança de estado recebe o estado atual e a ação como parâmetros, e retorna um novo estado.

Efeitos

Efeitos permitem realizar efeitos colaterais quando uma ação é despachada para a loja. Vamos tentar entender isso através de um exemplo. Quando um usuário faz login com sucesso em uma aplicação, uma ação com type Login Action será despachada para a loja com as informações do usuário no arquivo payload. Uma função redutora ouvirá esta ação e modificará o estado com a informação do usuário. Além disso, como efeito secundário, também se deseja guardar a informação do utilizador no armazenamento local do navegador. Um efeito pode ser utilizado para realizar esta tarefa adicional (efeito secundário).

Existem várias formas de criar efeitos no NgRx. A seguir está uma forma bruta e auto-explicativa de criar efeitos. Observe que você geralmente não usa este método para criar efeitos. Tomei isto apenas como exemplo para explicar o que acontece por trás da cortina.

  • actions$ observável irá emitir ações recebidas pela loja. Estes valores irão passar por uma cadeia de operadores.
  • ofType é o primeiro operador utilizado. Este é um operador especial fornecido pela NgRx (Não RxJS) para filtrar as ações com base no seu tipo. Neste caso, somente as ações do tipo login poderão passar pelo resto da cadeia de operadores.
  • tap é o segundo operador utilizado na cadeia para armazenar as informações do usuário no armazenamento local do navegador. O operador tap é geralmente usado para realizar efeitos secundários numa cadeia de operadores.
  • Finalmente, temos de subscrever manualmente o login$ observável.

No entanto, esta abordagem tem um par de grandes desvantagens.

  • Tens de subscrever manualmente o observável, o que não é uma boa prática. Desta forma, você sempre terá que cancelar a inscrição manualmente, o que leva à falta de manutenção.
  • Se aparecer um erro na cadeia do operador, o observável irá errar e deixará de emitir valores (ações) subsequentes. Como resultado, o efeito colateral não será realizado. Portanto, você tem que ter um mecanismo para criar manualmente uma nova instância observável e reativar se um erro ocorrer.

Para superar estes problemas, NgRx fornece uma função utilitária chamada createEffect para criar efeitos. Uma típica createEffect chamada de função seria parecida com a seguinte.

O método createEffect assume uma função que retorna um objeto observável e (opcionalmente) um objeto de configuração como parâmetros.

NgRx lida com a assinatura do observável retornado pela função de suporte, e portanto você não tem que assinar ou cancelar a assinatura manualmente. Além disso, se algum erro ocorrer na cadeia do operador, NgRx criará um novo observável e resubscribe para garantir que o efeito colateral seja sempre executado.

Se dispatch for true (valor padrão) no objeto de configuração, o método createEffect retorna um Observable<Action>. Caso contrário, ele retorna um Observable<Unknown>. Se a propriedade dispatch for true, NgRx irá subscrever o retorno observável de type Observable<Action> e enviar as ações recebidas para a loja.

Se você não estiver mapeando a ação recebida para um tipo diferente de ação na cadeia do operador, você terá que definir dispatch para false. Caso contrário, a execução resultará em um loop infinito, pois a mesma ação será despachada e recebida no fluxo actions$ novamente e novamente. Por exemplo, você não precisa definir dispatch para false no código abaixo porque você mapeia a ação original para um tipo diferente de ação na chain do operador.

No cenário acima,

  • Efeito recebe ações de type loadAllCourses.
  • Uma API é invocada e os cursos são carregados como efeito secundário.
  • A resposta da API a uma ação de type allCoursesLoaded é mapeada e os cursos carregados são passados como o payload à ação.
  • E finalmente, a allCoursesLoaded ação que foi criada é despachada para a loja. Isto é feito pelo NgRx sob o capô.
  • Um redutor ouvirá a ação de entrada allCoursesLoaded e modificará o estado com os cursos carregados.

Selectores

Selectores são funções puras usadas para obter fatias do estado do store. Como mostrado abaixo, você pode consultar o estado mesmo sem usar os seletores. Mas esta abordagem novamente vem com um par de grandes cons.

const isLoggedIn$ = this.store.pipe(map(state => !!state.user));
  • store é uma observação que você pode subscrever. Sempre que a loja receber uma ação, store empurrará o objeto de estado para seus subscritores.
  • Você pode usar funções de mapeamento para obter fatias de estado e realizar qualquer cálculo, se necessário. No exemplo acima, estamos obtendo a user fatia na árvore de objetos de estado e convertendo-a em um booleano para determinar se o usuário fez ou não o login.
  • Você pode assinar manualmente a isLoggedIn$ observável ou usá-la em um modelo angular com pipe assíncrono para ler os valores emitidos.

No entanto, esta abordagem tem uma grande desvantagem. Em geral, a loja recebe ações freqüentemente de diferentes partes da aplicação. Conforme a implementação acima, toda vez que a loja recebe uma ação, um objeto de estado será emitido pela loja. E esse objeto de estado passará novamente pela função de mapeamento e atualizará a IU.

No entanto, se o resultado da função de mapeamento não tiver mudado da última vez, não há necessidade de atualizar a IU novamente. Por exemplo, se o resultado da map(state => !!state.user) não mudou desde a última execução, não temos que empurrar novamente o resultado para a IU/Subscriber. Para conseguir isso, NgRx (não RxJS) introduziu um operador especial chamado select. Com o operador select, o código acima mudará da seguinte forma.

const isLoggedIn$ = this.store.pipe(select(state => !!state.user));

O operador select evitará que os valores sejam empurrados para os subscritores/usuários se o resultado da função de mapeamento não tiver mudado da última vez.

Esta abordagem pode ser melhorada ainda mais. Mesmo que o operador select não empurre valores inalterados para os subscritores/usuários, ele ainda terá que pegar o objeto de estado e fazer o cálculo para derivar o resultado toda vez.

Como já explicado acima, um state será emitido pelo observável quando a loja receber uma ação da aplicação. Uma ação nem sempre atualiza o estado. Se o estado não tiver mudado, o resultado do cálculo da função de mapeamento também não mudará. Portanto não precisamos fazer o cálculo novamente se o objeto emitido state não tiver mudado da última vez. É aqui que os selectores entram em jogo.

Um selector é uma função pura que mantém uma memória das execuções anteriores. Desde que a entrada não tenha mudado, a saída não será recalculada. Ao invés disso, a saída será retornada da memória. Este processo é chamado memoization.

NgRx fornece uma função utilitária chamada createSelector para construir seletores com capacidade de memoization. A seguir está um exemplo da função createSelector utilitário.

A função createSelector assume funções de mapeamento um-para-muitos que dão diferentes fatias do estado e uma função de projetor que realiza o cálculo. A função de projector não será invocada se as fatias do estado não tiverem mudado desde a última execução. Para usar a função seletora criada, você tem que passá-la como um argumento para o operador select.

this.isLoggedIn$ = this.store .pipe( select(isLoggedIn) );

.

Deixe uma resposta

O seu endereço de email não será publicado.