Redux: Recomendações de práticas recomendadas sobre criadores de ação, redutores e seletores

Criado em 22 dez. 2015  ·  106Comentários  ·  Fonte: reduxjs/redux

Minha equipe está usando o Redux há alguns meses. Ao longo do caminho, ocasionalmente me peguei pensando em um recurso e me perguntando "isso pertence a um criador ou redutor de ação?". A documentação parece um pouco vaga sobre esse fato. (Ou talvez eu tenha perdido onde está coberto, nesse caso peço desculpas.) Mas conforme eu escrevo mais códigos e mais testes, eu tenho opiniões mais fortes sobre onde as coisas _devem_ estar e achei que valeria a pena compartilhar e discutir com outras pessoas.

Então aqui estão meus pensamentos.

Use seletores em qualquer lugar

Este primeiro não está estritamente relacionado ao Redux, mas vou compartilhá-lo de qualquer maneira, já que está indiretamente mencionado abaixo. Minha equipe usa rackt / reselect . Normalmente definimos um arquivo que exporta seletores para um determinado nó de nossa árvore de estado (por exemplo, MyPageSelectors). Nossos contêineres "inteligentes" usam esses seletores para parametrizar nossos componentes "burros".

Com o tempo, percebemos que há um benefício adicional em usar esses mesmos seletores em outros lugares (não apenas no contexto de nova seleção). Por exemplo, nós os usamos em testes automatizados. Nós também os usamos em thunks retornados por criadores de ação (mais abaixo).

Portanto, minha primeira recomendação é - use seletores compartilhados _everywhere_- mesmo ao acessar dados de forma síncrona (por exemplo, prefira myValueSelector(state) vez de state.myValue ). Isso reduz a probabilidade de variáveis ​​digitadas incorretamente que levam a valores indefinidos sutis, simplifica as alterações na estrutura de sua loja, etc.

Faça _mais_ em criadores de ação e _menos_ em redutores

Acho que este é muito importante, embora possa não ser imediatamente óbvio. A lógica de negócios pertence aos criadores de ações. Os redutores devem ser estúpidos e simples. Em muitos casos individuais, não importa, mas a consistência é boa e por isso é melhor _consistentemente_ fazer isso. Existem alguns motivos pelos quais:

  1. Os criadores de ações podem ser assíncronos por meio do uso de middleware como redux-thunk . Como seu aplicativo frequentemente exigirá atualizações assíncronas em sua loja, alguma "lógica de negócios" acabará em suas ações.
  2. Os criadores de ações (mais precisamente os thunks que eles retornam) podem usar seletores compartilhados porque têm acesso ao estado completo. Os redutores não podem porque eles só têm acesso ao seu nó.
  3. Usando redux-thunk , um único criador de ação pode despachar várias ações - o que torna as atualizações de estado complicadas mais simples e encoraja uma melhor reutilização de código.

Imagine que seu estado possui metadados relacionados a uma lista de itens. Cada vez que um item é modificado, adicionado ou removido da lista, os metadados precisam ser atualizados. A "lógica de negócios" para manter a lista e seus metadados sincronizados pode residir em alguns lugares:

  1. Nos redutores. Cada redutor (adicionar, editar, remover) é responsável por atualizar a lista _ assim como_ os metadados.
  2. Nas visualizações (container / componente). Cada visão que invoca uma ação (adicionar, editar, remover) também é responsável por invocar uma ação updateMetadata . Essa abordagem é terrível por (espero) razões óbvias.
  3. Nos criadores de ação. Cada criador de ação (adicionar, editar, remover) retorna uma conversão que despacha uma ação para atualizar a lista e, em seguida, outra ação para atualizar os metadados.

Dadas as opções acima, a opção 3 é solidamente melhor. Ambas as opções 1 e 3 suportam compartilhamento de código limpo, mas apenas a opção 3 suporta o caso em que as atualizações de lista e / ou metadados podem ser assíncronas. (Por exemplo, talvez dependa de um trabalhador da web.)

Escreva testes "patos" que se concentrem em ações e seletores

A maneira mais eficiente de testar ações, redutores e seletores é seguir a abordagem "ducks" ao escrever testes. Isso significa que você deve escrever um conjunto de testes que cubra um determinado conjunto de ações, redutores e seletores, em vez de 3 conjuntos de testes que se concentrem em cada um individualmente. Isso simula com mais precisão o que acontece em sua aplicação real e fornece o melhor retorno para o investimento.

Dividindo ainda mais, descobri que é útil escrever testes que se concentram nos criadores de ação e, em seguida, verificar o resultado usando seletores. (Não teste os redutores diretamente.) O que importa é que uma determinada ação resulte no estado que você espera. Verificar esse resultado usando seus seletores (compartilhados) é uma maneira de cobrir todos os três em uma única passagem.

discussion

Comentários muito úteis

@dtinth @ denis-sokolov Também concordo com você nisso. A propósito, quando eu estava me referindo ao projeto da saga redux, talvez não tenha deixado claro que sou contra a ideia de fazer os actionCreators crescerem e se tornarem cada vez mais complexos com o tempo.

O projeto Redux-saga também é uma tentativa de fazer o que você está descrevendo como @dtinth, mas há uma diferença sutil com o que vocês dois dizem. Parece que você quer dizer que, se escrever todos os eventos brutos que aconteceram no log de ações, você poderá calcular facilmente qualquer estado dos redutores desse log de ações. Isso é absolutamente verdade, e eu tenho seguido esse caminho por um tempo até que meu aplicativo se tornou muito difícil de manter porque o log de ação se tornou não explícito e os redutores muito complexos com o tempo.

Talvez você possa olhar para este ponto da discussão original que levou à discussão da saga Redux: https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment -162822909

Caso de uso para resolver

Imagine que você tenha um aplicativo Todo, com o evento TodoCreated óbvio. Em seguida, pedimos que você codifique a integração de um aplicativo. Depois que o usuário cria um todo, devemos parabenizá-lo com um pop-up.

A maneira "impura":

Isso é o que @bvaughn parece preferir

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
       if ( getState().isOnboarding ) {
         dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
       }
   }
}

Não gosto dessa abordagem porque torna o criador da ação altamente acoplado ao layout da visualização do aplicativo. Ele assume que o actionCreator deve conhecer a estrutura da árvore de estados da IU para tomar sua decisão.

A maneira de "calcular tudo a partir de eventos brutos":

Isso é o que @ denis-sokolov @dtinth parece preferir:

function onboardingTodoCreateCongratulationReducer(state = defaultState, action) {
  var isOnboarding = isOnboardingReducer(state.isOnboarding,action);
  switch (action) {
    case "TodoCreated": 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: isOnboarding}
    default: 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: false}
  }
}

Sim, você pode criar um redutor que sabe se o parabéns deve ser exibido. Mas então você tem um pop-up que será exibido sem nem mesmo uma ação informando que o pop-up foi exibido. Em minha própria experiência fazendo isso (e ainda tenho código legado fazendo isso), é sempre melhor deixar bem explícito: NUNCA exiba o pop-up de parabéns se nenhuma ação DISPLAY_CONGRATULATION for disparada. Explícito é muito mais fácil de manter do que implícito.

A maneira simplificada da saga.

A saga redux usa geradores e pode parecer um pouco complicada se você não estiver acostumado, mas basicamente com uma implementação simplificada, você escreveria algo como:

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
   }
}

function onboardingSaga(state, action, actionCreators) {
  switch (action) {
    case "OnboardingStarted": 
        return {onboarding: true, ...state};
    case "OnboardingStarted": 
        return {onboarding: false, ...state};
    case "TodoCreated": 
        if ( state.onboarding ) dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
        return state;
    default: 
        return state;
  }
}

A saga é um ator com estado que recebe eventos e pode produzir efeitos. Aqui, ele é implementado como um redutor impuro para dar uma ideia do que é, mas na verdade não está no projeto da saga redux.

Complicando um pouco as regras:

Se você cuidar da regra inicial, ela não é muito explícita sobre tudo.
Se você observar as implementações acima, notará que o pop-up de parabéns abre sempre que criamos uma tarefa durante a integração. Provavelmente, queremos que ele seja aberto apenas para a primeira tarefa criada que acontecer durante a integração e não para todas elas. Além disso, queremos permitir que o usuário eventualmente refaça a integração desde o início.

Você pode ver como o código se tornaria confuso em todas as 3 implementações ao longo do tempo, conforme a integração se torna cada vez mais complicada?

O jeito da saga redux

Com a saga redux e as regras de integração acima, você escreveria algo como

function* onboarding() {
  while ( true ) {
    take(ONBOARDING_STARTED)
    take(TODO_CREATED)
    put(SHOW_TODO_CREATION_CONGRATULATION)
    take(ONBOARDING_ENDED)
  }
}

Acho que resolve esse caso de uso de uma maneira muito mais simples do que as soluções acima. Se eu estiver errado, por favor, me dê sua implementação mais simples :)

Você falou sobre código impuro e, neste caso, não há impureza na implementação da saga Redux porque os efeitos take / put são na verdade dados. Quando take () é chamado, ele não executa, ele retorna um descritor do efeito a ser executado e, em algum ponto, um interpretador entra em ação, então você não precisa de nenhum mock para testar as sagas. Se você é um desenvolvedor funcional fazendo Haskell, pense em mônadas grátis / IO.


Neste caso, permite:

  • Evite complicar actionCreator e torná-lo dependente de getState
  • Torne o implícito mais explícito
  • Evite o acoplamento de lógica transversal (como a integração acima) ao domínio de seu negócio principal (criação de tarefas)

Ele também pode fornecer uma camada de interpretação, permitindo traduzir eventos brutos em eventos mais significativos / de alto nível (um pouco como o ELM faz ao empacotar os eventos conforme eles surgem).

Exemplos:

  • "TIMELINE_SCROLLED_NEAR-BOTTOM" pode levar a "NEXT_PAGE_LOADED"
  • "REQUEST_FAILED" pode levar a "USER_DISCONNECTED" se o código de erro for 401.
  • "HASHTAG_FILTER_ADDED" pode levar a "CONTENT_RELOADED"

Se você deseja obter um layout de aplicativo modular com patos, pode evitar o acoplamento de patos. A saga se torna o ponto de acoplamento. Os patos precisam apenas saber de seus eventos brutos, e a saga interpreta esses eventos brutos. Isso é muito melhor do que ter duck1 despachando ações diretamente de duck2 porque torna o projeto duck1 mais fácil de reutilizar em outro contexto. Pode-se, no entanto, argumentar que o ponto de acoplamento também pode estar em actionCreators e isso é o que a maioria das pessoas está fazendo hoje.

Todos 106 comentários

Curioso se você usa Immutable.js ou outro. No punhado de coisas redux que construí, eu não poderia imaginar não usar imutável, mas _não_ tenho uma estrutura profundamente aninhada que Imutável ajuda a domar.

Uau. Que descuido para mim _não_ mencionar isso. Sim! Usamos Imutável! Como você disse, é difícil imaginar _não_ usá-lo para algo substancial.

@bvaughn Uma área com a qual tenho lutado é onde traçar a linha entre Imutável e os componentes. Passar objetos imutáveis ​​para os Componentes permite que você use decoradores / mixins de renderização pura com muita facilidade, mas então você acaba com código IMmutável em seus componentes (o que eu não gosto). Até agora, acabei de descobrir e fazer isso, mas suspeito que você usa seletores nos métodos render () em vez de acessar diretamente os métodos de Immutable.js.

Para ser honesto, isso é algo sobre o qual ainda não definimos uma política rígida. Freqüentemente, usamos seletores em nossos contêineres "inteligentes" para valores nativos extras de nossos objetos imutáveis ​​e, em seguida, passamos os valores nativos para nossos componentes como strings, booleanos, etc. Ocasionalmente, passaremos um objeto Imutável, mas quando o fazemos - quase sempre passe um tipo Record para que o componente possa tratá-lo como um objeto nativo (com getters).

Tenho me movido na direção oposta, tornando os criadores de ação mais triviais. Mas estou apenas começando com o redux. Algumas perguntas sobre sua abordagem:

1) Como você testa seus criadores de ação? Gosto de mover o máximo de lógica possível para funções síncronas puras que não dependem de serviços externos porque é mais fácil de testar.
2) Você usa a viagem no tempo com recarga a quente? Uma das coisas interessantes com o redux devtools com react é que quando o recarregamento a quente é configurado, o armazenamento irá executar novamente todas as ações contra o novo redutor. Se eu movesse minha lógica para os criadores de ação, eu perderia isso.
3) Se seus criadores de ação despacharem várias vezes para produzir um efeito, isso significa que seu estado está brevemente em um estado inválido? (Estou pensando aqui em vários despachos síncronos, não aqueles despachados de forma assíncrona em um ponto posterior)

Use seletores em qualquer lugar

Sim, isso parece dizer que seus redutores são um detalhe de implementação de seu estado e que você expõe seu estado ao seu componente por meio de uma API de consulta.
Como qualquer interface, ela permite desacoplar e facilitar a refatoração do estado.

Use ImmutableJS

IMO com a nova sintaxe JS não é mais tão útil usar ImmutableJS, pois você pode facilmente modificar listas e objetos com JS normal. A menos que você tenha listas e objetos muito grandes com muitas propriedades e precise de compartilhamento estrutural por motivos de desempenho, ImmutableJS não é um requisito estrito.

Faça mais em actionCreators

@bvaughn você realmente deveria dar uma olhada neste projeto: https://github.com/yelouafi/redux-saga
Quando comecei a discutir sobre sagas (inicialmente conceito de backend) para @yelouafi , era para resolver esse tipo de problema. No meu caso, tentei primeiro usar sagas enquanto conectava um usuário à integração em um aplicativo existente.

1) Como você testa seus criadores de ação? Gosto de mover o máximo de lógica possível para funções síncronas puras que não dependem de serviços externos porque é mais fácil de testar.

Tentei descrever isso acima, mas basicamente ... acho que faz mais sentido (para mim até agora) testar seus criadores de ação com uma abordagem semelhante a "patos". Comece um teste despachando o resultado de um criador de ação e, em seguida, verifique o estado usando seletores. Desta forma, com um único teste, você pode cobrir o criador da ação, seu (s) redutor (es) e todos os seletores relacionados.

