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 tipologin
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 operadortap
é 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 opayload
à 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) );
.