Redux: Melhor prática para atualizar o estado dependente em diferentes ramos da árvore de estado?

Criado em 18 set. 2015  ·  31Comentários  ·  Fonte: reduxjs/redux

Nos documentos do Redutor, diz:

Sometimes state fields depend on one another and more consideration is required, but in our case we can easily split updating todos into a separate function..

Gostaria de saber como lidar com esse caso de uso específico. Para dar um exemplo, digamos que eu tenha um formulário com vários campos de dados. Eu tenho outro componente em outro lugar que deve reagir às mudanças no formulário, por exemplo, mudar o estado com base na validade dos dados do formulário ou calcular valores de diferentes entradas para o formulário e exibi-los.

Embora eu acredite que posso ter redutores diferentes (que lidam com diferentes fatias da árvore de estado) ouvindo a mesma ação e se atualizando, o estado "reativo" não teria acesso aos outros ramos da árvore de estado de que precisa para computar a si mesmo.

Uma solução que pensei é fazer com que o processo de "reação" aconteça usando subscribe para ouvir as mudanças na árvore de estado, calcular se uma mudança relevante foi feita ou não e, em seguida, despachar uma nova ação para atualizar os campos dependentes. No entanto, essa explosão de publicação pode ter que se tornar muito genérica, para que todos os redutores que se importam com ela possam usar a carga útil que vem com ela.

Eu sou novo no Redux, então não tenho certeza se existe um padrão adotado para esse tipo de coisa. Eu apreciaria qualquer conselho ou indicação na direção certa. Obrigado.

question

Comentários muito úteis

Quando você precisar de diferentes partes da árvore de estado para resolver os efeitos de uma única ação que não pode ser tratada de forma mutuamente exclusiva em cada ramo, use reduce-reducers para combinar o redutor produzido por combineReducer com seu redutor transversal:

export default reduceReducers(
  combineReducers({
    router: routerReducer,
    customers,
    stats,
    dates,
    filters,
    ui
  }),
  // cross-cutting concerns because here `state` is the whole state tree
  (state, action) => {
    switch (action.type) {
      case 'SOME_ACTION':
        const customers = state.customers;
        const filters = state.filters;
        // ... do stuff
    }
  }
);

Todos 31 comentários

Desculpe, a outra opção que descobri é usar um rootReducer personalizado. Tecnicamente, esse redutor não teria que estar na raiz; poderia ser apenas o Ancestral Comum Mais Baixo das partes da árvore de estado que dependem umas das outras.

Se os dados _podem_ ser calculados a partir de um estado “inferior”, não os mantenha nesse estado. Use seletores memoized conforme descrito em Computing Derived Data .

Se não puder, de fato, usar um único redutor que pode (se desejado) passar parâmetros adicionais para os redutores ou usar seus resultados:

function a(state, action) { }
function b(state, action, a) { } // depends on a's state

function something(state = {}, action) {
  let a = a(state.a, action);
  let b = b(state.b, action, a); // note: b depends on a for computation
  return { a, b };
}

Obrigado @gaearon por esse trecho. Eu rapidamente me deparei com esse problema ao tentar portar um widget para redux e passei algum tempo tentando encontrar uma solução sancionada pela comunidade. Talvez isso seja algo que pode ser mais proeminente na documentação, pois imagino que seja bastante comum ter um estado dependente.

Também parece que seria trivial passar toda a árvore de estados como um terceiro parâmetro opcional dentro das entranhas da função combineReducers . Fazer isso facilitaria exatamente esse problema, embora eu não tenha experiência suficiente com redux para ter qualquer noção se seria uma boa ou má ideia. Pensamentos?

Ele tem sua própria seção nos documentos , então acho que é muito proeminente pessoalmente. :sorriso:

Você não precisa usar combineReducers se não quiser. É um utilitário útil, mas não um requisito. Talvez reduzir-redutores ou redux-ações seja mais o seu estilo?

Se os dados podem ser calculados a partir de um estado “inferior”, não os mantenha no estado.

Ele tem sua própria seção nos documentos, então acho que é muito proeminente pessoalmente.

Ok, acho que finalmente funcionou para mim. Obrigado!

"Se os dados podem ser calculados de um estado“ inferior ”, não os mantenha no estado.

E se os dados só pudessem ser calculados a partir de um estado "menor" combinado com a carga útil de uma ação?

Quando você precisar de diferentes partes da árvore de estado para resolver os efeitos de uma única ação que não pode ser tratada de forma mutuamente exclusiva em cada ramo, use reduce-reducers para combinar o redutor produzido por combineReducer com seu redutor transversal:

export default reduceReducers(
  combineReducers({
    router: routerReducer,
    customers,
    stats,
    dates,
    filters,
    ui
  }),
  // cross-cutting concerns because here `state` is the whole state tree
  (state, action) => {
    switch (action.type) {
      case 'SOME_ACTION':
        const customers = state.customers;
        const filters = state.filters;
        // ... do stuff
    }
  }
);

@neverfox Obrigado por isso! exatamente o que eu estava procurando. Você conhece alguma outra maneira de acessar o estado raiz do redutor combinado ou de fazer uma nova arquitetura?

@imton

Para o seu exemplo específico, não sei por que não quero permitir que diferentes redutores tratem das mesmas ações. Esse é o objetivo de usar o Redux. Consulte https://gist.github.com/imton/cf6f40578524ddd085dd#gistcomment -1656424.

Não acho que redutores “transversais” sejam geralmente uma boa ideia. Eles quebram o encapsulamento de redutores individuais. É difícil mudar a forma do estado interno porque alguns outros redutores externos do código podem depender dele.

Por outro lado, recomendamos _não_ depender da forma de estado em qualquer lugar, exceto nos próprios redutores. Mesmo os componentes podem evitar depender da forma do estado se você exportar "seletores" (funções que consultam o estado) dos mesmos arquivos que os redutores. Confira shopping-cart exemplo no repositório para esta abordagem.

Para ser claro, eu estava apenas sugerindo o redutor transversal para casos que não são mutuamente exclusivos, viz. não pode ser tratado apenas por redutores separados respondendo à mesma ação. Por exemplo, uma ação chega e precisa pegar alguns dados de um ramo da árvore (normalmente manipulado pelo redutor A) e alguns dados de outro (normalmente manipulado pelo redutor B) e derivar alguns novos dados de estado da combinação deles ambos (talvez para ser armazenado em outro ramo de estado C). Se houver uma maneira de fazer isso sem refatorar a forma de estado ou duplicar dados (o que nem sempre é viável ou desejável), certamente estou aberto a sugestões, porque esse tipo de situação surge muito à medida que os aplicativos crescem e novos aparecem recursos que não combinam com o design original.

Para computar dados derivados, sugerimos usar seletores em vez de armazená-los na árvore de estado.

http://redux.js.org/docs/recipes/ComputingDerivedData.html

O que é ótimo e útil, até que precise fazer parte do estado.

O que você quer dizer com "necessidades"? Você pode descrever um cenário específico ao qual está se referindo?

@gaearon ,

Conforme você insere caracteres em um campo de senha, executamos as seguintes regras de validação na propriedade de estado password :

  • Existem 3 ou mais caracteres consecutivos?
  • Existe um espaço em password ?
  • username parte de password ?

Se alguma das opções acima for verdadeira, mostraremos um erro de nível de página com uma mensagem de erro de nível de página específica correspondente e, dependendo de qual erro, podemos mostrar uma mensagem de erro de nível de campo.

Portanto, terei 3 redutores, 1 cada para username e password , para simplesmente manter os valores das strings, e 1 para notification , que lida com a notificação de erro no nível da página message e type :

// reducers/notifcation.js
import { ENTER_PASSWORD } from "../../actions/CreateAccount/types";
import strings from "../../../../example/CreateAccount/strings_EN";



const DEFAULT_STATE = {
  type: '',
  message: ''
};

export default (state = DEFAULT_STATE, action) => {
  const { value: password, type } = action;

  if (type === ENTER_PASSWORD) {
    const errors = strings.errors.password;
    let message;

    if (password.length <= 3) {
      message = errors.tooShort;
    } else if (password.indexOf(username) !== -1) {
      message = errors.containsUsername;
    } else if (password.indexOf(' ') !== -1) {
      message = errors.containsSpace;
    }

    if (message) {
      return {
        message,
        type: 'error'
      };
    }
  }
  return DEFAULT_STATE;
};

Portanto, neste caso, o redutor notification precisa saber sobre username e password . Podemos acessar password porque estamos preocupados com o tipo de ação ENTER_PASSWORD que o inclui, mas, como está organizado atualmente, eu não obteria username e password juntos. Em teoria, eu também deveria verificar se há um tipo de ação ENTER_USERNAME para ter certeza de que, se eles já inseriram password e, em seguida, retornar ao campo de nome de usuário e modificar o username , então eu verifico para ter certeza de que password não contém o username atualizado ... Mas podemos pular isso por agora.

Pensamentos?

Uma solução seria agrupar username e password juntos como credentials e fundir nossos ENTER_PASSWORD e ENTER_USERNAME tipos de ação em um único ENTER_CREDENTIALS dispatch, mas para merdas e risadinhas, digamos que isso não seja viável nesta situação.