2) Você usa a viagem no tempo com recarga a quente? Uma das coisas interessantes com o redux devtools com react é que quando o recarregamento a quente é configurado, o armazenamento irá executar novamente todas as ações contra o novo redutor. Se eu movesse minha lógica para os criadores de ação, eu perderia isso.

Não, não usamos viagem no tempo. Mas por que sua lógica de negócios sendo um criador de ações teria algum impacto aqui? A única coisa que atualiza o estado do seu aplicativo são os redutores. E assim, a reexecução das ações criadas alcançaria o mesmo resultado de qualquer maneira.

3) Se seus criadores de ação despacharem várias vezes para produzir um efeito, isso significa que seu estado está brevemente em um estado inválido? (Estou pensando aqui em vários despachos síncronos, não aqueles despachados de forma assíncrona em um ponto posterior)

O estado inválido transitório é algo que você realmente não pode evitar em alguns casos. Contanto que haja consistência eventual, geralmente não é um problema. E, novamente, seu estado pode ser temporariamente inválido, independentemente de sua lógica de negócios estar nos criadores ou redutores da ação. Tem mais a ver com efeitos colaterais e especificidades de sua loja.

IMO com a nova sintaxe JS não é mais tão útil usar ImmutableJS, pois você pode facilmente modificar listas e objetos com JS normal. A menos que você tenha listas e objetos muito grandes com muitas propriedades e precise de compartilhamento estrutural por motivos de desempenho, ImmutableJS não é um requisito estrito.

Os principais motivos para usar Immutable (aos meus olhos) não são desempenho ou açúcar sintático para atualizações. O principal motivo é que ele evita que você (ou outra pessoa) _acidentalmente_ mude seu estado de entrada dentro de um redutor. Isso é um não-não e, infelizmente, é fácil de fazer com objetos JS simples.

@bvaughn você realmente deveria dar uma olhada neste projeto: https://github.com/yelouafi/redux-saga
Quando comecei a discutir sobre sagas (inicialmente conceito de backend) para @yelouafi , era para resolver esse tipo de problema. No meu caso, tentei primeiro usar sagas enquanto conectava um usuário à integração em um aplicativo existente.

Na verdade, eu verifiquei esse projeto antes :) Embora ainda não o tenha usado. Parece legal.

Tentei descrever isso acima, mas basicamente ... acho que faz mais sentido (para mim até agora) testar seus criadores de ação com uma abordagem semelhante a "patos". Comece um teste despachando o resultado de um criador de ação e, em seguida, verifique o estado usando seletores. Desta forma, com um único teste, você pode cobrir o criador da ação, seu (s) redutor (es) e todos os seletores relacionados.

Desculpe, eu entendi essa parte. O que eu queria saber é a parte dos testes que interage com a assincronicidade. Posso escrever um teste mais ou menos assim:

var store = createStore();
store.dispatch(actions.startRequest());
store.dispatch(actions.requestResponseReceived({...});
strictEqual(isLoaded(store.getState());

Mas como é o seu teste? Algo assim?

var mock = mockFetch();
store.dispatch(actions.request());
mock.expect("/api/foo.bar").andRespond("{status: OK}");
strictEqual(isLoaded(store.getState());

Não, não usamos viagem no tempo. Mas por que sua lógica de negócios sendo um criador de ações teria algum impacto aqui? A única coisa que atualiza o estado do seu aplicativo são os redutores. E assim, a reexecução das ações criadas alcançaria o mesmo resultado de qualquer maneira.

E se o código for alterado? Se eu mudar o redutor, as mesmas ações serão repetidas, mas com o novo redutor. Ao passo que, se eu mudar o criador da ação, as novas versões não serão reproduzidas. Portanto, considere dois cenários:

Com um redutor:

1) Tento uma ação em meu aplicativo.
2) Há um bug no meu redutor, resultando no estado errado.
3) Corrijo o bug no redutor e salvo
4) A viagem no tempo carrega o novo redutor e me coloca no estado em que deveria estar.

Enquanto com um criador de ação

1) Tento uma ação em meu aplicativo.
2) Há um bug no criador da ação, resultando na criação da ação errada
3) Corrijo o bug no criador da ação e salvo
4) Ainda estou no estado incorreto, o que exigirá que eu pelo menos tente a ação novamente e, possivelmente, atualize se isso me colocar em um estado completamente quebrado.

O estado inválido transitório é algo que você realmente não pode evitar em alguns casos. Contanto que haja consistência eventual, geralmente não é um problema. E, novamente, seu estado pode ser temporariamente inválido, independentemente de sua lógica de negócios estar nos criadores ou redutores da ação. Tem mais a ver com efeitos colaterais e especificidades de sua loja.

Acho que minha maneira de pensar sobre redux insiste que a loja está sempre em um estado válido. O redutor sempre assume um estado válido e produz um estado válido. Que casos você acha que força alguém a permitir alguns estados inconsistentes?

Desculpe pela interrupção, mas o que você quer dizer com estados inválidos e válidos
aqui? Dados sendo carregados ou ação sendo executada, mas ainda não concluída, parece
como um estado transitório válido para mim.

O que você quer dizer com estado transiente no Redux @bvaughn e @sompylasar ? Ou o despacho termina ou joga. Se lançar, o estado não muda.

A menos que seu redutor tenha problemas de código, Redux só tem estados que são consistentes com a lógica do redutor. De alguma forma, todas as ações despachadas são tratadas em uma transação: ou a árvore inteira é atualizada ou o estado não muda de forma alguma.

Se toda a árvore for atualizada, mas não de maneira apropriada (como um estado que o React não consegue renderizar), é só que você não fez seu trabalho corretamente :)

No Redux, o estado atual é considerar que um único despacho é um limite de transação.

No entanto, eu entendo a preocupação de @winstonewert que parece querer despachar 2 ações de forma síncrona em uma mesma transação. Porque às vezes actionCreators despacha várias ações e espera que todas as ações sejam executadas corretamente. Se 2 ações forem despachadas e a segunda falhar, então apenas a 1ª será aplicada, levando a um estado que poderíamos considerar "inconsistente". Talvez @winstonewert deseje que, se o despacho da 2ª ação falhar, então reverteremos as 2 ações.

@winstonewert Implementei algo assim em nossa estrutura interna aqui e funciona bem até agora: https://github.com/stample/atom-react/blob/master/src/atom/atom.js
Eu também queria lidar com erros de renderização: se um estado não pudesse ser renderizado com sucesso, eu queria que meu estado fosse revertido para evitar o bloqueio da IU. Infelizmente, até a próxima versão, o React faz um péssimo trabalho quando os métodos de renderização geram erros, então não foi muito útil, mas pode ser no futuro.

Tenho certeza de que podemos permitir que uma loja aceite vários despachos de sincronização em uma transação com um middleware.

No entanto, não tenho certeza se seria possível reverter o estado em caso de erro de renderização, pois geralmente o redux store já "confirmou" quando tentamos renderizar seu estado. Em minha estrutura, há um gancho "beforeTransactionCommit" que uso para acionar a renderização e, eventualmente, reverter em qualquer erro de renderização.

@gaearon Gostaria de saber se você planeja oferecer suporte a esses tipos de recursos e se isso seria possível com a API atual.

Parece-me que redux-batched-subscribe não permite fazer transações reais, mas apenas reduzir o número de renderizações. O que vejo é que o armazenamento "confirma" após cada despacho, mesmo que o listener de assinatura seja acionado apenas uma vez no final

Por que precisamos de suporte completo para transações? Acho que não entendo o caso de uso.

@gaearon Não tenho certeza ainda, mas ficaria feliz em saber mais sobre @winstonewert usecase.

A ideia é que você poderia fazer dispatch([a1,a2]) e se a2 falhar, então voltamos ao estado anterior ao despacho de a1.

No passado, costumava despachar várias ações de forma síncrona (em um único ouvinte onClick, por exemplo, ou em um actionCreator) e implementar transações principalmente como uma forma de chamar a renderização apenas no final de todas as ações despachadas, mas isso tem sido resolvido de uma maneira diferente pelo projeto redux-batched-subscribe.

Em meus casos de uso, as ações que usei para disparar em uma transação eram principalmente para evitar renderizações desnecessárias, mas as ações faziam sentido de forma independente, portanto, mesmo que o despacho falhasse para a 2ª ação, não reverter a 1ª ação ainda me daria um estado consistente ( mas talvez não aquele que foi planejado ...). Eu realmente não sei se alguém pode inventar um caso de uso em que uma reversão completa seria útil

No entanto, quando a renderização falha, não faz sentido tentar retroceder para o último estado para o qual a renderização não falha, em vez de tentar fazer progresso em um estado não renderizável?

Um realçador redutor simples funcionaria? por exemplo

const enhanceReducerWithTheAbilityToConsumeMultipleActions = (reducer =>
  (state, actions) => (typeof actions.reduce === 'function'
    ? actions.reduce(reducer, state)
    : reducer(state, actions)
  )
)

Com isso, você pode enviar um array para a loja. O intensificador descompacta a ação individual e a alimenta para cada redutor.

Ohh @gaearon eu não sabia disso. Não percebi que havia 2 projetos distintos que tentavam resolver um caso de uso bastante semelhante de maneiras diferentes:

Ambos permitirão evitar renderizações desnecessárias, mas o primeiro iria reverter todas as ações em lote, enquanto o segundo apenas não aplicaria a ação com falha.

@gaearon Ai, meu mal por não olhar para isso. : flushed:


Criadores de ação representam código impuro

Provavelmente não tive tanta experiência prática com o Redux quanto a maioria das pessoas, mas à primeira vista, tenho que discordar de "fazer mais em criadores de ação e fazer menos em redutores", tive uma discussão semelhante em nosso empresa.

Em Hacker Way: Repensando o desenvolvimento de aplicativos da Web no Facebook, onde o padrão Flux é introduzido, o próprio problema que leva à invenção do Flux é o código imperativo.

Nesse caso, um Action Creator que faz E / S é esse código imperativo.

Não estamos usando o Redux no trabalho, mas onde eu trabalho, costumávamos ter ações refinadas (que, é claro, todas fazem sentido por conta própria) e acioná-las em um lote. Por exemplo, quando você clica em uma mensagem, três ações são acionadas: OPEN_MESSAGE_VIEW , FETCH_MESSAGE , MARK_NOTIFICATION_AS_READ .

Então, verifica-se que essas ações de “baixo nível” não são mais do que um “comando” ou “setter” ou “mensagem” para definir algum valor dentro de uma loja. Podemos também voltar e usar MVC e acabar com um código mais simples se continuarmos fazendo assim.

Em certo sentido, Action Creators representam código impuro, enquanto Redutores (e Seletores) representam código puro . O pessoal de Haskell descobriu que é melhor ter um código menos impuro e um código mais puro .

Por exemplo, no meu projeto paralelo (usando Redux), eu uso a API de reconhecimento de voz do webkit. Ele emite onresult evento enquanto você fala. Existem duas opções - onde esses eventos são processados?

  • Faça com que o criador da ação processe o evento primeiro e, em seguida, envie-o para a loja.
  • Basta enviar o objeto de evento para a loja.

Eu escolhi o número dois: basta enviar o objeto de evento bruto para a loja.

No entanto, parece que Redux dev-tools não gosta quando objetos não-simples são enviados para a loja, então adicionei uma pequena lógica no criador de ação para transformar esses objetos de evento em objetos simples. (O código no criador da ação é tão trivial que não pode dar errado.)

Em seguida, o redutor pode combinar esses eventos muito primitivos e construir a transcrição do que é falado. Como essa lógica reside dentro do código puro, posso facilmente ajustá-la ao vivo (recarregando a quente o redutor).

Eu gostaria de apoiar @dtinth. As ações devem representar eventos que aconteceram no mundo real, não como queremos reagir a esses eventos. Em particular, consulte CQRS: queremos registrar o máximo de detalhes sobre eventos da vida real e, provavelmente, os redutores serão aprimorados no futuro e processarão eventos antigos com uma nova lógica.

@dtinth @ denis-sokolov Também concordo com você nisso. A propósito, quando eu estava me referindo ao projeto da saga redux, talvez não tenha deixado claro que sou contra a ideia de fazer os actionCreators crescerem e se tornarem cada vez mais complexos com o tempo.

O projeto Redux-saga também é uma tentativa de fazer o que você está descrevendo como @dtinth, mas há uma diferença sutil com o que vocês dois dizem. Parece que você quer dizer que, se escrever todos os eventos brutos que aconteceram no log de ações, você poderá calcular facilmente qualquer estado dos redutores desse log de ações. Isso é absolutamente verdade, e eu tenho seguido esse caminho por um tempo até que meu aplicativo se tornou muito difícil de manter porque o log de ação se tornou não explícito e os redutores muito complexos com o tempo.

Talvez você possa olhar para este ponto da discussão original que levou à discussão da saga Redux: https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment -162822909

Caso de uso para resolver

Imagine que você tenha um aplicativo Todo, com o evento TodoCreated óbvio. Em seguida, pedimos que você codifique a integração de um aplicativo. Depois que o usuário cria um todo, devemos parabenizá-lo com um pop-up.

A maneira "impura":

Isso é o que @bvaughn parece preferir

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
       if ( getState().isOnboarding ) {
         dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
       }
   }
}

Não gosto dessa abordagem porque torna o criador da ação altamente acoplado ao layout da visualização do aplicativo. Ele assume que o actionCreator deve conhecer a estrutura da árvore de estados da IU para tomar sua decisão.

A maneira de "calcular tudo a partir de eventos brutos":

Isso é o que @ denis-sokolov @dtinth parece preferir:

function onboardingTodoCreateCongratulationReducer(state = defaultState, action) {
  var isOnboarding = isOnboardingReducer(state.isOnboarding,action);
  switch (action) {
    case "TodoCreated": 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: isOnboarding}
    default: 
        return {isOnboarding: isOnboarding, isCongratulationDisplayed: false}
  }
}

