
O que vamos cobrir aqui?
Vamos apresentar a evolução dos redutores nos meus aplicativos Redux / NGRX que ocorreram nos últimos dois anos. Começando pela switch-case
chaves de baunilha, indo para selecionar um redutor de um objeto por chave, finalmente estabelecendo-se com redutores baseados em classe. Não vamos falar apenas sobre como, mas também sobre o porquê.
Se você estiver interessado em trabalhar com muita clichê no Redux / NGRX, consulte este artigo .
Se você já está familiarizado com a seleção de um redutor em uma técnica de mapa, pense em pular direto para redutores baseados em classe .
Caixa de distribuição de baunilha
Então, vamos dar uma olhada em uma tarefa cotidiana de criar uma entidade no servidor de forma assíncrona. Desta vez, sugiro que descrevamos como poderíamos criar um novo jedi.
const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' const reducerJediInitialState = { loading: false,
Deixe-me ser honesto, nunca usei esse tipo de redutor na produção. Meu raciocínio é triplo:
switch-case
apresenta alguns pontos de tensão, tubos com vazamentos, que podemos esquecer de corrigir em algum momento. Sempre poderíamos esquecer de break
se não return
imediatamente, sempre poderíamos esquecer de adicionar o default
, que precisamos adicionar a cada redutor.switch-case
possui algum código padrão, que não adiciona nenhum contexto.switch-case
é O (n), mais ou menos. Não é um argumento sólido por si só, porque o Redux não tem muito desempenho, mas deixa meu perfeccionista interior louco.
O próximo passo lógico sugerido pela documentação oficial do Redux é escolher um redutor de um objeto por chave.
Selecionando um redutor de um objeto por chave
A ideia é simples. Cada transformação de estado é uma função do estado e da ação e possui um tipo de ação correspondente. Considerando que cada tipo de ação é uma string, poderíamos criar um objeto, onde cada chave é um tipo de ação e cada valor é uma função que transforma o estado (um redutor). Então, poderíamos escolher um redutor necessário desse objeto por chave, que é O (1), quando recebermos uma nova ação.
const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' const reducerJediInitialState = { loading: false, data: [], error: undefined, } const reducerJediMap = { [actionTypeJediCreateInit]: (state) => ({ ...state, loading: true, }), [actionTypeJediCreateSuccess]: (state, action) => ({ loading: false, data: [...state.data, action.payload], error: undefined, }), [actionTypeJediCreateError]: (state, action) => ({ ...state, loading: false, error: action.payload, }), } const reducerJedi = (state = reducerJediInitialState, action) => {
O legal aqui é que a lógica dentro do reducerJedi
permanece a mesma para qualquer redutor, o que significa que podemos reutilizá-lo. Existe até uma pequena biblioteca, chamada redux-create-reducer , que faz exatamente isso. Faz com que o código fique assim:
import { createReducer } from 'redux-create-reducer' const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' const reducerJediInitialState = { loading: false, data: [], error: undefined, } const reducerJedi = createReducer(reducerJediInitialState, { [actionTypeJediCreateInit]: (state) => ({ ...state, loading: true, }), [actionTypeJediCreateSuccess]: (state, action) => ({ loading: false, data: [...state.data, action.payload], error: undefined, }), [actionTypeJediCreateError]: (state, action) => ({ ...state, loading: false, error: action.payload, }), })
Bonito e bonito, hein? Embora isso ainda tenha algumas ressalvas:
- No caso de redutores complexos, temos que deixar muitos comentários descrevendo o que esse redutor faz e por quê.
- Mapas redutores enormes são difíceis de ler.
- Cada redutor possui apenas um tipo de ação correspondente. E se eu quiser executar o mesmo redutor para várias ações?
O redutor baseado em classe tornou-se meu galpão de luz no reino da noite.
Redutores baseados em classe
Desta vez, deixe-me começar com por que dessa abordagem:
- Os métodos de classe serão nossos redutores e os métodos terão nomes, o que é uma meta-informação útil, e poderíamos abandonar comentários em 90% dos casos.
- Os métodos da classe podem ser decorados, o que é uma maneira declarativa de fácil leitura para combinar ações e redutores.
- Ainda poderíamos usar um mapa de ações sob o capô para ter complexidade O (1).
Se isso soa como uma lista razoável de razões para você, vamos nos aprofundar!
Antes de mais, gostaria de definir o que queremos obter como resultado.
const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' class ReducerJedi {
Agora, como vemos onde queremos chegar, podemos fazê-lo passo a passo.
Etapa 1. Decorador de ação .
O que queremos fazer aqui é aceitar qualquer número de tipos de ação e armazená-los como meta-informações para que o método de uma classe possa ser usado posteriormente. Para fazer isso, podemos utilizar o polyfill reflect-metadata , que traz funcionalidade de metadados ao objeto Reflect . Depois disso, esse decorador apenas anexaria seus argumentos (tipos de ação) a um método como meta-dados.
const METADATA_KEY_ACTION = 'reducer-class-action-metadata' export const Action = (...actionTypes) => (target, propertyKey, descriptor) => { Reflect.defineMetadata(METADATA_KEY_ACTION, actionTypes, target, propertyKey) }
Etapa 2. Criando uma função redutora a partir de uma classe redutora
Como sabemos, cada redutor é uma função pura que aceita um estado e uma ação e retorna um novo estado. Bem, classe também é uma função, mas as classes ES6 não podem ser invocadas sem new
e temos que fazer um redutor real de uma classe com alguns métodos de qualquer maneira. Então, precisamos transformá-lo de alguma forma.
Precisamos de uma função que leve nossa classe, percorra cada método, colete metadados com tipos de ação, construa um mapa redutor e crie um redutor final a partir desse mapa redutor.
Veja como poderíamos examinar cada método de uma classe.
const getReducerClassMethodsWthActionTypes = (instance) => {
Agora queremos processar a coleção recebida em um mapa redutor.
const getReducerMap = (methodsWithActionTypes) => methodsWithActionTypes.reduce((reducerMap, { method, actionType }) => { reducerMap[actionType] = method return reducerMap }, {})
Portanto, a função final pode se parecer com isso.
import { createReducer } from 'redux-create-reducer' const createClassReducer = (ReducerClass) => { const reducerClass = new ReducerClass() const methodsWithActionTypes = getReducerClassMethodsWthActionTypes( reducerClass, ) const reducerMap = getReducerMap(methodsWithActionTypes) const initialState = reducerClass.initialState const reducer = createReducer(initialState, reducerMap) return reducer }
E poderíamos aplicá-lo à nossa classe ReducerJedi
assim.
const reducerJedi = createClassReducer(ReducerJedi)
Etapa 3. Mesclando tudo.
Próximas etapas
Aqui está o que perdemos:
- E se a mesma ação corresponder a vários métodos? A lógica atual não lida com isso.
- Podemos adicionar immer ?
- E se eu usar ações baseadas em classe? Como eu poderia passar um criador de ações, não um tipo de ação?
Tudo isso com exemplos e exemplos de código adicionais é coberto pela classe redutor .
Devo dizer que usar classes para redutores não é um pensamento original. O @amcdnl surgiu com incríveis ações do ngrx há algum tempo, mas parece que agora ele está focado no NGXS , sem mencionar que eu queria uma digitação e dissociação mais estritas da lógica específica do Angular. Aqui está uma lista das principais diferenças entre classe redutora e ações ngrx.
Se você gosta da ideia de usar classes para seus redutores, pode fazer o mesmo com seus criadores de ações. Dê uma olhada na classe de ação e fluxo .
Felizmente, você encontrou algo útil para o seu projeto. Sinta-se livre para me comunicar seus comentários! Certamente aprecio qualquer crítica e pergunta.