Por favor, dê uma olhada no redux-form e como ele lida com cenários como este. Em geral, é melhor fazer perguntas no StackOverflow do que aqui. No momento, não posso dar respostas longas porque estou ocupado.

Criei combineSectionReducers .
Ele resolve isso de uma forma como combineReducers 'one.

Talvez saiba algo sobre isso? @gaearon https://github.com/omnidan/redux-undo/issues/102

Obrigado!

@gaearon A discussão com @neverfox parou abruptamente, mas acho que sei o que ele quis dizer - me deparei com o mesmo problema:

Digamos que temos um redutor de raiz composto usando combineReducers com ramos de estado: chat e ui . Dentro do redutor específico de chat , precisamos reagir a uma ação _ (por exemplo, recebemos novas mensagens do servidor) _ de uma forma que depende de um estado de ui part _ (por exemplo, anexar o novo msgs apenas se o final da lista de msgs estiver na janela de visualização - essa informação é mantida na parte ui ) _. Não podemos acessar o estado do branch ui dentro do branch chat - portanto, é impossível usar um seletor.

A primeira solução que me veio à mente foi fazer a parte condicional em um thunk dedicado e desse lugar despachar uma das duas ações dedicadas. Isso, entretanto, introduz várias criações adicionais e, portanto, obscurece bastante o que está acontecendo.

Por este motivo acho a solução do @neverfox a mais limpa, embora, como você disse, quebre o encapsulamento. Você tem outra opinião / solução sobre este assunto depois de declarar o problema como eu fiz acima?

@jalooc : O Redux FAQ discute esse problema, em http://redux.js.org/docs/FAQ.html#reducers -share-state.

Basicamente, as opções principais são "passar mais dados na ação" ou "escrever lógica redutora que sabe pegar de A e passar para B". Eu definitivamente sinto que a abordagem de @neverfox é a melhor maneira de lidar com isso se você optar pela abordagem centrada no redutor.

Também estou escrevendo um novo conjunto de documentos sobre redutores de estruturação, em # 1784. Uma das novas páginas aborda especificamente esse conceito - "Além de combineReducers" .

Eu definitivamente estaria interessado em mais comentários sobre a página de rascunho do documento e sobre este tópico em geral.

O que quer que as pessoas digam aqui. Eu suspeito que os problemas que as pessoas geralmente enfrentam são apenas quando arquitetam seu estado de uma maneira não ideal. Se houver muita dependência entre seus ramos na árvore de estado, isso pode ser uma indicação de dados duplicados e pode usar o estado 'menor' / 'mais enxuto'. A plataforma React + Redux está dando a você um imenso poder em suas mãos e atualizações de IU altamente determinísticas. Aqueles que não sofreram os problemas de atualizações de interface do usuário estranhas e loops de ligações de 2 vias provavelmente não entenderão isso. Quando há muito poder em suas mãos, significa que você pode facilmente atirar no próprio pé, o que será um pesadelo e sentirá a dor de todos. Depois de acertar a parte da arquitetura estadual, todo o resto se tornará fácil. Então, basicamente, você precisa gastar mais tempo na arquitetura de estado, mas pode colher benefícios depois disso.
Também acho que é altamente recomendável usar o estado enxuto e a recomputação normal e primeiro ver se você tem problemas de desempenho. Muito importante. Use a abordagem de nova seleção apenas se você observar problemas de desempenho na recomputação (sim, a nova seleção pode ser aplicada de forma incremental, certo @gaearon ?). Eu fiz a abordagem e percebi que a recomputação não afetou em nada o que eu temia que certamente aconteceria, mas nunca aconteceu, por outro lado, sou capaz de classificar os itens a cada pressionamento de tecla e o aplicativo parece extremamente rápido, mesmo sem usar selecione novamente. Muitas vezes as pessoas tendem a pensar muito sobre as etapas de recomputação, mas nunca se esqueça que os processadores médios de hoje podem realizar centenas e milhares de tipos em fração de segundo. História verdadeira.

Alguém já considerou essa abordagem? Quais são as armadilhas?

const combinedReducers = combineReducers({
  dog: dogReducer,
  cat: catReducer
})

const stateInjectedReducers = (state, action) => {
  return {
    ...state,
    frog : frogReducer(state.frog, action, state) // frogReducer depends on cat state
  }
}

export default reduceReducers(combinedReducers, stateInjectedReducers)

É barato e fácil, mas me atrai porque significa que não terei que reescrever stateInjectedReducers para quaisquer novos redutores que precisem de injeção de estado de outro lugar.