Sim, você pode criar um redutor que sabe se o parabéns deve ser exibido. Mas então você tem um pop-up que será exibido sem nem mesmo uma ação informando que o pop-up foi exibido. Em minha própria experiência fazendo isso (e ainda tenho código legado fazendo isso), é sempre melhor deixar bem explícito: NUNCA exiba o pop-up de parabéns se nenhuma ação DISPLAY_CONGRATULATION for disparada. Explícito é muito mais fácil de manter do que implícito.

A maneira simplificada da saga.

A saga redux usa geradores e pode parecer um pouco complicada se você não estiver acostumado, mas basicamente com uma implementação simplificada, você escreveria algo como:

function createTodo(todo) {
   return (dispatch, getState) => {
       dispatch({type: "TodoCreated",payload: todo});
   }
}

function onboardingSaga(state, action, actionCreators) {
  switch (action) {
    case "OnboardingStarted": 
        return {onboarding: true, ...state};
    case "OnboardingStarted": 
        return {onboarding: false, ...state};
    case "TodoCreated": 
        if ( state.onboarding ) dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
        return state;
    default: 
        return state;
  }
}

A saga é um ator com estado que recebe eventos e pode produzir efeitos. Aqui, ele é implementado como um redutor impuro para dar uma ideia do que é, mas na verdade não está no projeto da saga redux.

Complicando um pouco as regras:

Se você cuidar da regra inicial, ela não é muito explícita sobre tudo.
Se você observar as implementações acima, notará que o pop-up de parabéns abre sempre que criamos uma tarefa durante a integração. Provavelmente, queremos que ele seja aberto apenas para a primeira tarefa criada que acontecer durante a integração e não para todas elas. Além disso, queremos permitir que o usuário eventualmente refaça a integração desde o início.

Você pode ver como o código se tornaria confuso em todas as 3 implementações ao longo do tempo, conforme a integração se torna cada vez mais complicada?

O jeito da saga redux

Com a saga redux e as regras de integração acima, você escreveria algo como

function* onboarding() {
  while ( true ) {
    take(ONBOARDING_STARTED)
    take(TODO_CREATED)
    put(SHOW_TODO_CREATION_CONGRATULATION)
    take(ONBOARDING_ENDED)
  }
}

Acho que resolve esse caso de uso de uma maneira muito mais simples do que as soluções acima. Se eu estiver errado, por favor, me dê sua implementação mais simples :)

Você falou sobre código impuro e, neste caso, não há impureza na implementação da saga Redux porque os efeitos take / put são na verdade dados. Quando take () é chamado, ele não executa, ele retorna um descritor do efeito a ser executado e, em algum ponto, um interpretador entra em ação, então você não precisa de nenhum mock para testar as sagas. Se você é um desenvolvedor funcional fazendo Haskell, pense em mônadas grátis / IO.


Neste caso, permite:

  • Evite complicar actionCreator e torná-lo dependente de getState
  • Torne o implícito mais explícito
  • Evite o acoplamento de lógica transversal (como a integração acima) ao domínio de seu negócio principal (criação de tarefas)

Ele também pode fornecer uma camada de interpretação, permitindo traduzir eventos brutos em eventos mais significativos / de alto nível (um pouco como o ELM faz ao empacotar os eventos conforme eles surgem).

Exemplos:

  • "TIMELINE_SCROLLED_NEAR-BOTTOM" pode levar a "NEXT_PAGE_LOADED"
  • "REQUEST_FAILED" pode levar a "USER_DISCONNECTED" se o código de erro for 401.
  • "HASHTAG_FILTER_ADDED" pode levar a "CONTENT_RELOADED"

Se você deseja obter um layout de aplicativo modular com patos, pode evitar o acoplamento de patos. A saga se torna o ponto de acoplamento. Os patos precisam apenas saber de seus eventos brutos, e a saga interpreta esses eventos brutos. Isso é muito melhor do que ter duck1 despachando ações diretamente de duck2 porque torna o projeto duck1 mais fácil de reutilizar em outro contexto. Pode-se, no entanto, argumentar que o ponto de acoplamento também pode estar em actionCreators e isso é o que a maioria das pessoas está fazendo hoje.

@slorber Este é um excelente exemplo! Obrigado por dedicar seu tempo para explicar as vantagens e desvantagens de cada abordagem com clareza. (Eu até acho que deveria ir na documentação.)

Eu costumava explorar uma ideia semelhante (que chamei de “componentes de trabalho”). Basicamente, é um componente React que não renderiza nada ( render: () => null ), mas escuta eventos (por exemplo, de lojas) e ativa outros efeitos colaterais. Esse componente trabalhador é então colocado dentro do componente raiz do aplicativo. Apenas outra maneira maluca de lidar com efeitos colaterais complexos. : stick_out_tongue:

Muita discussão aqui enquanto eu estava dormindo.

@winstonewert , você levantou um bom ponto sobre viagem no tempo e repetição de código com erros. Acho que certos tipos de bugs / mudanças não funcionarão com a viagem no tempo de qualquer maneira, mas acho que no geral você está certo.

@dtinth , sinto muito, mas não estou acompanhando a maior parte do seu comentário. Alguma parte do seu código de ação-criador / redutor "abaixa" tem que ser impura, já que parte dele precisa buscar dados. Além disso, você me perdeu. Um dos objetivos principais da minha postagem inicial foi apenas o pragmatismo.


@winstonewert disse: "Acho que minha maneira de pensar sobre o redux insiste em que a loja está sempre em um estado válido."
@slorber perguntou: "O que você quer dizer com estado transitório no Redux @bvaughn e @sompylasar ? Nós ou o despacho termina ou joga. Se ele joga, o estado não muda."

Tenho certeza de que estamos pensando em coisas diferentes. Quando eu disse "estado inválido transitório", estava me referindo a um caso de uso como o seguinte. Por exemplo, eu e um colega recentemente lançamos redux-search . Este middleware de pesquisa escuta as alterações em coleções de itens pesquisáveis ​​e, em seguida, (re) indexa-as para pesquisa. Se o usuário fornecer texto de filtro, redux-search retorna a lista de uids de recursos que correspondem ao texto do usuário. Portanto, considere o seguinte:

Imagine que sua loja de aplicativos contém alguns objetos pesquisáveis: [{id: 1, name: "Alex"}, {id: 2, name: "Brian"}, {id: 3, name: "Charles"}] . O usuário inseriu o texto de filtro "e" e, portanto, o middleware de pesquisa contém uma matriz de ids 1 e 3. Agora imagine que o usuário 1 (Alex) seja excluído - seja em resposta a uma ação do usuário localmente ou uma atualização de dados remotos que não contém mais esse registro do usuário. No ponto em que seu redutor atualizar a coleção de usuários, sua loja ficará temporariamente inválida - porque redux-search fará referência a um id que não existe mais na coleção. Assim que o middleware for executado novamente, ele corrigirá o estado inválido. Esse tipo de coisa pode acontecer sempre que um nó de sua árvore está relacionado a outro nó.


@slorber disse: "Não gosto dessa abordagem porque torna o criador da ação altamente acoplado ao layout da visualização do aplicativo. Ela pressupõe que o actionCreator deve conhecer a estrutura da árvore de estado da IU para tomar sua decisão."

Não entendo o que você quer dizer com a abordagem que acopla o criador da ação "ao layout da visualização do aplicativo". A árvore de estado _drives_ (ou informa) a IU. Esse é um dos principais objetivos do Flux. E seus criadores e redutores de ação são, por definição, acoplados a esse estado (mas não à IU).

Pelo que vale a pena, o código de exemplo que você escreveu como algo que eu prefiro não é o tipo de coisa que eu tinha em mente. Talvez eu tenha feito um péssimo trabalho ao me explicar. Acho que uma dificuldade em discutir algo assim é que normalmente não se manifesta em exemplos simples ou comuns. (Por exemplo, o aplicativo TODO MVC padrão não é complexo o suficiente para discussões matizadas como esta.)

Editado para maior clareza no último ponto.

A propósito, @slorber aqui é um exemplo do que eu tinha em mente. É um pouco artificial.

Digamos que seu estado tenha muitos nós. Um desses nós armazena recursos compartilhados. (Por "compartilhado", quero dizer recursos armazenados em cache localmente e acessados ​​por várias páginas em seu aplicativo.) Esses recursos compartilhados têm seus próprios criadores e redutores de ação ("patos"). Outro nó armazena informações para uma página de aplicativo específica. Sua página também tem seu próprio pato.

Digamos que sua página precise carregar o melhor e mais recente Thing e permitir que um usuário o edite. Aqui está um exemplo de abordagem de criador de ação que posso usar para essa situação:

import { fetchThing, thingSelector } from 'resources/thing/duck'
import { showError } from 'messages/duck'

export function fetchAndProcessThing ({ params }): Object {
  const { id } = params
  return async ({ dispatch, getState }) => {
    try {
      await dispatch(fetchThing({ id }))

      const thing = thingSelector(getState())

      dispatch({ type: 'PROCESS_THING', thing })
    } catch (err) {
      dispatch(showError(`Invalid thing id="${id}".`))
    }
  }
}

Talvez @winstonewert deseje que, se o despacho da 2ª ação falhar, então reverteremos as 2 ações.

Não. Eu não escreveria um criador de ação que despacha duas ações. Eu definiria uma única ação que fez duas coisas. O OP parece preferir criadores de ação que despacham ações menores que permitem os estados inválidos transitórios que eu não gosto.

No ponto em que seu redutor atualizar a coleção de usuários, sua loja ficará temporariamente inválida - porque redux-search fará referência a um id que não existe mais na coleção. Assim que o middleware for executado novamente, ele corrigirá o estado inválido. Esse tipo de coisa pode acontecer sempre que um nó de sua árvore está relacionado a outro nó.

Este é realmente o tipo de caso que me incomoda. Em minha opinião, o índice ideal seria algo inteiramente controlado pelo redutor ou seletor. Ter que despachar ações extras para manter a pesquisa atualizada parece um uso menos puro do redux.

O OP parece preferir criadores de ação que despacham ações menores que permitem os estados inválidos transitórios que eu não gosto.

Não exatamente. Eu favoreceria ações únicas quando em relação ao nó do criador de ações da árvore de estados. Mas se uma única "ação" conceitual do usuário afetar vários nós da árvore de estados, você precisará despachar várias ações. Você pode invocar cada ação separadamente (o que eu acho que é _bad_) ou você poderia ter um único criador de ação despachando as ações (a maneira redux-thunk, que eu acho que é _melhor_ porque oculta essa informação de sua camada de visão).

Este é realmente o tipo de caso que me incomoda. Em minha opinião, o índice ideal seria algo inteiramente controlado pelo redutor ou seletor. Ter que despachar ações extras para manter a pesquisa atualizada parece um uso menos puro do redux.

Você não está despachando ações extras. A pesquisa é um middleware. É automático. Mas existe um estado transitório quando os dois nós de sua árvore não concordam.

@bvaughn Oh, desculpe por ser tão purista!

Bem, o código impuro tem a ver com a busca de dados e outros efeitos colaterais / E / S, enquanto o código puro não pode desencadear nenhum efeito colateral. Consulte esta tabela para comparação entre código puro e impuro.

As melhores práticas de fluxo dizem que uma ação deve “descrever a ação de um usuário, não são configuradores”. Os documentos do Flux também sugeriram de onde essas ações deveriam vir:

Quando novos dados entram no sistema, seja por meio de uma pessoa interagindo com o aplicativo ou por meio de uma chamada de API da web , esses dados são empacotados em uma ação - um literal de objeto contendo os novos campos de dados e um tipo de ação específico.

Basicamente, ações são fatos / dados que descrevem “o que aconteceu”, não o que deveria acontecer. As lojas só podem reagir a essas ações de maneira síncrona, previsível e sem qualquer outro efeito colateral. Todos os outros efeitos colaterais devem ser tratados nos criadores de ação (ou sagas: wink :).

Exemplo

Não estou dizendo que esta é a melhor forma ou melhor do que qualquer outra forma, ou mesmo uma boa forma. Mas isso é o que considero atualmente como a melhor prática.

Por exemplo, digamos que o usuário deseja visualizar o placar que requer conexão com um servidor remoto. Aqui está o que deve acontecer:

  • O usuário clica no botão visualizar placar.
  • A visualização do placar é exibida com um indicador de carregamento.
  • Uma solicitação é enviada ao servidor para buscar o placar.
  • Aguarde a resposta.
  • Se for bem-sucedido, exiba o placar.
  • Se falhar, o placar fecha e uma caixa de mensagem aparece com uma mensagem de erro. O usuário pode fechá-lo.
  • O usuário pode fechar o placar.

Assumindo que as ações só podem chegar à loja como resultado da ação do usuário ou da resposta do servidor, podemos criar 5 ações.

  • SCOREBOARD_VIEW (como resultado do usuário clicar no botão visualizar placar)
  • SCOREBOARD_FETCH_SUCCESS (como resultado de uma resposta bem-sucedida do servidor)
  • SCOREBOARD_FETCH_FAILURE (como resultado de uma resposta de erro do servidor)
  • SCOREBOARD_CLOSE (como resultado do usuário clicar no botão Fechar)
  • MESSAGE_BOX_CLOSE (como resultado do usuário clicar no botão Fechar na caixa de mensagem)

Essas 5 ações são suficientes para atender a todos os requisitos acima. Você pode ver que as primeiras 4 ações não têm nada a ver com qualquer "pato". Cada ação descreve apenas o que aconteceu no mundo externo (o usuário quer fazer isso, o servidor disse aquilo) e pode ser consumida por qualquer redutor. Também não temos MESSAGE_BOX_OPEN action, porque não foi “o que aconteceu” (embora seja o que deveria acontecer).

A única maneira de mudar a árvore de estado é emitir uma ação, um objeto que descreve o README de Redux

Eles são colados com estes criadores de ação:

function viewScoreboard () {
  return async function (dispatch) {
    dispatch({ type: 'SCOREBOARD_VIEW' })
    try {
      const result = fetchScoreboardFromServer()
      dispatch({ type: 'SCOREBOARD_FETCH_SUCCESS', result })
    } catch (e) {
      dispatch({ type: 'SCOREBOARD_FETCH_FAILURE', error: String(e) })
    }
  }
}
function closeScoreboard () {
  return { type: 'SCOREBOARD_CLOSE' }
}

Então, cada parte da loja (governada por redutores) pode então reagir a estas ações:

| Parte da Loja / Redutor | Comportamento |
| --- | --- |
| scoreboardView | Atualize a visibilidade para verdadeira em SCOREBOARD_VIEW , falsa em SCOREBOARD_CLOSE e SCOREBOARD_FETCH_FAILURE |
| scoreboardLoadingIndicator | Atualize a visibilidade para verdadeira em SCOREBOARD_VIEW , falsa em SCOREBOARD_FETCH_* |
| scoreboardData | Atualize os dados dentro da loja em SCOREBOARD_FETCH_SUCCESS |
| messageBox | Atualize a visibilidade para verdadeiro e armazene a mensagem em SCOREBOARD_FETCH_FAILURE e atualize a visibilidade para falso em MESSAGE_BOX_CLOSE |

Como você pode ver, uma única ação pode afetar muitas partes da loja. As lojas só recebem uma descrição de alto nível de uma ação (o que aconteceu?), Em vez de um comando (o que fazer?). Como resultado:

  1. É mais fácil identificar erros.

Nada pode afetar o estado da caixa de mensagem. Ninguém pode dizer para abrir por qualquer motivo. Ele apenas reage ao que está inscrito (ações do usuário e respostas do servidor).

Por exemplo, se o servidor falhar em buscar o placar e uma caixa de mensagem não aparecer, você não precisa descobrir por que SHOW_MESSAGE_BOX action não foi despachada. Torna-se óbvio que a caixa de mensagem não manipulou a ação SCOREBOARD_FETCH_FAILURE corretamente.

Uma correção é trivial e pode ser recarregada a quente e viajar no tempo.

  1. Criadores e redutores de ação podem ser testados separadamente.

Você pode testar se os criadores de ação descreveram o que acontece no mundo exterior corretamente, sem levar em conta como as lojas reagem a eles.

Da mesma forma, os redutores podem simplesmente ser testados para verificar se reagem adequadamente às ações do mundo exterior.

(Um teste de integração ainda seria muito útil.)

Sem problemas. :) Agradeço o esclarecimento adicional. Na verdade, parece que estamos concordando aqui. Olhando para o seu criador de ações de exemplo, viewScoreboard , ele se parece muito com o meu criador de ações de exemplo fetchAndProcessThing , logo acima dele.

Criadores e redutores de ação podem ser testados separadamente.

Embora eu concorde com isso, acho que geralmente faz mais sentido pragmático testá-los juntos. É provável que sua ação _ou_ ou seu redutor (talvez ambos) sejam super simples e, portanto, o valor do retorno sobre o esforço de testar o simples de forma isolada é meio baixo. É por isso que propus testar o criador de ação, o redutor e os seletores relacionados juntos (como um "pato").

Mas se uma única "ação" conceitual do usuário afetar vários nós da árvore de estados, você precisará despachar várias ações.

É precisamente aí que eu acho que o que você está fazendo difere do que é considerado as melhores práticas para redux. Acho que a forma padrão é ter uma ação à qual vários nós da árvore de estado respondem.

Ah, observação interessante @winstonewert. Temos seguido um padrão de uso de constantes de tipo exclusivas para cada pacote "patos" e, portanto, por extensão, uma resposta redutora apenas às ações despachadas por seus criadores de ação irmãos. Sinceramente, não tenho certeza de como me sinto, inicialmente, sobre redutores arbitrários respondendo a uma ação. Parece um pouco como um encapsulamento ruim.

Temos seguido um padrão de uso de constantes de tipo exclusivas para cada pacote "patos"

Observe que não o endossamos em nenhum lugar da documentação ;-) Não estou dizendo que é ruim, mas dá às pessoas certas idéias às vezes erradas sobre o Redux.

então, por extensão, um redutor apenas responde às ações despachadas por seus criadores de ação irmãos

Não existe emparelhamento redutor / criador de ação no Redux. Isso é puramente uma coisa dos Ducks. Algumas pessoas gostam, mas obscurece os pontos fortes fundamentais do modelo Redux / Flux: mutações de estado são desacopladas umas das outras e do código que as causa.

Sinceramente, não tenho certeza de como me sinto, inicialmente, sobre redutores arbitrários respondendo a uma ação. Parece um pouco como um encapsulamento ruim.

Dependendo do que você considera os limites do encapsulamento. As ações são globais no aplicativo, e acho que está tudo bem. Uma parte do aplicativo pode querer reagir às ações de outra parte devido a requisitos complexos do produto, e achamos que isso é bom. O acoplamento é mínimo: tudo de que você depende é um fio e a forma do objeto de ação. O benefício é que é fácil introduzir novas derivações das ações em diferentes partes do aplicativo sem criar toneladas de fiação com os criadores de ação. Seus componentes permanecem ignorantes sobre o que exatamente acontece quando uma ação é despachada - isso é decidido na extremidade do redutor.

Portanto, nossa recomendação oficial é que você deve primeiro tentar fazer com que diferentes redutores respondam às mesmas ações. Se ficar estranho, então com certeza, faça criadores de ação separados. Mas não comece com essa abordagem.

Recomendamos o uso de seletores - na verdade, recomendamos exportar funções de manutenção que lêem do estado ("seletores") junto com redutores e sempre usá-los em mapStateToProps vez de codificar a estrutura de estado no componente. Dessa forma, é fácil alterar a forma do estado interno. Você pode (mas não precisa) usar a seleção novamente para desempenho, mas também pode implementar seletores ingenuamente como no exemplo shopping-cart .

Talvez, tudo se resume ao fato de você programar no estilo imperativo ou no estilo reativo. Usar patos pode fazer com que as ações e redutores se tornem altamente acoplados, o que incentiva ações mais imperativas.

  • No estilo imperativo, a loja recebe "o que fazer", por exemplo, SHOW_MESSAGE_BOX ou SHOW_ERROR
  • No estilo reativo, a loja recebe “um fato do que aconteceu”, por exemplo, DATA_FETCHING_FAILED ou USER_ENTERED_INVALID_THING_ID . A loja reage de acordo.

No exemplo anterior, não tenho o criador de SHOW_MESSAGE_BOX action ou showError('Invalid thing id="'+id+'"') action, porque isso não é verdade. Isso é um comando.

Assim que esse fato entrar na loja, você pode traduzi-lo em comandos, dentro de seus redutores puros, por exemplo

// type Command = State → State
// :: Action → Command
function interpretAction (action) {
  switch (action.type) {
  case 'DATA_FETCHING_FAILED':
    return showErrorMessage('Data fetching failed')
    break
  case 'USER_ENTERED_INVALID_THING_ID':
    return showErrorMessage('User entered invalid thing ID')
    break
  case 'CLOSE_ERROR_MESSAGE':
    return hideErrorMessage()
    break
  default:
    return doNothing()
  }
}

// :: (State, Action) → State
function errorMessageReducer (state, action) {
  return interpretAction(action)(state)
}

const showErrorMessage = message => state => ({ visible: true, message })
const hideErrorMessage = () => state => ({ visible: false })
const doNothing = () => state => state

Quando uma ação vai para a loja como "um fato" em vez de "um comando", há menos chance de dar errado, porque, bem, é um fato.

Agora, se seus redutores interpretaram mal esse fato, ele pode ser consertado facilmente e a correção pode viajar no tempo. Se seus criadores de ação interpretam mal esse fato, entretanto, você precisa refazer seus criadores de ação.

Você também pode alterar seu redutor para que, quando USER_ENTERED_INVALID_THING_ID disparar, o campo de texto de ID da coisa seja redefinido. E essa mudança também viaja no tempo. Você também pode localizar sua mensagem de erro e sem atualizar a página. Isso aperta o ciclo de feedback e torna a depuração e ajustes muito mais fáceis.

(Estou apenas falando sobre os prós, é claro que há contras. Você precisa pensar muito mais sobre como representar esse fato, visto que sua loja só pode responder a esses fatos de forma síncrona e sem efeitos colaterais. Veja a discussão sobre modelos alternativos assíncronos / efeitos colaterais e esta pergunta que postei no StackOverflow . Acho que ainda não acertamos essa parte.)


Sinceramente, não tenho certeza de como me sinto, inicialmente, sobre redutores arbitrários respondendo a uma ação. Parece um pouco como um encapsulamento ruim.

Também é muito comum que vários componentes obtenham dados do mesmo armazenamento. Também é bastante comum que um único componente dependa de dados de várias partes da loja. Isso também não soa um pouco como um encapsulamento ruim? Para se tornar verdadeiramente modular, um componente React também não deveria estar dentro do pacote "pato"? (A arquitetura Elm faz isso .)

O React torna sua IU reativa (daí seu nome) tratando os dados de sua loja como um fato. Assim, você não precisa dizer ao seu modo de exibição 'como atualizar a IU'.

Da mesma forma, também acredito que Redux / Flux torna seu modelo de dados reativo , tratando as ações como um fato, para que você não precise dizer ao seu modelo de dados como se atualizar.

Obrigado por escrever e compartilhar suas idéias, @dtinth. Também agradeço a @gaearon por

Também é muito comum que vários componentes obtenham dados do mesmo armazenamento. Também é bastante comum que um único componente dependa de dados de várias partes da loja. Isso também não soa um pouco como um encapsulamento ruim?

Eh ... parte disso é subjetivo, mas não. Eu penso nos criadores e seletores de ação exportados como a API para o módulo.

De qualquer forma, acho que esta foi uma boa discussão. Como Thai mencionou em sua resposta anterior, há prós e contras nessas abordagens que estamos discutindo. Tem sido bom conhecer outras abordagens. :)

A propósito, a caixa de mensagem é um bom exemplo de onde eu prefiro ter um criador de ação separado para mostrar. Principalmente porque quero passar um tempo quando ele foi criado para que possa ser dispensado automaticamente (e o criador da ação é onde você chama impuro Date.now() ), porque eu quero configurar um cronômetro para dispensá-lo, eu quero debounce aquele timer, etc. Portanto, eu consideraria uma caixa de mensagem o caso em que seu "fluxo de ação" é importante o suficiente para justificar suas ações pessoais. Dito isso, talvez o que descrevi possa ser resolvido de maneira mais elegante em https://github.com/yelouafi/redux-saga.

Escrevi isso inicialmente no bate-papo do Discord Reactiflux, mas fui solicitado a colá-lo aqui.

Tenho pensado muito nas mesmas coisas recentemente. Eu sinto que as atualizações de estado são divididas em três partes.

  1. O criador da ação recebe a quantidade mínima de informações necessárias para executar a atualização. Ou seja, tudo o que pode ser calculado a partir do estado atual não deve estar nele.
  2. O estado é consultado por qualquer informação necessária para preencher as atualizações (por exemplo, quando você deseja copiar Todo com id X, você busca os atributos de Todo com id X para que possa fazer uma cópia). Isso pode ser feito no criador da ação e essa informação é então incluída no objeto de ação. Isso resulta em objetos de ação gordos . OU pode ser calculado no redutor - objetos de ação finos .
  3. Com base nessas informações, a lógica pura do redutor é aplicada para obter o próximo estado.

Agora, o problema é o que colocar no criador da ação e o que no redutor, a escolha entre objetos de ação gordos e finos. Se você colocar toda a lógica no criador da ação, acabará com objetos de ação gordos que basicamente declaram as atualizações do estado. Os redutores se tornam puros, burros, acrescentam, removem aquilo, atualizam essas funções. Eles serão fáceis de compor. Mas não haverá muito de sua lógica de negócios.

Se você colocar mais lógica no redutor, acabará com objetos de ação finos e agradáveis, a maior parte da lógica de seus dados em um só lugar, mas seus redutores são mais difíceis de compor, pois você pode precisar de informações de outros branches. Você acaba com grandes redutores ou redutores que usam argumentos adicionais de níveis superiores no estado.

Não sei qual é a resposta para esses problemas, não tenho certeza se há uma ainda

Obrigado por compartilhar esses pensamentos @tommikaikkonen. Ainda estou indeciso sobre qual é a "resposta" aqui. Eu concordo com seu resumo. Eu acrescentaria uma pequena observação à seção "coloque toda a lógica no criador da ação ...", que permite que você use seletores (compartilhados) para ler dados, o que em alguns casos pode ser bom.

Este é um tópico interessante! Descobrir onde colocar o código em um aplicativo redux é um problema que todos nós enfrentamos. Eu gosto da ideia do CQRS de apenas registrar as coisas que aconteceram.
Mas eu posso ver uma incompatibilidade de ideias aqui porque no CQRS, AFAIK a melhor prática é construir um estado desnormalizado a partir da ação / eventos que podem ser consumidos diretamente pelas visualizações. Mas no redux, a melhor prática é construir um estado totalmente normalizado e derivar os dados de suas visualizações por meio de seletores.
Se construirmos um estado desnormalizado que é diretamente consumível pela visão, então acho que o problema de um redutor querer dados em outro redutor vai embora (porque cada redutor pode simplesmente armazenar todos os dados de que precisa, sem se preocupar com a normalização). Mas então temos outros problemas ao atualizar os dados. Talvez este seja o cerne da discussão?

Vindo de muitos anos de desenvolvimento Orientado a Objetos ... Redux parece um grande retrocesso. Encontro-me implorando para criar classes que encapsulem eventos (criadores de ação) e a lógica de negócios. Ainda estou tentando descobrir um meio-termo significativo, mas ainda não consegui. Alguém mais sente a mesma coisa?

A programação orientada a objetos incentiva a junção de leituras com gravações. Isso torna um monte de coisas problemáticas: instantâneo e reversão, registro centralizado, depuração de mutações de estado incorretas, atualizações eficientes granulares. Se você não acha que esses são os problemas para você, se você sabe como evitá-los enquanto escreve o código MVC tradicional orientado a objetos, e se Redux apresenta mais problemas do que resolve em seu aplicativo, não use Redux: wink: .