@funwithtriangles : sim, abordagem perfeitamente boa! Na verdade, eu falo sobre isso em http://redux.js.org/docs/recipes/reducers/BeyondCombineReducers.html e na minha postagem do blog em http://blog.isquaredsoftware.com/2017/01/practical-redux -part-7-forms-edition-reducers / .

@markerikson feliz em saber que não estava fazendo nada muito louco! :) A ironia é que, depois de chegar a essa abordagem, decidi que minhas fatias de estado dog , cat e frog estavam muito entrelaçadas para serem redutores separados e fundi-os em um ! 😛

Só por curiosidade, é uma má prática simplesmente ter vários redutores respeitando a mesma ação e, em seguida, acionar suas próprias alterações respectivas em suas próprias árvores de estado? Eu também acho que é possível se você tiver que fazer isso, pode ser um indicador de que você estruturou sua árvore de estados incorretamente, mas, para fins, digamos que você tenha dois estados diferentes

const combinedReducers = combineReducers({
  a: aReducer,
  b: bReducer
})

E você queria acionar uma mudança de estado em ambas as árvores de estado a partir de um despacho. É uma má prática simplesmente ter em ambos os redutores a mesma ação? Inevitavelmente, o despacho passa por todo combinedReducers qualquer maneira e acaba fazendo a mudança de estado em ambas as árvores. O único problema é que você tem que cavar e ver se certas ações são mudanças de gatilho em diferentes árvores de estado (mas podemos usar uma convenção de nomenclatura para ajudar a lidar com isso).

@augbog Isso é totalmente

@augbog : esse é absolutamente um caso de uso pretendido para o Redux! Para obter mais informações, consulte minha postagem no blog The Tao of Redux, Parte 1 - Implementação e Intenção .

Outro ângulo - use um seletor na raiz de sua árvore de estado para derivar os dados específicos de que você precisa, chamando seletores de sub-ramos do estado para obter os dados necessários e, em seguida, retorne a forma de dados de que você precisa:

const selector = state => {
    const x = someSelectorForA(state.a);
    const y = someSelectorForB(state.b);
    return compute(x,y);
}

Seus redutores permanecem encapsulados, sem condições de corrida, e o seletor está fazendo o trabalho pesado. Pode levar a alguns bons padrões recursivos.

Sempre que fico tentado a alcançar os ramos da árvore de estado, geralmente é quando eu deveria estar usando um seletor. Tento muito seguir estes princípios:

  1. A árvore de estado não deve conter nada que seja derivado de qualquer outra coisa na árvore de estado.
  2. Os seletores devem ser usados ​​para calcular os dados derivados.

No que diz respeito aos formulários, para mim você tem 2 coisas que pertencem à loja:

  1. Regras de validação
  2. Dados da entrada do usuário (texto, desfoque, outros eventos)

Em seguida, você usa um seletor para calcular se 2 segue as regras definidas em 1. Não há razão para cruzar a árvore de estado em um redutor.

Às vezes, penso: "Não parece que se um formulário é válido ou não deve ser refletido no estado do aplicativo? Parece uma coisa importante para controlar." Mesmo assim, viola o princípio de que o estado deve ser a única fonte da verdade. Se houver um bug onde a validade é calculada incorretamente, seu estado será inconsistente internamente. Isso deveria ser impossível.

Os seletores são lugares perfeitamente bons para armazenar dados derivados, mesmo se os resultados forem importantes. Use um seletor memoized como

E se o seu redutor precisa das informações contextuais dos dados derivados para interpretar uma ação corretamente, você pode apenas incluir isso com a ação ao despachá-la.

Uma alternativa para esse problema é usar redux-thunk. A solução é usar ações intermediárias, para disparar mais ações cientes do contexto com base no estado atual.

function incrementIfOdd() {
  return (dispatch, getState) => {
    const { counter } = getState();

    if (counter % 2 === 0) {
      return;
    }

    dispatch(increment());
  };
}

Usando essa solução, as ações e os redutores permanecem burros e a lógica se move para uma camada intermediária que faz verificações e deduções extras com base na interação do usuário e no estado atual.

Ele tem sua própria seção nos documentos , então acho que é muito proeminente pessoalmente.

O link com essa citação está quebrado, mas acredito que se refira a isso: https://redux.js.org/docs/recipes/ComputingDerivedData.html

Em uma segunda olhada, porém, esta seção parece que pode ser tão relevante, senão mais: https://redux.js.org/docs/recipes/reducers/BeyondCombineReducers.html

Sim, é para onde ele foi movido.

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

Questões relacionadas

timdorr picture timdorr  ·  3Comentários

elado picture elado  ·  3Comentários

ramakay picture ramakay  ·  3Comentários

CellOcean picture CellOcean  ·  3Comentários

caojinli picture caojinli  ·  3Comentários