@jayesbe Vindo de uma experiência em programação orientada a objetos, você pode descobrir que isso conflita com as ideias emergentes em programação. Este é um dos muitos artigos sobre o assunto: https://www.leaseweb.com/labs/2015/08/object-oriented-programming-is-exceptionally-bad/.

Ao separar as ações da transformação de dados, o teste de aplicação das regras de negócios é mais simples. As transformações tornam-se menos dependentes do contexto do aplicativo.

Isso não significa abandonar objetos ou classes em Javascript. Por exemplo, os componentes React são implementados como objetos e agora como classes. Mas os componentes do React são projetados simplesmente para criar uma projeção dos dados fornecidos. Você é encorajado a usar componentes puros que não armazenam estado.

É aí que entra o Redux: para organizar o estado da aplicação e reunir ações e a correspondente transformação do estado da aplicação.

@johnsoftek obrigado pelo link. No entanto, com base na minha experiência nos últimos 10 anos ... Não concordo com isso, mas não precisamos entrar no debate entre OO e não OO aqui. O problema que tenho é com a organização de código e abstração.

Meu objetivo é criar um único aplicativo / arquitetura única que pode (usando apenas valores de configuração) ser usado para criar 100 aplicativos. O caso de uso com o qual tenho que lidar é lidar com uma solução de software de marca branca que está em uso por muitos clientes .. cada um chamando o aplicativo de seu próprio.

Eu descobri o que considero um compromisso interessante ... e acho que ele lida com isso muito bem, mas pode não atender aos padrões de multidões de programação funcional. Eu ainda gostaria de colocá-lo lá fora.

Eu tenho uma única classe de aplicativo que é independente com toda a lógica de negócios, wrappers de API, etc. que preciso para fazer a interface com meu aplicativo do lado do servidor.

exemplo..

export default Application {
    constructor(config) {
        this.config = config;
    } 

    config() {
        return this.config;
    }

    login(data, cb) {
        const url = [
            this.config.url,
            '?client=' + this.config.client,
            '&username=' + data.username,
            ....
        ].join('');

        fetch(url).then((responseText) => {
            cb(responseText);
        })
    }

    ... more business logic 
}

Eu criei uma única instância deste objeto e a coloquei no contexto .. estendendo o Provedor Redux

import { Provider } from 'react-redux';

export default class MyProvider extends Provider {
    getChildContext() {
        return Object.assign({}, Provider.prototype.getChildContext.call(this), {
            app: this.props.app
        });
    }

    render() {
        return this.props.children;
    }
}
MyProvider.childContextTypes = {
    store: React.PropTypes.object,
    app: React.PropTypes.object
}

Então usei este provedor como tal

import Application from './application';
import config from './config';

class MyApp extends Component {
  render() {
    return (
      <MyProvider store={store} app={new Application(config)}>
        <Router />
      </MyProvider>
    );
  }
}

AppRegistry.registerComponent('MyApp', () => MyApp);

finalmente, em meu componente, usei meu aplicativo ..

class Login extends React.Component {
    render() {
        const { app } = this.context;
        const { state, actions } = this.props;
        return (
              <View style={style.transparentContainer}>
                <Form ref="form" type={User} options={options} />
                <Button 
                  onPress={() => {
                    value = this.refs.form.getValue();
                    if (value) {
                      app.login(value, actions.login);
                    }
                  }}
                >
                  Login
                </Button>
              </View>
        );
    }
};
Login.contextTypes = {
  app: React.PropTypes.object,
};

function mapStateToProps(state) {
  return {
      state: state.default.auth
  };
};

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(authActions, dispatch),
    dispatch
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(Login);

O Action Creator é, portanto, apenas uma função de retorno de chamada para minha lógica de negócios.

                      app.login(value, actions.login);

Esta solução parece estar funcionando bem no momento, embora eu só tenha começado com a autenticação.

Acredito que também poderia passar o armazenamento para minha instância de aplicativo, mas não quero fazer isso porque não quero que o armazenamento sofra uma mutação casual. Embora acessar a loja possa ser útil. Vou pensar mais nisso se precisar.

Eu descobri o que considero um compromisso interessante ... e acho que ele lida com isso muito bem, mas pode não atender aos padrões de multidões de programação funcional.

Você não encontrará “a multidão funcional” aqui: wink:. A razão pela qual escolhemos soluções funcionais no Redux não é porque somos dogmáticos, mas porque elas resolvem alguns problemas que as pessoas costumam fazer por causa das aulas. Por exemplo, separar redutores de criadores de ação nos permite separar leituras e gravações, o que é importante para registrar e reproduzir bugs. As ações, sendo objetos simples, tornam a gravação e a reprodução possíveis porque são serializáveis. Da mesma forma, o estado sendo um objeto simples em vez de uma instância de MyAppState torna muito fácil serializá-lo no servidor e desserializá-lo no cliente para renderização no servidor ou persistir partes dele em localStorage . Expressar redutores como funções nos permite implementar viagem no tempo e recarga a quente, e expressar seletores como funções torna a memoização fácil de adicionar. Todos esses benefícios não têm nada a ver com o fato de sermos uma “multidão funcional” e tudo a ver com a resolução de tarefas específicas que esta biblioteca foi criada para resolver.

Eu criei uma única instância deste objeto e a coloquei no contexto .. estendendo o Provedor Redux

Isso parece totalmente sensato para mim. Não temos um ódio irracional pelas aulas. O ponto é que preferimos não usá-los em casos onde eles são severamente limitantes (como para redutores ou objetos de ação), mas é bom usá-los para gerar objetos de ação, por exemplo.

No entanto, eu evitaria estender Provider pois isso é frágil. Não há necessidade disso: o React mescla o contexto dos componentes, portanto, você pode apenas envolvê-lo.

import { Component } from 'react';
import { Provider } from 'react-redux';

export default class MyProvider extends Component {
    getChildContext() {
        return {
            app: this.props.app
        };
    }

    render() {
        return (
            <Provider store={this.props.store}>
                {this.props.children}
            </Provider>
        );
    }
}
MyProvider.childContextTypes = {
    app: React.PropTypes.object
}
MyProvider.propTypes = {
    app: React.PropTypes.object,
    store: React.PropTypes.object
}

Na verdade, é mais fácil de ler na minha opinião e menos frágil.

Portanto, em suma, sua abordagem faz todo o sentido. Usar uma classe neste caso não é realmente diferente de algo como createActions(config) que é um padrão que também recomendamos se você precisar parametrizar os criadores de ação. Não há absolutamente nada de errado com isso.

Nós apenas desencorajamos você de usar instâncias de classe para objetos de estado e ação porque as instâncias de classe tornam a serialização muito complicada. Para redutores, também não recomendamos o uso de classes porque será mais difícil usar a composição do redutor, ou seja, redutores que chamam outros redutores. Para todo o resto, você pode usar qualquer meio de organização de código, incluindo classes.

Se seu aplicativo e configuração forem imutáveis ​​(e acho que deveriam ser, mas talvez eu tenha bebido muito cool-aid funcional), então você pode considerar a seguinte abordagem:

const appSelector = createSelector(
   (state) => state.config,
   (config) => new Application(config)
)

E então em mapStateToProps:

function mapStateToProps(state) {
  return {
      app: appSelector(state)
  };
};

Então você não precisa da técnica de provedor que você adotou, você apenas obtém o objeto de aplicação do estado. Graças à nova seleção, o objeto Aplicativo só será construído quando a configuração for alterada, o que provavelmente é apenas uma vez.

Acho que essa abordagem pode ter uma vantagem é que ela permite facilmente estender a ideia para ter vários desses objetos e também fazer com que esses objetos dependam de outras partes do estado. Por exemplo, você pode ter uma classe UserControl com métodos de login / logout / etc que tem acesso à configuração e parte do seu estado.

Portanto, em suma, sua abordagem faz todo o sentido.

: +1: Obrigado, isso ajuda. Eu concordo com a melhoria no MyProvider. Vou atualizar meu código para seguir. Um dos maiores problemas que tive quando aprendi Redux pela primeira vez foi a noção semântica de "Criadores de ação" .. não funcionou até que eu os equiparou a eventos. Para mim foi uma espécie de percepção de que ... esses são eventos que estão sendo despachados.

@winstonewert o createSelector está disponível no react-native? Eu não acredito que seja. Ao mesmo tempo, parece que você está criando o novo aplicativo toda vez que o anexa em mapStateToProps em algum componente? Meu objetivo é ter um único objeto instanciado que forneça toda a lógica de negócios para o aplicativo e que esse objeto seja acessível globalmente. Não tenho certeza se sua sugestão funciona. Embora eu goste da ideia de ter objetos adicionais disponíveis, se necessário ... tecnicamente, posso instanciar conforme necessário por meio da instância do aplicativo também.

Um dos maiores problemas que tive quando aprendi Redux pela primeira vez foi a noção semântica de "Criadores de ação" .. não funcionou até que eu os equiparou a eventos. Para mim foi uma espécie de percepção de que ... esses são eventos que estão sendo despachados.

Eu diria que não há nenhuma noção semântica de Action Creators no Redux. Há uma noção semântica de Ações (que descrevem o que aconteceu e são aproximadamente equivalentes a eventos, mas não exatamente - por exemplo, veja a discussão em # 351). Os criadores de ações são apenas um padrão para organizar o código. É conveniente ter fábricas para ações porque você quer ter certeza de que ações do mesmo tipo tenham estrutura consistente, tenham o mesmo efeito colateral antes de serem despachadas, etc. Mas do ponto de vista do Redux, criadores de ação não existem - Redux só vê ações.

o createSelector está disponível no react-native?

Ele está disponível em Reselect, que é JavaScript simples, sem dependências e pode funcionar na web, no nativo, no servidor, etc.

Ahh. OK, entendi. As coisas estão muito mais claras. Saúde.

Não aninhe objetos em mapStateToProps e mapDispatchToProps

Recentemente, encontrei um problema em que objetos aninhados eram perdidos quando o Redux mesclava mapStateToProps e mapDispatchToProps (consulte reactjs / react-redux # 324). Embora @gaearon forneça uma solução que me permite usar objetos aninhados, ele continua dizendo que este é um

Observe que agrupar objetos como este causará alocações desnecessárias e também tornará as otimizações de desempenho mais difíceis, porque não podemos mais confiar na igualdade superficial de props de resultado como uma forma de saber se eles mudaram. Portanto, você verá mais renderizações do que com uma abordagem simples, sem namespacing, o que recomendamos nos documentos.

@bvaughn disse

redutores devem ser estúpidos e simples

e devemos colocar a maior parte da lógica de negócios em criadores de ação, estou absolutamente de acordo com isso. Mas se tudo mudou para ações, por que ainda temos que criar arquivos e funções redutoras manualmente? Por que não armazenar os dados que foram operados em ações diretamente?

Isso me confundiu um período de tempo ...

por que ainda temos que criar arquivos e funções redutoras manualmente?

Como os redutores são funções puras, se houver um erro na lógica de atualização de estado, você pode recarregar o redutor a quente. A ferramenta dev pode então retroceder o aplicativo ao seu estado inicial e, em seguida, reproduzir todas as ações, usando o novo redutor. Isso significa que você pode corrigir bugs de atualização de estado sem ter que reverter manualmente e executar novamente a ação. Este é o benefício de manter a maior parte da lógica de atualização de estado no redutor.

Este é o benefício de manter a maior parte da lógica de atualização de estado no redutor.

@dtinth Apenas para esclarecer, ao dizer "lógica de atualização de estado", você quer dizer "lógica de negócios"?

Não tenho certeza se colocar a maior parte de sua lógica em criadores de ação é uma boa ideia. Se todos os seus redutores forem apenas funções triviais que aceitam ações do tipo ADD_X , então é improvável que haja erros - ótimo! Mas então todos os seus erros foram enviados aos criadores de ação e você perde a grande experiência de depuração à qual

Mas também como @tommikaikkonen mencionou, não é tão simples escrever redutores complexos. Minha intuição é que é onde você iria querer empurrar se quisesse colher os benefícios do Redux - caso contrário, em vez de levar os efeitos colaterais ao limite, você está empurrando suas funções puras para lidar apenas com as tarefas mais triviais, deixando a maioria do seu aplicativo em um inferno dominado pelo estado. :)

@sompylasar "lógica de negócios" e "lógica de atualização de estado" são, imo, quase a mesma coisa.

No entanto, para concordar com minhas próprias especificações de implementação ... minhas ações são principalmente pesquisas sobre entradas para a ação. Na verdade, todas as minhas ações são puras, pois movi toda a minha "lógica de negócios" para um contexto de aplicativo.

como um exemplo .. este meu redutor típico

export default function reducer(state = initialState, action = {}) {
  switch (action.type) {
    case 'FOO_REQUEST':
    case 'FOO_RESPONSE':
    case 'FOO_ERROR':
    case 'FOO_RESET':
      return {
        ...state,
        ...action.data
      }; 
    default:
      return state;
  }
}

Minhas ações típicas:

export function fooRequest( res ) {
  return {
    type: 'FOO_REQUEST',
    data: {
        isFooing: true,
        toFoo: res.saidToFoo
    }
  };
}

export function fooResponse( res ) {
  return {
    type: 'FOO_RESPONSE',
    data: {
        isFooing: false,
        isFooed: true,
        fooData: res.data
    }
  };
}

export function fooError( res ) {
  return {
    type: 'FOO_ERROR',
    data: {
        isFooing: false,
        fooData: null,
        isFooed: false,
        fooError: res.error
    }
  };
}

export function fooReset( res ) {
  return {
    type: 'FOO_RESET',
    data: {
        isFooing: false,
        fooData: null,
        isFooed: false,
        fooError: null,
        toFoo: true
    }
  };
}

Minha lógica de negócios é definida em um objeto armazenado no contexto, ou seja,

export default class FooBar
{
    constructor(store)
    {
        this.actions = bindActionCreators({
            ...fooActions
        }, store.dispatch);
    }

    async getFooData()
    {
        this.actions.fooRequest({
            saidToFoo: true
        });

        fetch(url)
        .then((response) => {
            this.actions.fooResponse(response);
        })
    }
}

Se você ver meu comentário acima, eu também estava lutando com a melhor abordagem. Finalmente refatorei e resolvi passar o armazenamento para o construtor do meu objeto de aplicativo e conectar todas as ações ao despachante neste ponto central. Todas as ações que meu aplicativo conhece são atribuídas aqui.

Eu não uso mais mapDispatchToProps () em qualquer lugar. Para Redux, agora apenas mapStateToProps ao criar um componente conectado. Se eu precisar acionar alguma ação, posso acioná-la por meio do meu objeto de aplicativo por meio do contexto.

class SomeComponent extends React.Component {
    componentWillReceiveProps(nextProps) {
        if (nextProps.someFoo != this.props.someFoo) {
            const { app } = this.context;
            app.actions.getFooData();
        }
    }
}
SomeComponent.contextTypes = {
    app: React.PropTypes.object
};

O componente acima não precisa ser conectado novamente. Ele ainda pode despachar ações. Claro, se você precisar atualizar o estado dentro do componente, você deve transformá-lo em um componente conectado para garantir que a mudança de estado seja propagada.

Foi assim que organizei minha "lógica de negócios" central. Como meu estado é realmente mantido em um servidor de back-end ... isso funciona muito bem para o meu caso de uso.

O local onde você armazena sua "lógica de negócios" depende de você e como ela se ajusta ao seu caso de uso.

@jayesbe A parte a seguir significa que você não tem "lógica de negócios" nos redutores e, além disso, a estrutura de estado mudou para os criadores de ação que criam a carga útil que é transferida para a loja por meio do redutor:

    case 'FOO_REQUEST':
    case 'FOO_RESPONSE':
    case 'FOO_ERROR':
    case 'FOO_RESET':
      return {
        ...state,
        ...action.data
      }; 
export function fooRequest( res ) {
  return {
    type: 'FOO_REQUEST',
    data: {
        isFooing: true,
        toFoo: res.saidToFoo
    }
  };
}

@jayesbe Minhas ações e redutores são muito semelhantes aos seus, algumas ações recebem um objeto de resposta de rede simples como argumento, e eu encapsulei a lógica de como lidar com os dados de resposta na ação e, finalmente, retornei um objeto muito simples como valor retornado e depois passei para redutor via despacho de chamadas (). Exatamente como o que você fez. O problema é se sua ação escrita desta forma, sua ação fez quase tudo e a responsabilidade de seu redutor será muito leve, por que temos que transferir os dados para armazenar manualmente se o redutor apenas espalha o objeto de ação de forma simples? Redux dosar automaticamente para nós não é uma coisa difícil.

Não necessariamente. Mas, na maioria das vezes, faz parte do processo de negócios
envolve a atualização do estado do aplicativo de acordo com as regras de negócios,
então você pode ter que colocar alguma lógica de negócios lá.

Para um caso extremo, verifique isto:
“Synchronous RTS Engines and a Tale of Desyncs” @ForrestTheWoods
https://blog.forrestthewoods.com/synchronous-rts-engines-and-a-tale-of-desyncs-9d8c3e48b2be

Em 5 de abril de 2016, 17:54, "John Babak" [email protected] escreveu:

Este é o benefício de manter a maior parte da lógica de atualização de estado no
redutor.

@dtinth https://github.com/dtinth Só para esclarecer, dizendo
"lógica de atualização de estado", você quer dizer "lógica de negócios"?

-
Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/reactjs/redux/issues/1171#issuecomment -205754910

@LumiaSaki O conselho para manter seus redutores simples enquanto mantém uma lógica complexa nos criadores de ação vai contra a forma recomendada de usar o Redux. O padrão recomendado do Redux é o oposto: mantenha os criadores de ação simples enquanto mantém a lógica complexa nos redutores. Claro, você é livre para colocar toda a sua lógica em criadores de ação de qualquer maneira, mas ao fazer isso você não segue o paradigma Redux, mesmo se estiver usando Redux.

Por causa disso, o Redux não transfere automaticamente os dados das ações para a loja. Porque não é assim que você deve usar o Redux. Redux não vai ser alterado para facilitar o uso de uma forma diferente da pretendida. Claro, você é absolutamente livre para fazer o que funciona para você, mas não espere que o Redux mude por isso.

Por que vale a pena, eu produzo meus redutores usando algo como:

let {reducer, actions} = defineActions({
   fooRequest: (state, res) => ({...state, isFooing: true, toFoo: res.saidToFoo}),
   fooResponse: (state, res) => ({...state, isFooing: false, isFooed: true, fooData: res.data}),
   fooError: (state, res) => ({...state, isFooing: false, fooData: null, isFooed: false, fooError: res.error})
   fooReset: (state, res) => ({...state, isFooing: false, fooData: null, isFooed: false, fooError: null, toFoo: false})
})

defineActions retorna o redutor e os criadores da ação. Dessa forma, acho muito fácil manter minha lógica de atualização dentro do redutor sem ter que gastar muito tempo escrevendo criadores de ações triviais.

Se você insiste em manter sua lógica em seus criadores de ação, então você não deve ter problemas para automatizar os dados por si mesmo. Seu redutor é uma função e pode fazer o que quiser. Portanto, seu redutor pode ser tão simples como:

function reducer(state, action) {
    if (action.data) {
        return {...state, ...action.data}
   } else {
        return state;
   }
}

Expandindo os pontos anteriores sobre a lógica de negócios, acho que você pode separar sua lógica de negócios em duas partes:

  • A parte não determinística. Esta parte faz uso de serviços externos, código assíncrono, hora do sistema ou um gerador de números aleatórios. Na minha opinião, esta parte é melhor tratada por uma função de I / O (ou um criador de ação). Eu os chamo de processo de negócios.

Considere um software semelhante ao IDE em que o usuário pode clicar no botão Executar e compilar e executar o aplicativo. (Aqui, eu uso uma função assíncrona que leva uma loja, mas você pode usar redux-thunk vez disso.)

js export async function runApp (store) { try { store.dispatch({ type: 'startCompiling' }) const compiledApp = await compile(store) store.dispatch({ type: 'startRunning', app: compiledApp }) } catch (e) { store.dispatch({ type: 'errorCompiling', error: e }) } }

  • A parte determinística. Esta parte tem resultado totalmente previsível. Dado o mesmo estado e o mesmo evento, o resultado é sempre previsível. Em minha opinião, essa parte é mais bem tratada por um redutor. Eu chamo isso de regras de negócios.

`` `js
importar você de 'updeep'

export const reducer = createReducer ({
// [nome da ação]: ação => currentState => nextState
startCompiling: () => u ({compiling: true}),
errorCompiling: ({error}) => u ({compiling: false, compileError: error}),
startRunning: ({app}) => u ({
executando: () => aplicativo,
compilando: falso
}),
stopRunning: () => u ({running: false}),
discardCompileError: () => u ({compileError: null}),
// ...
})
`` `

Tento colocar o máximo de código possível nessa área determinística, mantendo em mente que a única responsabilidade do redutor é manter o estado do aplicativo consistente, considerando as ações recebidas. Nada mais. Qualquer coisa além disso, eu faria isso fora do Redux, porque Redux é apenas um contêiner de estado.

@dtinth Ótimo, porque o exemplo anterior em https://github.com/reactjs/redux/issues/1171#issuecomment -205782740 parece totalmente diferente do que você escreveu em https://github.com/reactjs/redux/ Issues / 1171 # issuecomment -205888533 - sugere construir um fragmento de criadores de estado em ação e passá-los para os redutores para que eles apenas divulguem as atualizações (esta abordagem parece errada para mim, e concordo com o mesmo apontado em https://github.com/reactjs/redux/issues/1171#issuecomment-205865840).

@winstonewert

O padrão recomendado do Redux é o oposto: mantenha os criadores de ação simples enquanto mantém a lógica complexa nos redutores.

Como você pode colocar lógica complexa nos ruducers e ainda mantê-los puros?

Se estou chamando fetch (), por exemplo, e carregando dados do servidor ... e processando de alguma forma. Ainda estou para ver um exemplo de um redutor que tem "lógica complexa"

@jayesbe : Uh ... "complexo" e "puro" são ortogonais. Você pode ter _realmente_ lógica condicional complicada ou manipulação dentro de um redutor e, desde que seja apenas uma função de suas entradas, sem efeitos colaterais, ainda é puro.

Se você tem um estado local complexo (pense em um editor de postagem, visualização em árvore, etc) ou lida com coisas como atualizações otimistas, seus redutores conterão uma lógica complexa. Realmente depende do aplicativo. Alguns têm solicitações complexas, outros têm atualizações de estado complexas. Alguns têm ambos :-)

@markerikson ok as declarações lógicas são uma coisa .. mas executar tarefas específicas? Por exemplo, eu tenho uma ação que em um caso aciona três outras ações ou, em outro caso, aciona duas ações distintas e separadas. Essa lógica + execução de tarefas não parece que devam ir em redutores.

Meus dados de estado / estado do modelo estão no servidor, o estado de exibição é diferente do modelo de dados, mas o gerenciamento desse estado está no cliente. O estado do meu modelo de dados é simplesmente transmitido para a visualização ... que é o que torna meus redutores e ações tão enxutos.

@jayesbe : Não acho que alguém já disse que o acionamento de outras ações deva ser reduzido. E, de fato, não deveria. O trabalho de um redutor é simplesmente (currentState + action) -> newState .

Se precisar unir várias ações, você pode fazer isso em algo como um pensamento ou saga e dispará-los em sequência, ou ter algo ouvindo as mudanças de estado, ou usar um middleware para interceptar uma ação e fazer um trabalho adicional.

Estou meio confuso sobre qual é a discussão neste momento, para ser honesto.

@markerikson, o tópico parece ser sobre a "lógica de negócios" e para onde ela vai. O fato da questão é ... tudo depende do aplicativo. Existem diferentes maneiras de fazer isso. Alguns mais complexos do que outros. Se você encontrar uma boa maneira de resolver seu problema, isso torna as coisas fáceis de manter e organizar. Isso é tudo o que realmente importa. Acontece que minha implementação é muito enxuta para meu caso de uso, mesmo que vá contra o paradigma.

Como você notou, tudo o que realmente importa é o que funciona para você. Mas aqui está como eu lidaria com os problemas que você perguntou.

Se estou chamando fetch (), por exemplo, e carregando dados do servidor ... e processando de alguma forma. Ainda estou para ver um exemplo de um redutor que tem "lógica complexa"

Meu redutor obtém uma resposta bruta do meu servidor e atualiza meu estado com ela. Dessa forma, a resposta de processamento de que você fala é feita no meu redutor. Por exemplo, a solicitação pode ser buscar registros JSON para meu servidor, que o redutor mantém em meu cache de registros local.

k declarações lógicas são uma coisa ... mas executar tarefas específicas? Por exemplo, eu tenho uma ação que em um caso aciona três outras ações ou, em outro caso, aciona duas ações distintas e separadas. Essa lógica + execução de tarefas não parece que devam ir em redutores.

Isso depende do que você está fazendo. Obviamente, no caso de busca do servidor, uma ação acionará outra. Isso está totalmente dentro do procedimento Redux recomendado. No entanto, você também pode estar fazendo algo assim:

function createFoobar(dispatch, state, updateRegistry) {
   dispatch(createFoobarRecord());
   if (updateRegistry) {
      dispatch(updateFoobarRegistry());
   } else {
       dispatch(makeFoobarUnregistered());
   }
   if (hasFoobarTemps(state)) {
      dispatch(dismissFoobarTemps());
   }
}

Esta não é a forma recomendada de usar o Redux. A forma Redux recomendada é ter uma única ação CREATE_FOOBAR que causa todas essas mudanças desejadas.

@winstonewert :

Esta não é a forma recomendada de usar o Redux. A forma Redux recomendada é ter uma única ação CREATE_FOOBAR que causa todas essas mudanças desejadas.

Você tem um ponteiro para algum lugar especificado? Porque quando eu estava fazendo uma pesquisa para a página de perguntas frequentes, o que eu pensei foi "depende", direto do Dan. Veja http://redux.js.org/docs/FAQ.html#actions -multiple-actions e esta resposta de Dan no SO .

"Lógica de negócios" é realmente um termo bastante amplo. Pode abranger coisas como "Aconteceu alguma coisa?", "O que fazemos agora que isso aconteceu?", "Isso é válido?" E assim por diante. Com base no design do Redux, essas questões _podem_ ser respondidas em vários lugares dependendo de qual é a situação, embora eu veria "aconteceu" mais como uma responsabilidade do criador de ação, e "o que agora" é quase definitivamente uma responsabilidade redutora.

No geral, minha opinião sobre esta _questão inteira_ de "lógica de negócios" é: _ "depende _". Há razões pelas quais você pode querer fazer a análise de solicitação em um criador de ação e razões pelas quais você pode querer fazer isso em um redutor. Há momentos em que seu redutor pode ser simplesmente "pegar este objeto e colocá-lo no meu estado", e outras vezes em que seu redutor pode ser uma lógica condicional muito complexa. Há momentos em que seu criador de ação pode ser muito simples e outros em que pode ser complexo. Há momentos em que faz sentido despachar várias ações em uma linha para representar as etapas de um processo, e outros momentos em que você deseja apenas despachar uma ação genérica "THING_HAPPENED" para representar tudo isso.

Sobre a única regra rígida com a qual eu concordaria é "não determinismo em criadores de ação, puro determinismo em redutores". Isso é um dado adquirido.

Além disso? Encontre algo que funcione para você. Ser consistente. Saiba por que você está fazendo isso de determinada maneira. Vai com isso.

embora eu considerasse "aconteceu" mais como uma responsabilidade do criador da ação, e "o que agora" é quase definitivamente uma responsabilidade redutora.

É por isso que há uma discussão paralela sobre como colocar os efeitos colaterais, ou seja, a parte não pura do "e agora", em redutores: # 1528 e torná-los apenas descrições puras do que deve acontecer, como as próximas ações a despachar.

O padrão que estou usando é:

  • O que fazer : ação / criador de ação
  • Como fazer : Middleware (por exemplo, middleware que escuta ações 'assíncronas' e faz chamadas para meu objeto API.)
  • O que fazer com o resultado : redutor

No início deste tópico, a declaração de Dan foi:

Portanto, nossa recomendação oficial é que você deve primeiro tentar fazer com que diferentes redutores respondam às mesmas ações. Se ficar estranho, então com certeza, faça criadores de ação separados. Mas não comece com essa abordagem.

A partir disso, concluo que a abordagem recomendada é despachar uma ação por evento. Mas, pragmaticamente, faça o que funciona.

@winstonewert : Dan está se referindo ao padrão de "composição do redutor", ou seja, "é uma ação ouvida apenas por um redutor" versus "muitos redutores podem responder à mesma ação". Dan adora redutores arbitrários que respondem a uma única ação. Outros preferem coisas como a abordagem "patos", em que redutores e ações são muito bem agrupados e apenas um redutor lida com uma determinada ação. Portanto, esse exemplo não é sobre "despachar várias ações em sequência", mas sim "quantas partes da minha estrutura de redutor estão esperando para responder a isso".

Mas, pragmaticamente, faça o que funciona.

: +1:

@sompylasar Eu vejo o erro dos meus caminhos por ter a estrutura de estado em minhas ações. Posso facilmente mudar a estrutura de estado para meus redutores e simplificar minhas ações. Saúde.

Parece-me que é a mesma coisa.

Ou você tem uma única ação disparando vários redutores, causando várias mudanças de estado, ou você tem várias ações, cada uma disparando um único redutor, causando uma única mudança de estado. Ter vários redutores respondendo a uma ação e um evento despachar várias ações são soluções alternativas para o mesmo problema.

Na pergunta StackOverflow que você mencionou, ele afirma:

Mantenha o log de ações o mais próximo possível do histórico de interações do usuário. No entanto, se isso torna os redutores difíceis de implementar, considere dividir algumas ações em várias, se uma atualização da IU puder ser considerada como duas operações separadas que por acaso estão juntas.

A meu ver, Dan endossa a manutenção de uma ação por interação do usuário como a forma ideal. Mas ele é pragmático, quando torna o redutor difícil de implementar, ele endossa a divisão da ação.

Estou visualizando alguns casos de uso semelhantes, mas um tanto diferentes aqui:

1) Uma ação requer atualizações em várias áreas do seu estado, especialmente se você estiver usando combineReducers para ter funções redutoras separadas para lidar com cada subdomínio. Você:

  • fazer com que o Redutor A e o Redutor B respondam à mesma ação e atualizem seus bits de estado independentemente
  • faça seu thunk colocar TODOS os dados relevantes na ação para que qualquer redutor possa acessar outros bits de estado fora de seu próprio pedaço
  • adicionar outro redutor de nível superior que pegue pedaços do estado do Redutor A e, caso especial, entregue-o ao Redutor B?
  • despachar uma ação destinada ao Redutor A com os bits de estado de que precisa e uma segunda ação para o Redutor B com o que precisa?

2) Você tem um conjunto de etapas que acontecem em uma sequência específica, cada etapa exigindo alguma atualização de estado ou ação de middleware. Você:

  • Despachar cada etapa individual como uma ação separada para representar o progresso e permitir que redutores individuais respondam a ações específicas?
  • Despachar um grande evento e confiar que os redutores o tratam de maneira adequada?

Então, sim, definitivamente alguma sobreposição, mas acho que parte da diferença na imagem mental aqui são os vários casos de uso.

@markerikson Portanto, seu conselho é 'depende da situação que você encontrou', e como equilibrar a 'lógica de negócios' em ações ou redutores depende apenas de sua consideração. Devemos também aproveitar os benefícios da função pura tanto quanto possível?

Sim. Os redutores _devem_ ser puros, como um requisito do Redux (exceto em 0,00001% dos casos especiais). Os criadores de ação absolutamente _não _ precisam ser puros e, de fato, são onde a maioria das suas "impurezas" viverá. No entanto, uma vez que funções puras são obviamente mais fáceis de entender e testar do que funções impuras, _se_ você pode fazer parte de sua lógica de criação de ação pura, ótimo! Se não, tudo bem.

E sim, do meu ponto de vista, depende de você, como desenvolvedor, determinar qual é o equilíbrio apropriado para a lógica do seu próprio aplicativo e onde ele reside. Não existe uma regra única e rígida para qual lado da divisão criador / redutor de ação ela deve permanecer. (Erm, exceto pelo "determinismo / não determinismo" que mencionei acima. O que eu claramente pretendia fazer referência neste comentário. Obviamente.)

@cpsubrian

O que fazer com o resultado: redutor

Na verdade, é para isso que servem as sagas: para lidar com efeitos como "se isso aconteceu, então também deveria acontecer"


@markerikson @LumiaSaki

Os criadores de ações absolutamente não precisam ser puros e, de fato, são onde a maioria das suas "impurezas" viverá.

Na verdade, os criadores de ações não precisam ser impuros ou mesmo existir.
Veja http://stackoverflow.com/a/34623840/82609

E sim, do meu ponto de vista, depende de você, como desenvolvedor, determinar qual é o equilíbrio apropriado para a lógica do seu próprio aplicativo e onde ele reside. Não existe uma regra única e rígida para qual lado da divisão criador / redutor de ação ela deve permanecer.

Sim, mas não é tão óbvio notar as desvantagens de cada abordagem sem experiência :) Veja também meu comentário aqui: https://github.com/reactjs/redux/issues/1171#issuecomment -167585575

Nenhuma regra estrita funciona bem para a maioria dos aplicativos simples, mas se você deseja construir componentes reutilizáveis, esses componentes não devem estar cientes de algo fora de seu próprio escopo.

Portanto, em vez de definir uma lista de ação global para todo o seu aplicativo, você pode começar a dividir seu aplicativo em componentes reutilizáveis ​​e cada componente tem sua própria lista de ações e só pode despachá-las / reduzi-las. O problema é, então, como você expressa "quando a data é selecionada no meu selecionador de data, devemos salvar um lembrete sobre esse item a fazer, mostrar um brinde de feedback e, em seguida, navegar no aplicativo para o todos com lembretes": é aqui que o saga entra em ação: orquestrando os componentes

Veja também https://github.com/slorber/scalable-frontend-with-elm-or-redux

E sim, do meu ponto de vista, depende de você, como desenvolvedor, determinar qual é o equilíbrio apropriado para a lógica do seu próprio aplicativo e onde ele reside. Não existe uma regra única e rígida para qual lado da divisão criador / redutor de ação ela deve permanecer.

Sim, não há nenhuma exigência do Redux se você colocar sua lógica nos redutores ou criadores de ação. Redux não vai quebrar de qualquer maneira. Não existe uma regra rígida e rápida que exija que você faça isso de uma forma ou de outra. Mas a recomendação de Dan era "manter o log de ações o mais próximo possível do histórico de interações do usuário". O despacho de uma única ação por evento do usuário não é obrigatório, mas é recomendado.

No meu caso tenho 2 redutores interessados ​​em 1 ação. O action.data bruto não é suficiente. Eles precisam lidar com dados transformados. Não queria realizar a transformação nos 2 redutores. Então, mudei a função para realizar a transformação em uma conversão. Desta forma, meus redutores recebem dados prontos para consumo. Este é o melhor que eu poderia pensar em minha curta experiência redux de 1 mês.

Que tal separar os componentes / visualizações da estrutura da loja? meu objetivo é que tudo o que é afetado pela estrutura da loja seja gerenciado nos redutores, por isso gosto de colocar os seletores com os redutores, para que os componentes não precisem realmente saber como chegar a um determinado nó da loja.

Isso é ótimo para passar dados para os componentes, e o contrário, quando os componentes despacham ações:

Digamos, por exemplo, em um aplicativo Todo, estou atualizando o nome de um item Todo, então despacho uma ação passando a parte do item que desejo atualizar, ou seja:

dispatch(updateItem({name: <text variable>}));

, e a definição da ação é:

const updateItem = (updatedData) => {type: "UPDATE_ITEM", updatedData}

que por sua vez é manipulado pelo redutor que poderia simplesmente fazer:

Object.assign({}, item, action.updatedData)

para atualizar o item.

Isso funciona muito bem, pois posso reutilizar a mesma ação e redutor para atualizar qualquer prop do item Todo, ou seja:

updateItem({description: <text variable>})

quando a descrição é alterada em vez disso.

Mas aqui o componente precisa saber como um item Todo é definido na loja e se essa definição mudar eu preciso lembrar de mudar em todos os componentes que dependem dele, o que obviamente é uma má ideia, alguma sugestão para esse cenário?

@dcoellarb

Minha solução nesse tipo de situação é aproveitar a flexibilidade do Javascript para gerar o que seria um clichê.

Então, eu posso ter:

const {reducer, actions, selector} = makeRecord({
    name: TextField,
    description: TextField,
    completed: BooleanField
})

Onde makeRecord é uma função para construir automaticamente redutores, criadores de ação e seletores a partir da minha descrição. Isso elimina o clichê, mas se eu precisar fazer algo que não se encaixa nesse padrão legal mais tarde, posso adicionar redutor / ações / seletor personalizados ao resultado de makeRecord.

tks @winstonewert gosto da abordagem para evitar o clichê, vejo que economiza muito tempo em aplicativos com muitos modelos; mas ainda não vejo como isso irá separar o componente da estrutura da loja, ou seja, mesmo que a ação seja gerada, o componente ainda precisará passar os campos atualizados para ele, o que significa que o componente ainda precisa conhecer a estrutura de a loja certo?

@winstonewert @dcoellarb Em minha opinião, a estrutura de carga útil da ação deve pertencer a ações, não a redutores, e ser explicitamente traduzida em estrutura de estado em um redutor. Deve ser uma feliz coincidência que essas estruturas se espelhem para uma simplicidade inicial. Essas duas estruturas não precisam se espelhar sempre porque não são a mesma entidade.

@sompylasar certo, eu faço isso, traduzo os dados api / rest para a estrutura da minha loja, mas ainda assim o único que deve saber a estrutura da

@dcoellarb Você pode pensar em suas visualizações como entradas de dados de certo tipo (como string ou número, mas objeto estruturado com campos). Este objeto de dados não reflete necessariamente a estrutura da loja. Você coloca este objeto de dados em uma ação. Isso não está associado à estrutura da loja. Tanto a loja quanto a visualização devem ser acopladas à estrutura de carga útil da ação.

@sompylasar faz sentido, vou tentar, muito obrigado !!!

Provavelmente também devo acrescentar que você pode tornar as ações mais puras usando redux-saga . No entanto, redux-saga se esforça para lidar com eventos assíncronos, então você pode levar essa ideia um passo adiante usando RxJS (ou qualquer biblioteca FRP) em vez de redux-saga. Aqui está um exemplo usando KefirJS: https://github.com/awesome-editor/awesome-editor/blob/saga-demo/src/stores/app/AppSagaHandlers.js

Olá @frankandrobot ,

redux-saga se esforça para lidar com eventos assíncronos

O que você quer dizer com isso? redux-saga feito para lidar com eventos assíncronos e efeitos colaterais de uma maneira elegante? Dê uma olhada em https://github.com/reactjs/redux/issues/1171#issuecomment -167715393

Não @IceOnFire . A última vez que li os documentos redux-saga , lidar com fluxos de trabalho assíncronos complexos é difícil. Veja, por exemplo: http://yelouafi.github.io/redux-saga/docs/advanced/NonBlockingCalls.html
Disse (ainda diz?) Algo nesse sentido

vamos deixar o resto dos detalhes para o leitor porque está começando a ficar complexo

Compare isso com o método FRP: https://github.com/frankandrobot/rflux/blob/master/doc/06-sideeffects.md#a -more-complex-workflow
Todo esse fluxo de trabalho é controlado completamente. (Devo acrescentar apenas algumas linhas.) Além disso, você ainda obtém a maior parte das vantagens de redux-saga (tudo é uma função pura, principalmente testes de unidade fáceis).

A última vez que pensei sobre isso, cheguei à conclusão de que o problema é que a saga redux faz tudo parecer síncrono. É ótimo para fluxos de trabalho simples, mas para fluxos de trabalho assíncronos complexos, é mais fácil se você lidar com o assíncrono explicitamente ... que é o que o FRP se destaca.

Olá @frankandrobot ,

Obrigado pela sua explicação. Não vejo a citação que você mencionou, talvez a biblioteca tenha evoluído (por exemplo, agora vejo um efeito cancel que nunca vi antes).

Se os dois exemplos (saga e FRP) estão se comportando exatamente da mesma forma, não vejo muita diferença: um é uma sequência de instruções dentro de blocos try / catch, enquanto o outro é uma cadeia de métodos em streams. Devido à minha falta de experiência em streams, acho ainda mais legível o exemplo da saga e mais testável, pois você pode testar cada yield um por um. Mas tenho certeza de que isso se deve mais à minha mentalidade do que às tecnologias.

De qualquer forma, adoraria saber a opinião de @yelouafi sobre isso.

@bvaughn você pode apontar para algum exemplo decente de ação de teste, redutor, seletor no mesmo teste que você descreve aqui?

A maneira mais eficiente de testar ações, redutores e seletores é seguir a abordagem "ducks" ao escrever testes. Isso significa que você deve escrever um conjunto de testes que cubra um determinado conjunto de ações, redutores e seletores, em vez de 3 conjuntos de testes que se concentrem em cada um individualmente. Isso simula com mais precisão o que acontece em sua aplicação real e fornece o melhor retorno para o investimento.

Olá @ morgs32 😄

Esse problema é um pouco antigo e eu não uso o Redux há algum tempo. Há uma seção no site Redux sobre como escrever testes que você pode querer dar uma olhada.

Basicamente, eu estava apenas apontando que - em vez de escrever testes para ações e redutores isoladamente - pode ser mais eficiente escrevê-los juntos, assim:

import configureMockStore from 'redux-mock-store'
import { actions, selectors, reducer } from 'your-redux-module';

it('should support adding new todo items', () => {
  const mockStore = configureMockStore()
  const store = mockStore({
    todos: []
  })

  // Start with a known state
  expect(selectors.todos(store.getState())).toEqual([])

  // Dispatch an action to modify the state
  store.dispatch(actions.addTodo('foo'))

  // Verify the action & reducer worked successfully using your selector
  // This has the added benefit of testing the selector also
  expect(selectors.todos(store.getState())).toEqual(['foo'])
})

Esta foi apenas minha própria observação após usar Redux por alguns meses em um projeto. Não é uma recomendação oficial. YMMV. 👍

"Faça mais em criadores de ação e menos em redutores"

E se o aplicativo for servidor e cliente e o servidor deve conter lógica de negócios e validadores?
Então, eu envio a ação como está, e a maior parte do trabalho será feita no lado do servidor pelo redutor ...

Primeiro, desculpe pelo meu inglês. Mas tenho algumas opiniões diferentes.

Minha escolha é fat redutor, thin criadores de ação.

Meus criadores de ação apenas __dispatch__ ações (async, sync, serial-async, paralelo-async, paralelo-async em for-loop) com base em algum __promise middleware__.

Meus redutores se dividem em várias pequenas fatias de estado para lidar com a lógica de negócios. use combineReduers combiná-los. reducer é __função pura__, então é fácil de ser reutilizada. Talvez um dia eu use o angularJS, acho que posso reutilizar meu reducer em meu serviço para a mesma lógica de negócios. Se o seu reducer tem muitos códigos de linhas, ele pode se dividir em um redutor menor ou abstrair algumas funções.

Sim, existem alguns casos de estado cruzado cujos significados A dependem de B, C .. e B, C são dados assíncronos. Devemos usar B,C para preencher ou inicializar A. É por isso que uso crossSliceReducer .

Sobre __Faça mais em criadores de ação e menos em redutores__.

  1. Se você usar redux-thunk ou etc. Sim. Você pode acessar o estado completo dos criadores de ação por getState() . Esta é uma escolha. Ou, você pode criar algum __crossSliceReducer__, para que possa acessar o estado completo também, você pode usar alguma fatia de estado para calcular seu outro estado.

Sobre __Teste da unidade__

reducer é __função pura__. Portanto, é fácil testar. Por enquanto, apenas testo meus redutores, porque é mais importante do que a outra parte.

Para testar action creators ? Eu acho que se eles são "gordos", talvez não seja fácil testar. Especialmente __criadores de ação assíncrona__.

Eu concordo com você @mrdulin, esse é o jeito que eu também fui.

@mrdulin Sim. Parece que o middleware é o lugar certo para colocar sua lógica impura.
Mas, para a lógica de negócios, o redutor não parece o lugar certo. Você acabará com várias ações "sintéticas" que não representam o que o usuário pediu, mas o que sua lógica de negócios exige.

Uma escolha muito mais simples é apenas chamar algumas funções / métodos de classe puros do middleware:

middleware = (...) => {
  // if(action.type == 'HIGH_LEVEL') 
  handlers[action.name]({ dispatch, params: action.payload })
}
const handlers = {
  async highLevelAction({ dispatch, params }) {
    dispatch({ loading: true });
    const data = await api.getData(params.someId);
    const processed = myPureLogic(data);
    dispatch({ loading: false, data: processed });
  }
}

@bvaughn

Isso reduz a probabilidade de variáveis ​​digitadas incorretamente que levam a valores indefinidos sutis, simplifica as alterações na estrutura de sua loja, etc.

Meu caso contra o uso de seletores em todos os lugares, mesmo para peças triviais de estado que não requerem memoização ou qualquer tipo de transformação de dados:

  • Os testes de unidade para redutores já não deveriam detectar propriedades de estado digitadas incorretamente?
  • Mudanças na estrutura da loja não ocorrem com tanta frequência na minha experiência, principalmente na fase de desenvolvimento de um projeto. Mais tarde, se você alterar state.stuff.item1 para state.stuff.item2 você pesquisa o código e o altera em todos os lugares - exatamente como alterar o nome de qualquer outra coisa. É uma tarefa comum e um não-brainer para pessoas que usam IDEs especialmente.
  • Usar seletores em todos os lugares é uma espécie de abstração vazia. simples de estado diretamente. Claro, você ganha consistência por ter essa API para acessar o estado, mas desiste da simplicidade.

Obviamente, os seletores são necessários, mas gostaria de ouvir alguns outros argumentos para torná-los uma API obrigatória.

De um ponto de vista prático, um bom motivo em minha experiência é que bibliotecas como a nova seleção ou a saga redux aproveitam os seletores para acessar partes do estado. Isso é o suficiente para eu ficar com os seletores.

Filosoficamente falando, sempre vejo os seletores como os "métodos getter" do mundo funcional. E pelo mesmo motivo pelo qual eu nunca acessaria atributos públicos de um objeto Java, nunca acessaria subestados diretamente em um aplicativo Redux.

@IceOnFire Não há nada a aproveitar se a computação não for cara ou a transformação de dados não for necessária.

Os métodos getter podem ser uma prática comum em Java, mas também o é acessar POJOs diretamente em JS.

@timotgl

Por que existe uma API entre a loja e outro código redux?

Os seletores são uma API pública de consulta (leitura) do redutor, as ações são uma API pública de comando (gravação) do redutor. A estrutura do Redutor é o seu detalhe de implementação.

Seletores e ações são usados ​​na camada de IU e na camada de saga (se você usar redux-saga), não no redutor em si.

@sompylasar Não tenho certeza se estou seguindo seu ponto de vista aqui. Não há alternativa para ações, devo usá-los para interagir com redux. Não preciso usar seletores, no entanto, posso apenas escolher algo diretamente do estado em que está exposto, o que é por design.

Você está descrevendo uma maneira de pensar sobre os seletores como uma API de "leitura" do redutor, mas minha pergunta era o que justifica tornar os seletores uma API obrigatória em primeiro lugar (obrigatória como para impor isso como uma prática recomendada em um projeto, não por biblioteca Projeto).

@timotgl Sim, os seletores não são obrigatórios. Mas eles são uma boa prática para se preparar para mudanças futuras no código do aplicativo, tornando possível refatorá-los separadamente:

  • como determinado pedaço de estado é estruturado e escrito para (a preocupação do redutor, um lugar)
  • como essa parte do estado é usada (a IU e os efeitos colaterais, muitos lugares onde a mesma parte do estado é consultada)

Quando você está prestes a alterar a estrutura da loja, sem seletores, você terá que encontrar e refatorar todos os lugares onde as partes de estado afetadas são acessadas, e isso poderia ser uma tarefa não trivial, não simplesmente localizar e substitua, especialmente se você passar fragmentos de estado obtidos diretamente na loja, não por meio de um seletor.

@sompylasar Obrigado por sua contribuição.

esta poderia ser uma tarefa potencialmente não trivial

Essa é uma preocupação válida, apenas parece uma troca cara para mim. Acho que não encontrei uma refatoração de estado que causasse esses problemas. Eu encontrei "espaguete de seletor", no entanto, onde seletores aninhados para cada subparte trivial de estado causaram bastante confusão. Afinal, essa contra-medida em si também deve ser mantida. Mas agora entendo melhor a razão por trás disso.

@timotgl Um exemplo simples que posso compartilhar publicamente:

export const PROMISE_REDUCER_STATE_IDLE = 'idle';
export const PROMISE_REDUCER_STATE_PENDING = 'pending';
export const PROMISE_REDUCER_STATE_SUCCESS = 'success';
export const PROMISE_REDUCER_STATE_ERROR = 'error';

export const PROMISE_REDUCER_STATES = [
  PROMISE_REDUCER_STATE_IDLE,
  PROMISE_REDUCER_STATE_PENDING,
  PROMISE_REDUCER_STATE_SUCCESS,
  PROMISE_REDUCER_STATE_ERROR,
];

export const PROMISE_REDUCER_ACTION_START = 'start';
export const PROMISE_REDUCER_ACTION_RESOLVE = 'resolve';
export const PROMISE_REDUCER_ACTION_REJECT = 'reject';
export const PROMISE_REDUCER_ACTION_RESET = 'reset';

const promiseInitialState = { state: PROMISE_REDUCER_STATE_IDLE, valueOrError: null };
export function promiseReducer(state = promiseInitialState, actionType, valueOrError) {
  switch (actionType) {
    case PROMISE_REDUCER_ACTION_START:
      return { state: PROMISE_REDUCER_STATE_PENDING, valueOrError: null };
    case PROMISE_REDUCER_ACTION_RESOLVE:
      return { state: PROMISE_REDUCER_STATE_SUCCESS, valueOrError: valueOrError };
    case PROMISE_REDUCER_ACTION_REJECT:
      return { state: PROMISE_REDUCER_STATE_ERROR, valueOrError: valueOrError };
    case PROMISE_REDUCER_ACTION_RESET:
      return { ...promiseInitialState };
    default:
      return state;
  }
}

export function extractPromiseStateEnum(promiseState = promiseInitialState) {
  return promiseState.state;
}
export function extractPromiseStarted(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_PENDING);
}
export function extractPromiseSuccess(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_SUCCESS);
}
export function extractPromiseSuccessValue(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_SUCCESS ? promiseState.valueOrError || null : null);
}
export function extractPromiseError(promiseState = promiseInitialState) {
  return (promiseState.state === PROMISE_REDUCER_STATE_ERROR ? promiseState.valueOrError || true : null);
}

Não é a preocupação desse usuário redutor se o que está sendo armazenado é state, valueOrError ou outra coisa. Estão expostos a string de estado (enum), algumas verificações freqüentemente usadas nesse estado, o valor e o erro.

Eu encontrei "espaguete de seletor", no entanto, onde seletores aninhados para cada subparte trivial de estado causaram bastante confusão.

Se esse aninhamento foi causado pelo espelhamento do aninhamento do redutor (composição), isso não é o que eu recomendaria, é o detalhe de implementação do redutor. Usando o exemplo acima, os redutores que usam a promessaReducer para algumas partes de seu estado exportam seus próprios seletores nomeados de acordo com essas partes. Além disso, nem toda função que se parece com um seletor precisa ser exportada e fazer parte da API do redutor.

function extractTransactionLogPromiseById(globalState, transactionId) {
  return extractState(globalState).transactionLogPromisesById[transactionId] || undefined;
}

export function extractTransactionLogPromiseStateEnumByTransactionId(globalState, transactionId) {
  return extractPromiseStateEnum(extractTransactionLogPromiseById(globalState, transactionId));
}

export function extractTransactionLogPromiseErrorTransactionId(globalState, transactionId) {
  return extractPromiseError(extractTransactionLogPromiseById(globalState, transactionId));
}

export function extractTransactionLogByTransactionId(globalState, transactionId) {
  return extractPromiseSuccessValue(extractTransactionLogPromiseById(globalState, transactionId));
}

Ah, mais uma coisa que quase esqueci. Nomes de funções e exportações / importações podem ser minimizados de maneira adequada e segura. Chaves de objetos aninhados - nem tanto, você precisa de um rastreador de fluxo de dados adequado no minificador para não bagunçar o código.

@timotgl : muitas de nossas melhores práticas encorajadas com o Redux são sobre tentar encapsular a lógica e o comportamento relacionados ao Redux.

Por exemplo, você pode despachar ações diretamente de um componente conectado, fazendo this.props.dispatch({type : "INCREMENT"}) . No entanto, desencorajamos isso, porque força o componente a "saber" que está falando com Redux. Uma maneira mais idiomática do React de fazer as coisas é passar criadores de ação vinculada, de modo que o componente possa apenas chamar this.props.increment() , e não importa se essa função é um criador de ação Redux vinculado, um retorno de chamada passado baixado por um dos pais ou uma função simulada em um teste.

Você também pode escrever à mão os tipos de ação em qualquer lugar, mas encorajamos a definição de variáveis ​​constantes para que elas possam ser importadas, rastreadas e diminuam a chance de erros de digitação.

Da mesma forma, não há nada que o impeça de acessar state.some.deeply.nested.field em suas funções mapState ou thunks. Mas, como já foi descrito neste tópico, isso aumenta as chances de erros de digitação, torna mais difícil rastrear lugares em que uma parte específica do estado está sendo usada, torna a refatoração mais difícil e significa que qualquer lógica de transformação cara é provavelmente -executando todas as vezes, mesmo que não seja necessário.

Portanto, não, você _não_ tem que usar seletores, mas eles são uma boa prática arquitetônica.

Você pode querer ler meu post Redux idiomático: Usando seletores de novo para encapsulamento e desempenho .

@markerikson Não estou contestando os seletores em geral ou os seletores para cálculos caros. E eu nunca tive a intenção de passar o despacho para um componente :)

Meu ponto é que eu discordo dessa crença:

O ideal é que apenas as funções e seletores do redutor conheçam a estrutura de estado exata, portanto, se você alterar a localização de algum estado, precisará apenas atualizar essas duas partes da lógica.

Sobre o seu exemplo com state.some.deeply.nested.field , posso ver o valor de ter um seletor para encurtar isso. selectSomeDeeplyNestedField() é um nome de função contra 5 propriedades que eu poderia errar.

Por outro lado, se você seguir esta diretriz à risca, também estará fazendo const selectSomeField = state => state.some.field; ou mesmo const selectSomething = state => state.something; e, em algum ponto, a sobrecarga (importação, exportação, teste) de fazer isso de forma consistente não justifica mais a (discutível) segurança e pureza em minha opinião. É bem intencionado, mas não consigo me livrar do espírito dogmático da diretriz. Eu confiava que os desenvolvedores do meu projeto usariam seletores com sabedoria e quando aplicável - porque minha experiência até agora tem sido que eles fazem isso.

Sob certas condições, eu poderia ver porque você iria querer errar do lado da segurança e das convenções. Obrigado pelo seu tempo e envolvimento, estou muito feliz em termos redux.

Certo. FWIW, existem outras bibliotecas de seletores e também wrappers para Reselect . Por exemplo, https://github.com/planttheidea/selectorator permite definir caminhos de chave de notação de ponto e faz os seletores intermediários para você internamente.

Esta página foi útil?
0 / 5 - 0 avaliações