Redux: Tentando colocar chamadas de API no lugar correto

Criado em 20 jul. 2015  ·  115Comentários  ·  Fonte: reduxjs/redux

Estou tentando fazer um fluxo de sucesso / erro de login, mas minha principal preocupação é onde posso colocar essa lógica.

Atualmente estou usando actions -> reducer (switch case with action calling API) -> success/error on response triggering another action .

O problema com essa abordagem é que o redutor não está funcionando quando eu chamo a ação da chamada de API.

estou esquecendo de algo?

Redutor

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';
import Immutable from 'immutable';
import LoginApiCall from '../utils/login-request';

const initialState = new Immutable.Map({
  email: '',
  password: '',
}).asMutable();

export default function user(state = initialState, action) {
  switch (action.type) {
    case LOGIN_ATTEMPT:
      console.log(action.user);
      LoginApiCall.login(action.user);
      return state;
    case LOGGED_FAILED:
      console.log('failed from reducer');
      return state;
    case LOGGED_SUCCESSFULLY:
      console.log('success', action);
      console.log('success from reducer');
      break;
    default:
      return state;
  }
}

ações

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return dispatch => {
    dispatch({ error, type: LOGGED_FAILED });
  };
}

/*
 * Should add the route like parameter in this method
*/
export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    // router.transitionTo('/dashboard'); // will fire CHANGE_ROUTE in its change handler
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return dispatch => {
    dispatch({ user, type: LOGIN_ATTEMPT });
  };
}

Chamadas API

 // Use there fetch polyfill
 // The main idea is create a helper in order to handle success/error status
import * as LoginActions from '../actions/LoginActions';

const LoginApiCall = {
  login(userData) {
    fetch('http://localhost/login', {
      method: 'post',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: userData.email,
        password: userData.password,
      }),
    })
    .then(response => {
      if (response.status >= 200 && response.status < 300) {
        console.log(response);
        LoginActions.loginSuccess(response);
      } else {
        const error = new Error(response.statusText);
        error.response = response;
        LoginActions.loginError();
        throw error;
      }
    })
    .catch(error => { console.log('request failed', error); });
  },
};

export default LoginApiCall;
docs question

Comentários muito úteis

Finalmente, onde você coloca a chamada da API de login?

É exatamente para isso que serve o criador de ações dispatch => {} . Efeitos colaterais!

É apenas mais um criador de ação. Junte-o a outras ações:

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return { error, type: LOGGED_FAILED };
}

export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return { user, type: LOGIN_ATTEMPT };
}

export function login(userData) {
  return dispatch =>
    fetch('http://localhost/login', {
      method: 'post',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: userData.email,
        password: userData.password,
      }),
    })
    .then(response => {
      if (response.status >= 200 && response.status < 300) {
        console.log(response);
        dispatch(loginSuccess(response));
      } else {
        const error = new Error(response.statusText);
        error.response = response;
        dispatch(loginError(error));
        throw error;
      }
    })
    .catch(error => { console.log('request failed', error); });
}

Em seus componentes, basta chamar

this.props.login(); // assuming it was bound with bindActionCreators before

// --or--

this.props.dispatch(login()); // assuming you only have dispatch from Connector

Todos 115 comentários

Isso é quase correto, mas o problema é _você não pode simplesmente chamar criadores de ação pura e esperar que as coisas aconteçam_. Não se esqueça de que seus criadores de ação são apenas funções que especificam _o que_ precisa ser despachado.

// CounterActions
export function increment() {
  return { type: INCREMENT }
}


// Some other file
import { increment } from './CounterActions';
store.dispatch(increment()); <--- This will work assuming you have a reference to the Store
increment(); <---- THIS DOESN'T DO ANYTHING! You're just calling your function and ignoring result.


// SomeComponent
import { increment } from './CounterActions';

@connect(state => state.counter) // will inject store's dispatch into props
class SomeComponent {
  render() {
    return <OtherComponent {...bindActionCreators(CounterActions, this.props.dispatch)} />
  }
}


// OtherComponent
class OtherComponent {
  handleClick() {
    // this is correct:
    this.props.increment(); // <---- it was bound to dispatch in SomeComponent

    // THIS DOESN'T DO ANYTHING:
    CounterActions.increment(); // <---- it's just your functions as is! it's not bound to the Store.
  }
}

Agora, vamos ao seu exemplo. A primeira coisa que quero esclarecer é que você não precisa do formulário dispatch => {} assíncrono se apenas despachar uma única ação de forma síncrona e não tiver efeitos colaterais (verdadeiro para loginError e loginRequest ).

Este:

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return dispatch => {
    dispatch({ error, type: LOGGED_FAILED });
  };
}

export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    // router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return dispatch => {
    dispatch({ user, type: LOGIN_ATTEMPT });
  };
}

pode ser simplificado como

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return { error, type: LOGGED_FAILED };
}

// You'll have a side effect here so (dispatch) => {} form is a good idea
export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    // router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return { user, type: LOGIN_ATTEMPT };
}

Em segundo lugar, seus redutores devem ser _funções puras que não têm efeitos colaterais_. Não tente chamar sua API de um redutor.

Este:

const initialState = new Immutable.Map({
  email: '',
  password: '',
  isLoggingIn: false,
  isLoggedIn: false,
  error: null
}).asMutable(); // <---------------------- why asMutable?

export default function user(state = initialState, action) {
  switch (action.type) {
    case LOGIN_ATTEMPT:
      console.log(action.user);
      LoginApiCall.login(action.user); // <------------------------ no side effects in reducers! :-(
      return state;
    case LOGGED_FAILED:
      console.log('failed from reducer');
      return state;
    case LOGGED_SUCCESSFULLY:
      console.log('success', action);
      console.log('success from reducer');
      break;
    default:
      return state;
  }

provavelmente deve se parecer mais com

const initialState = new Immutable.Map({
  email: '',
  password: '',
  isLoggingIn: false,
  isLoggedIn: false,
  error: null
});

export default function user(state = initialState, action) {
  switch (action.type) {
    case LOGIN_ATTEMPT:
      return state.merge({
        isLoggingIn: true,
        isLoggedIn: false,
        email: action.email,
        password: action.password // Note you shouldn't store user's password in real apps
      });
    case LOGGED_FAILED:
      return state.merge({
        error: action.error,
        isLoggingIn: false,
        isLoggedIn: false
      });
    case LOGGED_SUCCESSFULLY:
      return state.merge({
        error: null,
        isLoggingIn: false,
        isLoggedIn: true
      });
      break;
    default:
      return state;
  }

Finalmente, onde você coloca a chamada da API de login?

É exatamente para isso que serve o criador de ações dispatch => {} . Efeitos colaterais!

É apenas mais um criador de ação. Junte-o a outras ações:

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return { error, type: LOGGED_FAILED };
}

export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return { user, type: LOGIN_ATTEMPT };
}

export function login(userData) {
  return dispatch =>
    fetch('http://localhost/login', {
      method: 'post',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: userData.email,
        password: userData.password,
      }),
    })
    .then(response => {
      if (response.status >= 200 && response.status < 300) {
        console.log(response);
        dispatch(loginSuccess(response));
      } else {
        const error = new Error(response.statusText);
        error.response = response;
        dispatch(loginError(error));
        throw error;
      }
    })
    .catch(error => { console.log('request failed', error); });
}

Em seus componentes, basta chamar

this.props.login(); // assuming it was bound with bindActionCreators before

// --or--

this.props.dispatch(login()); // assuming you only have dispatch from Connector

Finalmente, se você costuma escrever grandes criadores de ação como este, é uma boa ideia escrever um middleware personalizado para chamadas assíncronas compatíveis com promessas, seja o que for que você use para assíncronas.

A técnica que descrevi acima (criadores de ação com dispatch => {} assinatura) está agora incluída no Redux, mas no 1.0 estará disponível apenas como um pacote separado chamado redux-thunk . Enquanto você está nisso, você também pode verificar redux-promessa-middleware ou redux-promessa .

@gaearon : clap: essa foi uma explicação incrível! ;) Deve definitivamente fazer parte dos documentos.

@gaearon explicação incrível! :troféu:

@gaearon E se você precisar executar alguma lógica para determinar qual chamada de API deve ser chamada? Não é a lógica de domínio que deveria estar em um lugar (redutores)? Manter isso em ação, os criadores me soa como quebrar uma única fonte de verdade. Além disso, principalmente você precisa parametrizar a chamada da API por algum valor do estado do aplicativo. Você provavelmente também deseja testar a lógica de alguma forma. Achamos que fazer chamadas API em Redutores (fluxo atômico) é muito útil e testável em projetos de grande escala.

Achamos que fazer chamadas de API em Redutores (fluxo atômico) é muito útil e testável em projetos de grande escala.

Isso quebra o recorde / repetição. Funções com efeitos colaterais são mais difíceis de testar do que funções puras por definição. Você pode usar o Redux assim, mas é totalmente contra o seu design. :-)

Manter isso em ação, os criadores me soa como quebrar uma única fonte de verdade.

“Fonte única da verdade” significa que os dados residem em um só lugar e não têm cópias independentes. Isso não significa que “toda a lógica do domínio deve estar em um só lugar”.

E se você precisar executar alguma lógica para determinar qual chamada de API deve ser chamada? Não é a lógica de domínio que deveria estar em um lugar (redutores)?

Os redutores especificam como o estado é transformado por ações. Eles não devem se preocupar com a origem dessas ações. Eles podem vir de componentes, criadores de ações, sessões gravadas em série, etc. Essa é a beleza do conceito de usar ações.

Qualquer chamada de API (ou lógica que determina qual API é chamada) ocorre antes dos redutores. É por isso que Redux suporta middleware. O middleware Thunk descrito acima permite que você use condicionais e até mesmo leia a partir do estado:

// Simple pure action creator
function loginFailure(error) {
  return { type: LOGIN_FAILURE, error };
}

// Another simple pure action creator
function loginSuccess(userId) {
  return { type: LOGIN_SUCCESS, userId };
}

// Another simple pure action creator
function logout() {
  return { type: LOGOUT };  
}


// Side effect: uses thunk middleware
function login() {
  return dispatch => {
    MyAwesomeAPI.performLogin().then(
      json => dispatch(loginSuccess(json.userId)),
      error => dispatch(loginFailure(error))
    )
  };
}


// Side effect *and* reads state
function toggleLoginState() {
  return (dispatch, getState) => {
    const { isLoggedIn } = getState().loginState;
    if (isLoggedIn) {
      dispatch(login());
    } else {
      dispatch(logout());
    }
  };
}

// Component
this.props.toggleLoginState(); // Doesn't care how it happens

Os criadores de ação e middleware são _designados_ para realizar efeitos colaterais e se complementarem.
Redutores são apenas máquinas de estado e não têm nada a ver com assíncrono.

Os redutores especificam como o estado é transformado por ações.

Este é realmente um ponto muito válido, obrigado.

Vou reabrir para a posteridade até que uma versão disso esteja nos documentos.

: +1:

Eu diria que, embora os redutores sejam máquinas de estado, os criadores de ações em geral também são (mas implicitamente).

Digamos que o usuário clicou no botão "enviar" duas vezes. Na primeira vez, você enviará uma solicitação HTTP ao servidor (no criador da ação) e mudará o estado para "submetendo" (no redutor). Da segunda vez, você não quer fazer isso.

No modelo atual, seu criador de ação terá que escolher quais ações despachar, dependendo do estado atual. Se não estiver "submetendo" no momento - envie uma solicitação HTTP e despache uma ação, caso contrário - não faça nada ou até despache uma ação diferente (como um aviso).

Portanto, seu fluxo de controle é efetivamente dividido em duas máquinas de estado e uma delas é implícita e cheia de efeitos colaterais.

Eu diria que essa abordagem do Flux lida muito bem com a maioria dos casos de uso de um aplicativo da web típico. Mas quando você tem uma lógica de controle complexa, pode se tornar problemático. Todos os exemplos existentes simplesmente não têm lógica de controle complexa, portanto, esse problema está um tanto oculto.

https://github.com/Day8/re-frame é um exemplo de abordagem diferente em que eles têm um único manipulador de eventos que atua como o único FSM.

Mas então os redutores têm efeitos colaterais. Portanto, é interessante como eles lidam com o recurso de "repetição" em tais casos - perguntado a eles em https://github.com/Day8/re-frame/issues/86

Em geral, acho que este é um problema real e não é uma surpresa que apareça repetidamente. É interessante ver quais soluções surgirão eventualmente.

Mantenha-nos informados sobre o re-frame e os efeitos colaterais!

No meu livro, existem 3 tipos de estado em qualquer aplicativo web Redux decente.

1) Exibir componentes (React this.state). Isso deve ser evitado, mas às vezes eu só quero implementar algum comportamento que pode ser reutilizado apenas reutilizando um componente independentemente do Redux.

2) Estado compartilhado do aplicativo, ou seja, Redux.state. É o estado que uma camada de visualização está usando para apresentar um aplicativo a um usuário e os componentes de visualização o utilizam para "comunicar-se" entre si. E este estado pode ser usado para "comunicar" entre ActionCreators, Middlewares, visualizações, pois suas decisões podem depender deste estado. Portanto, o "compartilhado" é importante. No entanto, não precisa ser um estado de aplicativo completo. Ou acho que é impraticável implementar tudo nesse tipo de estado (em termos de ações).

3) Estado que é mantido por outros módulos / libs / serviços complexos de efeito colateral. Eu os escreveria para lidar com os cenários que vladar está descrevendo. Ou react-router é outro exemplo. Você pode tratá-lo como uma caixa preta que tem seu próprio estado ou pode decidir elevar parte do estado do roteador de reação para o estado compartilhado do aplicativo (Redux.state). Se eu fosse escrever um módulo HTTP complexo que pudesse lidar habilmente com todas as minhas solicitações e seus tempos, geralmente não estou interessado em detalhes de tempo de solicitação em Redux.state. Eu só usaria este módulo da ActionCreators / Middlewares, possivelmente junto com Redux.state para obter um novo Redux.state.

Se eu gostaria de escrever o componente de visualização mostrando os tempos de minha solicitação, eu teria que obter esse estado para Redux.state.

@vladar É o que você quis dizer que AC / MW são máquinas de estado implícitas? Isso porque eles não mantêm o estado por si só, mas ainda dependem do estado mantido em algum outro lugar e podem definir a lógica de controle como esses estados evoluem no tempo? Para alguns casos, acho que eles ainda poderiam ser implementados como encerramentos e manter seu próprio estado, tornando-se máquinas de estado explícitas?

Como alternativa, eu poderia chamar Redux.state de "estado público", enquanto outros estados são privados. Como projetar meu aplicativo é um ato de decidir o que manter como estado privado e o que como estado público. Parece-me uma boa oportunidade para encapsulamento, portanto, não vejo como problemático ter estados divididos em lugares diferentes, desde que não se torne um inferno como eles afetam um ao outro.

@vladar

Mas então os redutores têm efeitos colaterais. Portanto, é interessante como eles lidam com o recurso de "repetição" em tais casos

Para obter uma reprodução fácil, o código deve ser determinístico. É o que o Redux consegue ao exigir redutores puros. Em um efeito, Redux.state divide o aplicativo em partes não determinísticas (assíncronas) e determinísticas. Você pode reproduzir o comportamento acima de Redux.state, supondo que você não faça loucuras nos componentes da visualização. O primeiro objetivo importante para atingir o código determinístico é remover o código assíncrono e convertê-lo em código síncrono por meio do log de ação. É o que a arquitetura Flux faz em geral. Mas, em geral, não é suficiente e outro código com efeito colateral ou mutante ainda pode interromper o processamento determinístico.

Alcançar a capacidade de repetição com redutores de efeito colateral é muito difícil, até impossível, ou funcionará apenas parcialmente com muitos casos de canto com provavelmente um pequeno efeito prático.

Para obter uma viagem de tempo fácil, armazenamos instantâneos do estado do aplicativo em vez de repetir as ações (o que é sempre difícil) como feito no Redux.

Para obter uma viagem de tempo fácil, armazenamos instantâneos do estado do aplicativo em vez de repetir as ações (o que é sempre difícil) como feito no Redux.

Redux DevTools armazena instantâneos e ações. Se você não tem ações, não pode recarregar os redutores a quente. É o recurso mais poderoso do fluxo de trabalho que o DevTools permite.

A viagem no tempo funciona bem com criadores de ações impuras porque _ ela acontece no nível das ações brutas despachadas (finais )_. Esses são objetos simples, portanto, são completamente determinísticos. Sim, você não pode “reverter” uma chamada de API, mas não vejo problema nisso. Você pode reverter as ações brutas emitidas por ele.

Isso porque eles não mantêm o estado por si só, mas ainda dependem do estado mantido em algum outro lugar e podem definir a lógica de controle como esses estados evoluem no tempo?

Sim, é exatamente o que quero dizer. Mas você também pode acabar com a duplicação de sua lógica de controle. Algum código para demonstrar o problema (do meu exemplo acima com clique duplo de envio).

function submit(data) {
  return (dispatch, getState) => {
    const { isSubmitting } = getState().isSubmitting;
    if (!isSubmitting) {
      dispatch(started(data));

      MyAPI.submit(data).then(
        json => dispatch(success(json)),
        error => dispatch(failure(error))
      )
    }
  }
}

function started(data) {
   return { type: SUBMIT_STARTED, data };
}

function success(result) {
  return { type: SUBMIT_SUCCESS, result };
}

function failure(error) {
  return { type: SUBMIT_FAILURE, error };
}

E então você tem seu redutor para verificar o estado "isSubmitting" novamente

const initialState = new Immutable.Map({
  isSubmitting: false,
  pendingData: null,
  error: null
});

export default function form(state = initialState, action) {
  switch (action.type) {
    case SUBMIT_STARTED:
      if (!state.isSubmitting) {
        return state.merge({
          isSubmitting: true,
          pendingData: action.data
        });
      } else {
        return state;
      }
  }
}

Então, você acaba tendo as mesmas verificações lógicas em dois lugares. Obviamente, a duplicação neste exemplo é mínima, mas para cenários mais complexos, pode não ficar bonita.

Acho que no último exemplo o cheque _não_ deve estar dentro do redutor. Caso contrário, ela pode rapidamente cair na inconsistência (por exemplo, a ação é despachada por engano, mas é ignorada, mas então a ação “sucesso” também é despachada, mas era inesperada, e o estado pode ser mesclado incorretamente).

(Desculpe se era esse o seu ponto ;-)

Concordo com gaeron porque! IsSubmitting já está codificado no fato de que a ação SUBMIT_STARTED foi despachada. Quando uma ação é o resultado de alguma lógica, então essa lógica não deve ser replicada no redutor. Então, a responsabilidade do redutor é apenas que quando ele recebe SUBMIT_STARTED, ele não pensa e apenas atualiza o estado com a carga de ação porque outra pessoa assumiu a responsabilidade de decidir esse SUBMIT_STARTED.

Deve-se sempre objetivar que haja uma única coisa que assume a responsabilidade por uma única decisão.

O redutor pode então construir mais sobre o fato SUBMIT_STARTED e estendê-lo com lógica adicional, mas essa lógica deve ser diferente. Fe:

case SUBMIT_STARTED:
  if (goodMood) loading...
  else nothing_happens

Posso imaginar que às vezes pode ser complicado decidir quem deve ser responsável por quê. ActionCreator, Middleware, módulo complexo autônomo, redutor?

Meu objetivo é tomar o máximo de decisões possível em redutores, mantendo-os puros, porque eles podem ser recarregados a quente. Se as decisões são necessárias para efeitos colaterais / assíncronos, eles vão para AC / MW / algum lugar_algo. Também pode depender de como as melhores práticas evoluem em relação à reutilização de código, ou seja, redutores versus middleware.

O AC não pode alterar o estado. Está enviando diretamente. Portanto, se sua lógica depende dele, o AC precisa de garantias de que state.isSubmitting estará em sincronia com sua lógica. Se a lógica AC quiser definir state.isSubmitted como true com novos dados e as leituras estão de volta, ele espera que seja definido como tal. Não deveria, por outra lógica, potencialmente alterar essa premissa. ACTION é a própria primitiva de sincronização. Basicamente, as ações definem o protocolo e os protocolos devem ser bem e claramente definidos.

Mas acho que sei o que você está tentando dizer. Redux.state é um estado compartilhado. O estado compartilhado é sempre complicado. É fácil escrever código em redutores que mudam de estado de uma maneira inesperada pela lógica AC. Portanto, posso imaginar que seja difícil manter a lógica entre o CA e os redutores em sincronia em alguns cenários muito complexos. Isso pode levar a bugs que podem ser difíceis de seguir e depurar. Acredito que para tais cenários é sempre possível reduzir a lógica para AC / Middleware e fazer os redutores agirem apenas estupidamente com base em ações bem definidas com pouca ou nenhuma lógica.

Alguém sempre pode adicionar um novo redutor que quebrará algum antigo AC dependente. Deve-se pensar duas vezes antes de tornar o AC dependente de Redux.state.

Eu tenho um criador de ação que requer um token de autenticação retornado por uma solicitação de API com credenciais de usuário. Há um limite de tempo para esse token e ele precisa ser atualizado e gerenciado por algo. O que você está propondo parece indicar que este estado não pertence a Redux.state?

Para onde deve ir? Conceitualmente, isso é passado para o construtor da loja Redux como um estado externo imutável?

Portanto, posso imaginar que seja difícil manter a lógica entre o CA e os redutores em sincronia em alguns cenários muito complexos.

Sim. Não estou dizendo que seja um empecilho, mas pode se tornar inconsistente. O objetivo da máquina de estado é reagir aos eventos - independentemente da ordem em que aparecem - e permanecer consistente.

Se você confia na ordem adequada de eventos do AC - você apenas move implicitamente partes de seu mecanismo de estado para AC e o acopla com o redutor.

Talvez seja apenas uma questão de preferência, mas me sinto muito melhor quando a loja / redutor sempre atua de forma consistente e não depende de algo externo para manter a consistência em outro lugar.

Deve-se pensar duas vezes antes de tornar o AC dependente de Redux.state.

Acho que você não pode evitá-lo em casos complexos (sem mover alguns efeitos colaterais para redutores / manipuladores de eventos).

Parece que é uma troca: "descartar o recurso hot-reloading ou replay" versus "gerenciar o estado em um só lugar". No caso posterior, você pode escrever FSMs ou Statecharts reais como propõe o re-frame; no caso anterior, é mais lógica de controle ad-hoc como temos no Flux agora.

Eu li (muito rápido) através do Re-frame e é o mesmo tipo de coisa que Redux com algumas diferenças de detalhes de implementação. Os componentes de visualização do Re-frame são baseados em observáveis, Eventos são Ações, Re-frame despacha eventos (ações) diretamente como objetos (eles são construídos em um componente de visualização) sem usar criadores, Manipuladores de Eventos são puros e são a mesma coisa que Redux redutores.

Manipulação de efeitos colaterais não é muito discutido ou eu perdi isso, mas o que estou assumindo é que eles são tratados em Middlewares. E aí está a principal diferença. Middlewares envolvem manipuladores de eventos (redutores), compõem com eles de fora, enquanto Redux Middlewares não envolvem redutores. Em outras palavras, o Re-frame compõe os efeitos colaterais diretamente nos redutores puros, enquanto o Redux os mantém separados.

Isso significa que não se pode gravar eventos Re-frame e reproduzi-los facilmente.

Quando estava falando sobre possíveis inconsistências, considerei o estado compartilhado como a causa raiz e acredito que o Re-frame tem o mesmo risco.

A diferença está em acoplar Middlewares com redutores. Quando os Middlewares terminam no Re-frame, eles passam o resultado diretamente para os manipuladores de eventos (redutores), este resultado não é registrado como evento / ação, portanto, não pode ser reproduzido. Eu diria que o Redux apenas coloca ganchos no meio em vez de na forma de ações, portanto, acredito que posso tecnicamente conseguir o mesmo que o Re-frame, com mais flexibilidade às custas de mais digitação (criação de ações) e provavelmente mais espaço para fazê-lo errado.

No final, não há nada que Redux me impeça de implementar exatamente a mesma abordagem do Re-frame. Posso criar qualquer função que tenha redutor como parâmetro, fazer algum processamento nela e então chamar diretamente a função de redutor com o resultado e chamá-la de Redutor de Middlewares ou qualquer outra coisa. É apenas sobre se eu quiser torná-lo um pouco mais fácil à custa de perder DevTools e mais acoplamento.

Ainda tenho que pensar mais se há uma vantagem significativa na abordagem Re-frame acima de alguma simplificação (quantidade decrescente de ações). Estou aberto para provar que estou errado, como já disse, li rapidamente.

Na verdade, eu estava um pouco incorreto. A diferença não está nas implementações de Redux vs Re-frame, mas realmente em "onde você coloca seus efeitos colaterais".

Se você fizer isso em manipuladores de eventos (redutores) e não permitir que os criadores de ação afetem seu fluxo de controle, então, tecnicamente, não há diferença - você também pode usar fsm / statecharts no Redux e ter seu fluxo de controle em um só lugar.

Mas, na realidade - há uma diferença. @gaearon explicou muito bem: Redux espera que seus redutores sejam puros, caso contrário, o recurso de repetição quebra. Portanto, você não deve fazer isso (colocar efeitos colaterais nos redutores), pois é contra o design Redux.

Re-frame também afirma que os manipuladores de eventos são puros, mas mesmo no Leiame eles mencionam que as solicitações HTTP são iniciadas em manipuladores de eventos. Portanto, isso não está claro para mim, mas parece que as expectativas deles são diferentes: você pode colocar seus efeitos colaterais em manipuladores de eventos.

Então é interessante como eles abordam o replay (e foi isso que perguntei em https://github.com/Day8/re-frame/issues/86).

Presumo que a única maneira de fazer isso quando seus manipuladores de eventos podem ter efeitos colaterais é o que @ tomkis1 mencionou - estado de gravação retornado por todos os manipuladores de eventos (não execute redutores novamente em todos os eventos).

O recarregamento a quente ainda pode funcionar, exceto que não pode afetar eventos "passados" - apenas produzir algum novo ramo de estado no tempo.

Portanto, a diferença no design é provavelmente sutil, mas para mim é importante.

Acho que eles fazem solicitações HTTP compondo manipuladores de eventos puros com middlewares de efeito colateral. Esta composição retorna um novo manipulador de eventos que não é mais puro, mas o interno permanece puro. Não acho que eles sugiram a construção de manipuladores de eventos que combinem mutação app-db (estado) e efeitos colaterais. Isso torna o código mais testável.

Se você registrar o resultado de um manipulador de eventos, não poderá torná-lo recarregável a quente, o que significa que pode alterar seu código imediatamente. Você teria que registrar o resultado do middleware e isso seria exatamente o mesmo que o Redux faz - registrar este resultado como uma ação e passá-lo para o redutor (ouvir).

Acho que poderia dizer que os middlewares Re-frame são proativos, enquanto o Redux permite reatividade (qualquer redutor ad-hoc pode ouvir um resultado de middleware / ac). Uma coisa que estou começando a perceber é que AC, como usado principalmente em Flux e middlewares, são a mesma coisa que controladores.

Pelo que entendi, @ tomkis1 sugere

Ok, percebendo que redutores / manipuladores de eventos retornam um novo estado completo, portanto, você disse o mesmo.

@merk , tentarei escrever minha reflexão sobre isso provavelmente na segunda-feira, pois provavelmente não chegarei aqui durante um fim de semana.

Os documentos de re-frame recomendam falar com o servidor em manipuladores de eventos: https://github.com/Day8/re-frame#talking -to-a-server

Portanto, é aqui que diverge do Redux. E essa diferença é mais profunda do que parece à primeira vista.

Hm, não consigo ver a diferença do Redux:

Os manipuladores de eventos iniciais devem organizar para que os manipuladores em caso

Substitua o evento por Ação. Redux apenas tem um nome especial para um manipulador de eventos puro - um redutor. Acho que estou perdendo alguma coisa ou meu cérebro está mal neste fim de semana.

Eu pude sentir uma diferença mais significativa de como os eventos são executados - enfileirados e assíncronos. Acho que o Redux executa sincronização de ações e ação em etapas de bloqueio por ação para garantir a consistência do estado. Isso pode afetar ainda mais a capacidade de reprodução, pois não tenho certeza se apenas salvar eventos e reproduzi-los novamente resultará no mesmo estado final. Desconsiderando isso, não posso simplesmente fazer isso porque não sei quais eventos desencadeiam efeitos colaterais, ao contrário do Redux, onde ações sempre desencadeiam código puro.

@vladap Para resumir a diferença como eu vejo:

1

  • no Redux você inicia a solicitação do servidor no criador da ação (veja a resposta de @gaearon à questão original deste problema); seus redutores devem ser puros
  • no Re-frame você faz isso no manipulador de eventos (redutor); seus manipuladores de eventos (redutores) podem ter efeitos colaterais

2

  • No primeiro caso, seu fluxo de controle é dividido entre CA e redutor.
  • No segundo caso, todo o seu fluxo de controle está em um único lugar - manipulador de eventos (redutor).

3 -

  • Se seu fluxo de controle for dividido - você precisa de coordenação entre AC e redutor (mesmo que implícita): AC lê o estado produzido pelo redutor; o redutor assume a ordenação adequada dos eventos do AC. Tecnicamente, você introduz o acoplamento de CA e redutor, porque mudanças no redutor também podem afetar CA e vice-versa.
  • Se seu fluxo de controle está em um só lugar - você não precisa de coordenação adicional + você pode usar ferramentas como FSMs ou Statecharts em seus manipuladores / redutores de eventos


    1. A maneira Redux de aplicar redutores puros e ter efeitos colaterais movidos para ACs permite recarregar a quente e reproduzir

  • Reestruturar a forma de ter efeitos colaterais em manipuladores de eventos (redutores) fecha a porta para a repetição, como é feito no Redux (reavaliando redutores), mas outras opções (provavelmente menos poderosas) ainda são possíveis - como @ tomkis1 mencionou

Quanto à diferença na experiência de desenvolvimento - ela só aparece quando você tem muitas coisas assíncronas (timers, solicitações http paralelas, sockets da web, etc). Para o material de sincronização, é tudo igual (uma vez que não há fluxo de controle em ACs).

Acho que preciso preparar algum exemplo do mundo real de um cenário tão complexo para deixar meu ponto mais claro.

Eu posso te deixar louco, sinto muito por isso, mas não concordamos em uma coisa básica. Talvez eu deva ler o código de exemplos de reformulação, aprenderei Clojure um dia. Talvez você tenha, portanto, você sabe mais. Obrigado pelo seu esforço.

1

no Redux você inicia a solicitação do servidor no criador da ação (veja a resposta de @gaearon à questão original deste problema); seus redutores devem ser puros
no Re-frame você faz isso no manipulador de eventos (redutor); seus manipuladores de eventos (redutores) podem ter efeitos colaterais

Doc que fiz em negrito diz explicitamente que devo iniciar a chamada webapi em um manipulador de eventos e, em seguida, despachar o evento em seu sucesso. Não entendo como posso lidar com esse evento despachado no mesmo manipulador de eventos. Este evento de sucesso é despachado para o roteador (eq. Do despachante, eu acho) e eu preciso de outro manipulador de eventos para lidar com isso. Este segundo manipulador de eventos é puro e é equivalente ao redutor Redux, o primeiro é equivalente ao ActionCreator. Para mim é a mesma coisa ou eu claramente perco alguns insights importantes.

Quanto à diferença na experiência de desenvolvimento - ela só aparece quando você tem muitas coisas assíncronas (timers, solicitações http paralelas, sockets da web, etc). Para o material de sincronização, é tudo igual (uma vez que não há fluxo de controle em ACs).

Nós não concordamos com este. Coisas assíncronas na verdade não importam, você não pode reproduzi-las no Redux, portanto, você não tem experiência de desenvolvimento aqui. A diferença aparece quando você tem muitos redutores puros complexos. No Redux, você pode pegar o estado A, alguma sequência de ação, redutor, reproduzi-la e obter o estado B. Você mantém tudo o mesmo, mas muda o código para o redutor e recarrega-o a quente, repete a mesma sequência de ação do estado A e você obtenha o estado C, percebendo imediatamente o impacto da alteração do código. Você não pode fazer isso com a abordagem @ tomkis1 .

Você pode até construir cenários de teste em cima dele. Salve algum estado inicial, salve alguma sequência de ação, reduza a sequência de ação para o estado A, obtenha o estado B e afirme-o. Em seguida, faça alterações no código que você espera que resultem no mesmo estado B, repita o cenário de teste para afirmar que é verdadeiro e que suas alterações não interromperam seu aplicativo. Não digo que seja a melhor abordagem de teste, mas pode ser feito.

Na verdade, eu ficaria muito surpreso se os clojuristas que considero praticantes quase tão obstinados de programação funcional quanto Haskellers recomendassem misturar efeitos colaterais com código que poderia ser puro pelo menos sem algum nível de composição.

Avaliação / reatividade preguiçosa é a técnica básica para mover o código de efeito colateral o mais longe possível do código puro. A avaliação preguiçosa é como Haskell finalmente aplicou o conceito impraticável de pureza funcional para obter um programa que faz algo prático, porque um programa completamente puro não pode fazer muito. Usando composição monádica e outros, todo o programa é criado como fluxo de dados e para manter a última etapa no pipeline puro e nunca realmente chamar efeitos colaterais em seu código, o programa retorna a descrição do que deve ser feito. Ele é passado para o tempo de execução que o executa preguiçosamente - você não aciona a execução por uma chamada imperativa. Pelo menos é como eu entendo a programação funcional do ponto de vista do pássaro. Não temos esse tempo de execução, nós o simulamos com ActionCreators e reatividade. Isenção de responsabilidade, não tome isso garantido, é o que eu entendi por não ter lido tanto sobre FP para ser qualquer tipo de autoridade aqui. Posso estar desligado, mas realmente acredito que entendi o ponto.

Não entendo como posso lidar com esse evento despachado no mesmo manipulador de eventos.

Não estou a dizer isso. Por "efeitos colaterais", quero dizer ter uma solicitação HTTP iniciada no manipulador de eventos (redutor). Executar IO também é um efeito colateral, mesmo que não afete o retorno de sua função. Não quero dizer "efeitos colaterais" em termos de modificação de estado diretamente em manipuladores de sucesso / erro.

Este segundo manipulador de eventos é puro e é equivalente ao redutor Redux, o primeiro é equivalente ao ActionCreator.

Não acho que o primeiro seja equivalente ao Action Creator. Do contrário, por que você precisa que Action Creators faça isso? Se você pode fazer o mesmo no redutor?

Você não pode fazer isso com a abordagem @ tomkis1 .

Aceita. E é isso que eu quis dizer quando escrevi "outras opções (provavelmente menos poderosas)".

Não acho que o primeiro seja equivalente ao Action Creator. Do contrário, por que você precisa que Action Creators faça isso? Se você pode fazer o mesmo no redutor?

ActionCreators são usados ​​para isolar efeitos colaterais de aplicativos puros (que podem ser reproduzidos). Como o Redux foi projetado, você não pode (não deve) executar efeitos colaterais em redutores. Você precisa de outra construção onde possa colocar código de efeito colateral. No Redux, essas construções são AC ou middlewares. A única diferença que vejo no re-frame é o que os middlewares fazem e que o re-frame não tem middlewares para transformar eventos (ações) antes que os manipuladores sejam acionados.

Em vez de AC / middlewares, na verdade, qualquer coisa pode ser usada - módulo utilitário, serviços (como serviços Angular), você pode construí-lo como uma biblioteca separada que pode ser conectada por AC ou middleware e traduzir sua API em ações. Se você me perguntar, o AC não é o lugar certo para colocar esse código. O AC deve ser apenas criadores / fábricas simples construindo objetos de ação a partir de argumentos. Se eu quiser ser um purista, seria melhor colocar código de efeito colateral estritamente em middlewares. ActionCreator é um termo ruim para uma espécie de responsabilidade do controlador. Mas se esse código for apenas uma chamada de webapi de uma linha, há uma tendência de simplesmente colocá-lo dentro do AC e para um aplicativo simples, pode funcionar.

AC, middlewares, libs de terceiros formam uma camada que gosto de chamar de camada de serviço. Os WebApiUtils originais do Facebook fariam parte dessa camada, da mesma forma que se você usasse react-router e ouvisse suas mudanças e as traduzisse em ações para atualizar Redux.state, Relay pode ser usado como camada de serviço (do que usar Redux para app / view Estado). Como a maioria das bibliotecas de terceiros são atualmente programadas, elas não funcionam bem com o replay. Obviamente, não quero reescrever tudo do zero para que assim seja. Gosto de pensar a respeito de que essas bibliotecas, serviços, utilitários etc. estendem o ambiente do navegador, formando juntos uma plataforma sobre a qual posso construir meu aplicativo reproduzível. Esta plataforma está cheia de efeitos colaterais, na verdade é o lugar para onde eu intencionalmente movo os efeitos colaterais. Eu traduzo essa API de plataforma em ações e meu aplicativo se conecta indiretamente a essa plataforma ouvindo esses objetos de ação (tentando simular a abordagem de Haskell que tentei descrever em um post acima). Este é um tipo de hack na programação funcional para ainda alcançar pureza (possivelmente não em um sentido acadêmico absoluto), mas desencadear efeitos colaterais.

Há outra coisa que me confunde, mas posso entendê-lo mal. Acho que você disse que, para uma lógica complexa, é benéfico ter tudo em um só lugar. Acho bem o contrário.

Todo esse negócio de ações é semelhante em conceito aos eventos DOM. Eu realmente não gostaria de misturar meu código de negócios com um código de como o navegador fe detecta o evento mousemove e tenho certeza de que você também não. Felizmente, estou acostumado a trabalhar em cima de addListener ('mousemove', ...) porque essas responsabilidades são bem separadas. O objetivo é conseguir essa boa separação de interesses também para minha lógica de negócios. ActionCreators, middlewares são ferramentas para isso.

Imagine que eu estaria escrevendo um aplicativo em um webapi obsoleto que não mapeia bem as minhas necessidades de negócios. Para obter os dados necessários, eu teria que chamar alguns terminais, usar seu resultado para criar próximas chamadas e, em seguida, mesclar todos os resultados em um formato canônico que eu usaria em Redux.state. Essa lógica não vazaria para meus redutores, de forma que eu não tivesse que transformar dados em meus redutores repetidamente. Seria em middlewares. Efetivamente, eu isolaria a api obsoleta confusa e desenvolveria meu aplicativo de negócios, pois seria escrito em uma boa api. Uma vez feito isso, talvez eu conseguisse uma mudança para reconstruir nosso webapi e então apenas reescreveria meu middleware.

Portanto, ter tudo em um só lugar parece fácil para um aplicativo fácil. Mas, ao contrário das coisas complexas, vejo uma boa separação de interesses benéfica. Mas talvez eu não tenha entendido seu ponto ou caso de uso ao qual você se refere.

Na verdade, provavelmente eu moveria o máximo de transformação de dados para sua forma canônica para redutores e usaria composição de redutores, pois isso me daria reprodução e testabilidade para este código que geralmente é puro.

Não entendo como posso lidar com esse evento despachado no mesmo manipulador de eventos.
Não estou a dizer isso.

Achei que você gostaria de misturar chamadas webapi com lógica de redutores para tê-lo em um só lugar. O que estou dizendo é que você não faz isso no re-frame porque eles sugerem despachar o evento com sucesso.

Pode haver situações em que webapi -> muita lógica -> webapi -> muita lógica -> ... -> resultado para redutor, toda a cadeia acionada por um único clique. Em tais casos, a maior parte da lógica provavelmente estaria em AC, eu não dividiria até que todos os efeitos colaterais terminassem. É a isso que você se refere?

O melhor que posso fazer aqui é mover o máximo de lógica para funções puras para testabilidade, mas elas seriam chamadas no escopo AC.

Acho que não gosto da possibilidade de fazer isso como uma série de manipuladores de eventos conectados indiretamente. Seria uma promessa ou uma cadeia observável.

Reduxers mais experientes podem ter opiniões diferentes sobre esse pensamento ( @gaearon , @johanneslumpe , @acdlite , @emmenko ...)

@vladap Acho que essa conversa se afastou muito do assunto desta edição. Você já mencionou o ponto principal dos meus comentários aqui:

Portanto, posso imaginar que seja difícil manter a lógica entre o CA e os redutores em sincronia em alguns cenários muito complexos.

Exemplo rápido:

Situação 1: você tem uma lista dos 10 principais autores de blog em algum lugar da página. Agora você excluiu uma categoria de postagens no blog. Após a exclusão bem-sucedida, você precisa atualizar esta lista de autores do servidor para torná-la atualizada.

Situação 2: você está em uma página diferente. Não tem lista de autores, mas tem uma lista dos 10 comentários principais. Você também pode excluir algumas categorias de postagens de blog e terá que atualizar a lista de comentários na exclusão bem-sucedida.

Então, como lidamos com isso no nível do mecanismo de estado? (também poderíamos usar ganchos React para obter ajuda, mas um bom mecanismo de estado deve ser capaz de permanecer consistente por conta própria)

Opções:
A) colocamos essa lógica em AC. Então o AC tocará em CategoryApi (para excluir), fará a leitura de userList state e commentList state (para verificar quais listas estão presentes no estado agora), fale com UserListApi e / ou CommentListApi (para atualizar as listas) + despache TOP_AUTHORS_UPDATED e / ou TOP_COMMENTS_UPDATED . Portanto, basicamente atingirá 3 domínios diferentes.

B) nós o colocamos nos manipuladores de eventos userList e commentList . Esses manipuladores ouviriam o evento DELETE_CATEGORY_SUCCESS , em seguida, chamariam seu serviço de API e, por sua vez, enviariam o evento TOP_AUTHORS_UPDATED / TOP_COMMENTS_UPDATED . Portanto, cada manipulador toca apenas os serviços / estado de seu próprio domínio.

Este exemplo provavelmente é muito simplista, mas mesmo nesse nível as coisas ficam menos bonitas com chamadas de API em ACs.

A diferença vem do fato de que, ao contrário dos manipuladores de eventos na reformulação, ActionCreators são proativos no tratamento da lógica de negócios. E apenas um AC pode ser executado com base no evento DOM. Este AC de nível superior pode chamar outros ACs. Pode haver ACs separados para UserListApi e CommentListApi para que os domínios sejam melhor separados, mas sempre deve haver AC (semelhante ao controlador) que os conecta. Esta parte é um código imperativo bastante clássico, enquanto o re-frame é totalmente baseado em eventos. Com um pouco de trabalho, ele pode ser substituído por uma abordagem preferencial, baseada em eventos, observáveis, cps etc.

@vladap Seria interessante ver se outra abordagem é viável: quando os redutores podem ter efeitos colaterais, mas também podem isolá-los para que possam ser ignorados na reprodução.

Digamos que a assinatura dos redutores mudaria para: (state, action) => (state, sideEffects?) onde sideEffects é um fechamento. Então, dependendo da estrutura de contexto, você pode avaliar esses efeitos colaterais ou ignorá-los (no caso de repetição).

Então, o exemplo da descrição do problema original seria assim:

export default function user(state = initialState, action) {
  switch (action.type) {
    case LOGIN_ATTEMPT:
      return [state, () => {LoginApiCall.login(action.user)}];
    case LOGGED_FAILED:
      // do something
      return state;
    case LOGGED_SUCCESSFULLY:
      // do something else
     return state;
    default:
      return state;
  }
}

(todo o resto permanece praticamente o mesmo)

Pelo menos seu fluxo de controle está em um lugar, seus efeitos colaterais são sempre simples (porque eles apenas criam outra ação / evento em manipuladores assíncronos e a lógica permanece em redutores - portanto, mais de seu código será reproduzido).

Você também pode escrever código FSM ou Statechart regular (com composição de redutores / manipuladores de eventos)

Além disso, você não precisa devolver o fechamento com assinatura predefinida nos criadores de ação.

Não tenho certeza de quais problemas essa abordagem pode causar, mas, para mim, vale a pena brincar.

Digamos que a assinatura dos redutores mudaria para: (estado, ação) => (estado, efeitos colaterais?) Em que efeitos colaterais são um fechamento. Então, dependendo da estrutura de contexto, você pode avaliar esses efeitos colaterais ou ignorá-los (no caso de repetição).

Na verdade, é semelhante ao que ouvi Elm fazer com (State, Action) => (State, Request?) . Não vi nenhum exemplo ainda, mas se alguém quiser explorar isso, sinta-se à vontade para.

Ainda outra abordagem para possivelmente obter alguma inspiração é uma implementação de eventsourcing no topo do modelo Actor - Akka Persistance , PersistentFSM . Não digo que seja melhor, mas não é ruim conhecer outras tentativas. Os atores podem usar o efeito colateral, mas se bem me lembro, às vezes é necessário escrever um código explícito como reproduzir.

Talvez o recurso de repetição possa estar distorcendo nossas decisões de qual código pertence. Parece que estamos começando a pensar como ... "Eu quero este reproduzível, portanto, ele tem que ir para um redutor". Isso pode levar a um código com limites obscuros porque não o estamos dividindo por suas responsabilidades / funções e tendemos a cortar artificialmente nossa lógica de negócios para torná-lo reproduzível.

Se eu colocasse o recurso de repetição de lado, como escreveria meu código de negócios? Meu objetivo era ter minha lógica de negócios na frente de um log de ações e tê-la em um só lugar. Máquinas de estado de lógica de negócios, ou o que quer que eu esteja usando, fariam todas as decisões complexas e difíceis, resultando em um fluxo já resolvido de FACTS (ações). Em geral, os redutores seriam simples atualizadores e sua principal responsabilidade seria como aplicar a carga útil da ação a Redux.state. Eles podem ter alguma lógica, mas de certo modo interpretar os fatos de maneira diferente para obter outro tipo de visão sobre o fluxo de fatos (ação) para apresentá-los a um usuário.

Para dividir as responsabilidades, gosto de pensar nisso também. Qual código poderia ser potencialmente solicitado para ser reimplementado em um servidor? Fe para suportar melhor dispositivos lentos, movendo a lógica de negócios de computação cara, talvez algumas razões de segurança, propriedade de inteligência escondendo tudo. Não consigo mover a lógica dos redutores para o servidor tão facilmente porque está ligada a uma camada de visualização. Se uma lógica de negócios complexa for escrita na frente do log de ação, posso substituí-la por chamadas REST que se traduzem no mesmo fluxo de ação e minha camada de visualização deve funcionar (provavelmente).

A desvantagem é que, ao contrário do Akka Persistance, não tenho facilidade em reproduzir esse código.

Eu tenho que dar uma olhada mais profunda no Elm um dia e entender como sua proposta funcionaria.

@gaearon Obrigado por mencionar o Elm. Encontrei esta conversa - https://gist.github.com/evancz/44c90ac34f46c63a27ae - com ideias semelhantes (mas muito mais avançadas).

Eles introduzem o conceito de Tarefas (http, db, etc) e Efeitos (apenas uma fila de tarefas). Portanto, seu "redutor" pode, de fato, retornar um novo estado e efeitos.

O legal sobre isso (e eu não tinha pensado nisso) - é que se você pode coletar tarefas enquanto encadeia seus redutores - e depois aplicar alguma função a essa lista. Digamos que você pudesse agrupar solicitações HTTP, agrupar consultas de banco de dados com transações etc.

Ou você poderia ter, digamos "gerenciador de reprodução", que é apenas um noop para efeitos.

@merk Acho que a maior parte já foi escrita. Só que esse tipo de código autônomo de efeito colateral é provavelmente o mais complicado. Provavelmente causará estragos no replay porque os intervalos não serão sincronizados com o cronograma, assumindo que o cronograma está rodando no mesmo código e os intervalos começarão no modo de replay também.

O aplicativo normal não precisa de token e contagem regressiva de expiração apresentados a um usuário, portanto, tecnicamente, não precisa estar em Redux.state. Pode ser necessário ter o aplicativo de administrador. Portanto, você pode implementá-lo de duas maneiras, o que for mais adequado para você.

Uma opção é iniciar a contagem regressiva de expiração no AC de login e suponho que ele seja executado indefinidamente e acabe com o aplicativo ou faça logoff para limpá-lo. Quando o intervalo é acionado, ele faz o que é necessário para confirmar o token e, se expirado, despacha a ação LOGIN_EXPIRED e o redutor de escuta limpa a sessão do usuário e altera a localização, que por sua vez aciona a transição do roteador para / login.

Outra é usar a biblioteca de terceiros e delegar preocupações a ela e traduzir sua api para a ação LOGIN_EXPIRED. Ou escreva o seu próprio e mantenha o token e o estado de contagem regressiva aqui se não precisar deles na camada de visualização.

Você pode mantê-lo em Redux.state, mas esse estado é volátil. Com o estado grande, podemos ter os mesmos problemas da programação com variáveis ​​globais. E é difícil, provavelmente impossível testá-lo. É fácil testar redutores, mas então prova-se que funciona quando apenas um redutor está atualizando aquela chave particular no objeto de estado. Isso pode ficar ruim em projetos grandes com muitos desenvolvedores de qualidades diferentes. Qualquer um pode decidir escrever redutor e pensar que algum pedaço de estado é bom para atualizar e eu não consigo imaginar como testar todos os redutores juntos em todas as sequências de atualização possíveis.

Dependendo do seu caso de uso, pode fazer sentido proteger esse estado, porque é um login - uma funcionalidade muito importante e separada. Se este estado for necessário em uma camada de visão, então replique-o para Redux.state de maneira semelhante, como o estado de localização do roteador react é replicado para Redux.state em alguns exemplos. Se o seu estado e a sua equipe forem pequenos e bem comportados, pode ser que você os tenha em um só lugar.

@vladar @vladap

Uau, essa essência do Elm é incrivelmente legal! Ele também me lembra da arquitetura de “drivers” do

@merk Na verdade, LOGIN_EXPIRED autônomo não afetaria muito o replay porque chegaria ao final do log de ações e não seria processado imediatamente por um replay e talvez nem chegasse ao log de ações - eu não sei como exatamente o replay é implementado.

@gaeron Parece que no alto nível Elm and Cycle implementa o padrão que eu estava tentando descrever, se entendi corretamente. Eu tenho um navegador que me dá uma plataforma base, eu estendo essa plataforma com minha camada de serviço (http, db ...) para construir uma plataforma para meu aplicativo. Então eu preciso de algum tipo de cola que interaja com a camada de serviço e permita que meu aplicativo puro se comunique indiretamente com ela para que meu aplicativo possa descrever o que deseja fazer, mas não realmente executá-lo (mensagens enviadas para motoristas de ciclo, Elm Effects ter uma lista de tarefas). Eu poderia dizer que meu aplicativo está construindo um "programa" usando estruturas de dados (me lembra do código como mantra de dados) que é então passado para a camada de serviço para execução e meu aplicativo está interessado apenas em um resultado desse "programa" que ele então aplica ao seu estado.

@merk : Além disso. Se o token e sua expiração forem colocados em Redux.state / ou em qualquer outro serviço js, ​​ele não será transferido quando o usuário abrir o aplicativo em uma nova guia. Eu diria que o usuário espera que ele permaneça conectado. Portanto, presumo que esse estado deve ser realmente em SessionStore.

Mesmo sem isso, os estados podem ser separados quando a expiração do token não significa limpeza imediata das informações do usuário e transição para / login. Até que o usuário não toque no servidor com suas interações (ele tem dados suficientes em cache), ele pode continuar a trabalhar e a ação LOGIN_EXPIRED é acionada apenas quando ele faz algo que exige um servidor. O estado isLogged baseado em Redux.state não precisa significar que existe um token válido. O estado isLogged em Redux.state é usado para decidir o que renderizar, o estado ao redor de um token pode ser oculto em uma camada de visão e mantido somente no nível dos criadores de ação, sem ter que escrever ações e redutores para ele, exceto aqueles que afetam uma camada de visão.

@vladar , acho que posso entender seu ponto geral. Acho que não percebi isso antes.

Não há uma maneira fácil de devolver o controle do redutor ao criador da ação e passar alguns valores. Vamos supor que esses valores não sejam necessários para renderização, eles são até temporários, mas são necessários para iniciar uma operação assíncrona.

actionCreator() {
  ... getState
  ... some logic L1
  ... dispatch({ACTION1})
}

reducer() {
  case ACTION1
    ... some logic L2
    ... x, y result from L2
    ... some logic L3
    ... resulting in new state
    ... return new state
}

Existem 3 opções para iniciar a operação assíncrona usando x, y calculados no redutor. Nenhum deles é perfeito.

1) Mova a lógica do redutor para o criador da ação e diminua o poder de recarga a quente. O redutor é apenas um atualizador de estado burro ou tem lógica L3 em diante.

2) Salve x, y em Redux.state, poluindo-o com valores temporários / transientes e basicamente usando-o como um canal de comunicação global entre redutores e criadores de ações que não estou convencido de gostar. Eu acho que está bem se for o estado real, mas não para esse tipo de valores. O criador da ação muda para:

actionCreator() {
  ... getState
  ... some logic L1
  ... dispatch({ACTION1})
  // I assume I get updated state if previous dispatch is sync.
  // If previous dispatch is async it has to be chained properly.
  // If updated state can't be received here after a dispatch
  // then I would think there is a flaw.
  ... getState
  ... asyncOperation(state.x, state.y)
}

3) Salve x, y para o estado, use-o como adereços em algum componente e faça um túnel através de todo o loop da camada de visualização usando componentWillReceiveProps, acionando o novo criador de ação e a operação assíncrona. A pior opção se você me perguntar, espalhar a lógica de negócios por todo o lugar.

@vladar
Geralmente, não importa onde o assíncrono é iniciado, pois o resultado dessa operação é compactado como uma ação, despachado e aplicado por um redutor. Vai funcionar da mesma forma. É a mesma discussão do fluxo vanilla se o async deve ser iniciado nos criadores de ação ou nas lojas.

Exigiria que o Redux fosse capaz de identificar e ignorar essas chamadas assíncronas no replay, que é exatamente o que você sugere.

Não há uma maneira fácil de devolver o controle do redutor ao criador da ação e passar alguns valores.

Eu diria que este é um dos sintomas. Seu segundo exemplo ilustra muito bem meu ponto de vista (veja abaixo).

Considere o fluxo generalizado de transição de estado (no mundo FSM):

  1. Evento chega
  2. Dado o estado atual, o FSM calcula o novo estado de destino
  3. FSM executa operações para a transição do estado atual para o novo estado

Observe que o estado atual é importante! O mesmo evento pode levar a resultados diferentes dependendo do estado atual. E, como resultado, para diferentes sequências de operações (ou argumentos) na etapa 3.

Então é basicamente assim que a transição de estado no Flux funciona quando sua ação é sincronizada. Seus redutores / lojas são de fato FSMs.

Mas e as ações assíncronas? A maioria dos exemplos de Flux com ações assíncronas forçam você a pensar que ele está invertido:

  1. AC realiza operação assíncrona (independentemente do estado atual)
  2. AC despacha ação como resultado desta operação
  3. Redutor / Loja lida com isso e muda de estado

Portanto, o estado atual é ignorado, o estado de destino é considerado sempre o mesmo. Embora, na realidade, o fluxo de transição de estado consistente ainda seja o mesmo. E tem que ser assim (com ACs):

  1. O AC obtém o estado atual antes que a ação seja despachada
  2. AC despacha ação
  3. AC lê novo estado após a ação
  4. Estado dado antes da ação e depois - ele decide quais operações executar (ou quais valores passar como argumentos)

Exatamente seu segundo exemplo. Não estou dizendo que todas essas etapas são necessárias o tempo todo. Freqüentemente, você pode omitir alguns deles. Mas em casos complexos - é assim que você terá que agir. É o fluxo generalizado de qualquer transição de estado. E também significa que os limites de seu FSM foram movidos para AC versus redutor / armazenamento.

Mas mover fronteiras causa outros problemas:

  1. Se as operações assíncronas estivessem na loja / redutor - você poderia ter dois FSMs independentes reagindo ao mesmo evento. Então, para adicionar um novo recurso - você pode apenas adicionar loja / redutor que reage a algum evento existente e isso é tudo.
    Mas com operações em AC - você teria que adicionar store / reducer e também editar AC existente adicionando parte assíncrona da lógica lá também. Se já continha lógica assíncrona para algum outro redutor ... não fica legal.
    Manter esse código é obviamente mais difícil.
  2. O fluxo de controle do seu aplicativo é diferente no caso de ações sincronizadas e sincronizadas. Para ações de sincronização, o redutor está no controle da transição, no caso de assíncrono - o AC está efetivamente no controle de todas as transições que podem ser potencialmente causadas por este evento / ação.

Observe que a maioria dos problemas são ocultados por requisitos de estado simples da maioria dos aplicativos da web. Mas se você tiver um aplicativo com estado complexo - você definitivamente enfrentará essas situações.

Na maioria dos casos, você provavelmente encontrará todos os tipos de soluções alternativas. Mas você poderia evitá-los em primeiro lugar se sua lógica de transição de estado fosse melhor encapsulada e seus FSMs não estivessem separados entre ACs e redutores.

Infelizmente, a parte de "efeito colateral" da transição de estado ainda é uma parte da transição de estado, e não uma parte separada e independente da lógica. É por isso que (state, action) => (state, sideEffects) parece mais natural. Sim, isso não é mais uma assinatura de "redução"%) Mas o domínio do framework não é transformações de dados, mas transições de estado.

É a mesma discussão do fluxo vanilla se o async deve ser iniciado nos criadores de ação ou nas lojas.

Sim, mas o Flux não proíbe que você tenha coisas assíncronas nas lojas, desde que você dispare () em callbacks assíncronos vs estado mutante diretamente. Eles não têm opinião própria, mesmo que a maioria das implementações recomende o uso de ACs para assíncrono. Pessoalmente, acho que é uma reminiscência do MVC, porque é mentalmente conveniente tratar os ACs como controladores em vez de fazer uma mudança mental para os FSMs.

@vladar

(state, action) => (state, sideEffects)

Só pensando. Isso significa que teríamos que alterar o tipo de retorno da função combineReducers para [state, SideEffects] ou [state, [SideEffects]] e alterar seu código para combinar SideEffects dos redutores. Original (estado, ação) => tipo de redutor de estado pode permanecer suportado porque combineReducers forçaria (estado) o tipo de retorno para [estado] ou [estado, []].

Então, precisamos executar e despachar SideEffects em algum lugar do Redux. Isso poderia ser feito em Store.dispatch () que aplicaria um novo estado e executaria a lista SideEffects e a despacharia. Estou pensando se podemos entrar em alguma recursão interminável agradável.

Alternativamente, ele poderia ser executado em algum lugar no nível de middleware, mas acho que Store.dispatch precisaria retornar a lista SideEffects junto com uma ação.

@vladar

Sim, mas o Flux não proíbe que você tenha coisas assíncronas nas lojas, desde que você dispare () em callbacks assíncronos vs estado mutante diretamente. Eles não têm opinião própria, mesmo que a maioria das implementações recomende o uso de ACs para assíncrono. Pessoalmente, acho que é uma reminiscência do MVC, porque é mentalmente conveniente tratar os ACs como controladores em vez de fazer uma mudança mental para os FSMs.

Concordo que tratar ActionCreators como controladores não é uma boa mentalidade. É a rota pela qual acabo pensando que, para uma lógica complexa, preciso devolver o controle do redutor ao AC. Eu não deveria precisar disso. Se o AC for assíncrono, eles devem ser no máximo como manipuladores de solicitação - executar com base na decisão tomada em outro lugar e essa solicitação não deve cruzar domínios.

Para evitar seu papel como controladores, a única opção agora é rotear a lógica por meio de componentes inteligentes e usar ActionCreators apenas para executar a etapa inicial de uma lógica em uma resposta direta à IU. A primeira decisão real é feita em um componente inteligente baseado em Redux.state e / ou estado local do componente.

Esta abordagem em seu exemplo anterior de exclusão de categoria e atualização / exclusão de outra coisa em resposta usando webapi não seria legal. Suponho que isso levará a uma mistura de diferentes técnicas com base nas preferências do desenvolvedor - deve ser no AC, no redutor, no componente inteligente? Vamos ver como os padrões irão evoluir.

Eu estava comprando o ponto para manter as chamadas assíncronas longe das lojas. Mas não acho que seja válido porque, imho, aumenta significativamente a complexidade em outro lugar. Por outro lado, não vejo muita complexidade em ter função assíncrona em uma loja / redutor, já que ela só executa lá e despacha - ou simplesmente abordar e manipular essas funções como dados e acioná-las em algum lugar distante.

Os atores fazem da mesma forma, eles podem executar assíncronos e enviar mensagem (objeto de ação) para outro ator, ou enviá-la para você mesmo para continuar em seu FSM.

Estou fechando porque não parece haver nada acionável aqui. O documento de

@gaearon você pode adicionar código real implementando a maneira correta de lidar com chamadas de API a um dos exemplos com redux (fora do exemplo do "mundo real" que usa middleware)?

Ainda tenho dúvidas sobre o posicionamento real depois de ler todos os documentos e esse problema algumas vezes. Estou tendo problemas para entender quais arquivos colocar esses efeitos colaterais https://github.com/rackt/redux/issues/291#issuecomment -123010379 . Acho que este é o exemplo "correto" deste tópico de discussão.

Agradecemos antecipadamente por qualquer esclarecimento nesta área.

Eu gostaria de apontar um pensamento importante sobre criadores de ação impura:

Colocar chamadas assíncronas nos criadores de ação, ou ter criadores de ação impuros em geral, tem uma grande desvantagem para mim: fragmenta a lógica de controle de tal forma que trocar completamente a lógica de controle de seu aplicativo não é tão simples quanto trocar sua loja O Criador. Pretendo sempre usar middleware para processos assíncronos e evitar completamente criadores de ações impuras.

O aplicativo que estou desenvolvendo terá pelo menos duas versões: uma que roda em um Raspberry Pi local e outra que roda um portal. Pode até haver versões separadas para um portal / portal público operado por um cliente. Não se sabe se devo usar APIs diferentes, mas quero me preparar para essa possibilidade da melhor maneira possível, e o middleware me permite fazer isso.

E, para mim, o conceito de ter chamadas API distribuídas entre os criadores de ações é totalmente contrário ao conceito Redux de controle centralizado sobre atualizações de estado. Claro, o simples fato de que uma solicitação de API está sendo executada em segundo plano não faz parte do estado da loja Redux - mas ainda é o estado do aplicativo, algo sobre o qual você deseja ter controle preciso. E eu acho que posso ter o controle mais preciso sobre isso quando, como com as lojas / redutores Redux, esse controle é centralizado em vez de distribuído.

@ jedwards1211 Você pode estar interessado em https://github.com/redux-effects/redux-effects , caso você não tenha verificado ainda, bem como na discussão em # 569.

@gaearon legal, obrigado por apontar isso! Em qualquer caso, usar meu próprio middleware não foi muito difícil até agora :)

Eu criei uma abordagem semelhante à do Elm: efeito colateral redux . O README explica a abordagem e a compara com alternativas.

@gregwebs Eu gosto de redux-side-effect , embora também seja estranho com a lógica de controle de troca, porque há a questão de como eu coloco a função sideEffect em meus módulos redutores, quando há vários configuradores de loja diferentes módulos a serem usados ​​em diferentes construções.

Eu poderia usar um hack engraçado: basta armazenar sideEffect no próprio state , para que fique automaticamente disponível para o redutor! : stick_out_tongue_winking_eye:

Algo como https://github.com/rackt/redux/pull/569/ é quase ideal para como desejo trabalhar, embora, é claro, não queira usá-lo em um projeto de produção, a menos que se torne uma parte padrão da API.

Aqui está uma ideia: faça com que um middleware coloque uma função sideEffect na ação. Um pouco hacky, mas a menor alteração possível é necessária para tornar os redutores capazes de iniciar o código assíncrono:

sideEffectMiddleware.js :

export default store => next => action => {
  let sideEffects = [];
  action.sideEffect = callback => sideEffects.push(callback);
  let result = next(action);
  sideEffects.forEach(sideEffect => sideEffect(store));
  return result;
};

Existem várias maneiras de usar a abordagem sideEffect simples. Experimente suas variantes e informe de volta

Refatorei meu código para usar a abordagem sideEffectMiddleware acima e realmente gosto da organização do código resultante. É bom não ter que importar a função sideEffect de qualquer lugar em meus redutores, ela apenas vem em action .

@ jedwards1211 Publiquei seu código como actionSideEffectMiddleware na versão 2.1.0 do pacote.

@gregwebs legal! Importa-se de me listar como colaborador?

@ jedwards1211 você foi adicionado. Talvez você adicione o suporte de replay adequado :) Não posso tirar vantagem disso ainda quando estou usando isso.

@gaearon Presumo que as ações gravadas e reproduzidas não passam pelo middleware, certo?

Eles fazem, é só que eles já são objetos simples no momento em que são gravados, portanto, o middleware geralmente não intervém.

@gaearon Hmmm. Então, na verdade, não testei esse recurso, mas ... estou supondo que alguns middlewares de efeitos colaterais quebrariam recordes / repetições também? Tome redux-effects-fetch por exemplo, ele ainda executaria uma solicitação ajax em uma ação FETCH de objeto simples:

function fetchMiddleware ({dispatch, getState}) {
  return next => action =>
    action.type === 'FETCH'
      ? fetch(action.payload.url, action.payload.params).then(checkStatus).then(deserialize, deserialize)
      : next(action)
}

Se o gravador limpar os retornos de chamada [success, failure] da ação FETCH steps , então o middleware redux-effects não despacharia nenhuma ação de alteração de estado, mas ainda assim, não seria desejável repetindo para acionar um monte de solicitações de ajax.

Existe alguma maneira de separar o middleware "puro" (que nunca despacha ações adicionais) como redux-logger de middleware "impuro" (que pode despachar ações)?

Infelizmente, não examinei redux-effects perto, por isso não posso responder à sua pergunta.

Não há problema, em qualquer caso, o gravador adiciona algum sinalizador às ações que o middleware pode usar para decidir se deve ou não executar efeitos colaterais? Estou apenas tentando descobrir uma maneira em meu próprio middleware ser capaz de suportar os devtools corretamente

Não é verdade. Espera-se que devTools() seja colocado _após_ applyMiddleware na cadeia de composição do intensificador, de forma que, no momento em que a ação seja alcançada, seja uma ação “final” que não necessite de interpretação adicional.

Ah, entendo, então, depois que um middleware executa um efeito colateral, ele pode simplesmente remover quaisquer campos da ação / ajustá-la para que não acione o efeito colateral quando voltar para o middleware na reprodução, e então deve trabalhar com as ferramentas de desenvolvimento, certo?

Sim, parece um bom caminho.

Ótimo, obrigado por me esclarecer!

@gaearon @vladar @ jedwards1211 Você pode estar interessado em https://github.com/salsita/redux-side-effects. Basicamente, é uma implementação do elmish (state, action) => (state, sideEffects) mas em vez do redutor retornar a tupla (que você já percebeu que não é possível), ele está produzindo efeitos.
Eu joguei com ele em breve, o hot reload e o replay parecem estar ok. Não vejo nenhum recurso redux quebrado, até agora, mas talvez alguns dos reduxers seniores possam encontrar algo. :)
Para mim, isso parece um acréscimo realmente importante para redux stack, pois permite que a lógica esteja principalmente em redutores, tornando-os máquinas de estado eficazes e testáveis, que delegam efeitos (ações de transição cujo resultado não depende apenas do estado atual) para serviços externos ( muito parecido com a arquitetura, efeitos e serviços do Elm).

Outra abordagem é a saga redux, que também usa geradores, mas mantém os efeitos colaterais separados dos redutores.

@gaearon , acredito que @minedeljkovic significava o

Para mim, isso parece um acréscimo realmente importante para redux stack, pois permite que a lógica esteja principalmente em redutores, tornando-os máquinas de estado eficazes e testáveis, que delegam efeitos (ações de transição cujo resultado não depende apenas do estado atual) para serviços externos ( muito parecido com a arquitetura, efeitos e serviços do Elm).

como a principal vantagem sobre a abordagem tradicional, porque https://github.com/yelouafi/redux-saga/ é muito semelhante ao modo como redux-thunk funciona, em vez de algum açúcar de sintaxe em cima dele, o que torna as transações de longa duração mais fáceis .

https://github.com/salsita/redux-side-effects por outro lado é mais parecido com a arquitetura Elm.

Sim, redux-saga e redux-side-effects usam geradores apenas para declarar os efeitos colaterais, mantendo sagas e redutores puros, respectivamente. Isso é semelhante.

Duas razões pelas quais eu prefiro isso em redutores:

  1. Você tem acesso explícito ao estado atual que pode afetar como o efeito deve ser declarado (acho que foi um dos pontos de @vladar durante esta discussão)
  2. Não há nenhum novo conceito introduzido (Saga em redux-saga)

Meu comentário em https://github.com/rackt/redux/issues/1139#issuecomment -165419770 ainda é válido, pois redux-saga não resolverá isso e não há outra maneira de resolver isso exceto aceitar o modelo usado na arquitetura Elm.

Eu coloquei uma essência simples que está tentando enfatizar meus pontos sobre redux-side-effects: https://gist.github.com/minedeljkovic/7347c0b528110889aa50

Tentei definir o cenário mais simples possível que acho que é apenas "mundo real" o suficiente, mas ainda enfatizo alguns dos pontos desta discussão.

O cenário é:
O aplicativo tem a parte "Registro do usuário" onde os dados pessoais do usuário devem ser inseridos. Entre outros dados pessoais, o país de nascimento é selecionado na lista de países. A seleção é realizada na caixa de diálogo "Seleção de país", onde o usuário seleciona um país e tem a opção de confirmar ou cancelar uma seleção. Se a usuária estiver tentando confirmar a seleção, mas nenhum país for selecionado, ela deve ser avisada.

Restrições de arquitetura:

  1. A funcionalidade CountrySelection deve ser modular (no espírito de redux ducks ), para que possa ser reutilizada em várias partes do aplicativo (por exemplo, também na parte "Administração do produto" do aplicativo, onde o país de produção deve ser inserido)
  2. A fatia de estado de CountrySelection não deve ser considerada global (estar na raiz do estado redux), mas deve ser local para o módulo (e controlada por esse módulo) que o está chamando.
  3. A lógica central desta funcionalidade deve incluir o mínimo possível de partes móveis (em minha essência, esta lógica central é implementada apenas em redutores (e apenas na parte mais importante da lógica). Os componentes devem ser triviais, como todos os componentes orientados a redux estar :) . Sua única responsabilidade seria processar o estado atual e despachar ações.)

A parte mais importante da essência, com relação a essa conversa, está na maneira como o redutor countrySelection está lidando com a ação CONFIRM_SELECTION.

@gaearon , eu

Uma possível ideia alternativa para implementação deste cenário (sem usar redux-side-effects, mas usando redux-saga, redux-thunk ou algum outro método), também seria muito apreciada.

@ tomkis1 , adoraria aqui sua opinião, estou usando ou abusando da sua biblioteca aqui. :)

(Há uma implementação ligeiramente diferente nesta essência https://gist.github.com/minedeljkovic/9d4495c7ac5203def688, onde globalActions é evitado. Não é importante para este tópico, mas talvez alguém queira comentar sobre este padrão)

@minedeljkovic Obrigado pela essência. Eu vejo um problema conceitual com seu exemplo. redux-side-effects deve ser usado apenas para efeitos colaterais. Em seu exemplo, entretanto, não há efeitos colaterais, mas sim uma transação comercial de longa duração e, portanto, redux-saga é muito mais adequado. @slorber e @yelouafi podem lançar mais luz sobre isso.

Em outras palavras, o problema mais preocupante para mim é dispatching síncrono de nova ação no redutor (https://github.com/salsita/redux-side-effects/pull/9#issuecomment-165475991):

yield dispatch => dispatch({type: COUNTRY_SELECTION_SUCCESS, payload: state.selectedCountryId});

Eu acredito que @slorber veio com o termo chamado "efeito colateral do negócio" e este é exatamente o seu caso. redux-saga brilha na solução deste problema específico.

@mindjuice, não tenho certeza se entendi seu exemplo, mas gosto do exemplo de integração que dei aqui: https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment -162822909

O padrão da saga também permite tornar explícitas algumas coisas implícitas.
Por exemplo, você apenas descreve o que aconteceu disparando ações (eu prefiro eventos porque para mim eles deveriam estar no tempo passado), e então algumas regras de negócios entram em ação e atualizam a IU. No seu caso, a exibição do erro está implícita. Com uma saga, ao confirmar, se nenhum país for selecionado, você provavelmente enviará uma ação "NO_COUNTRY_SELECTED_ERROR_DISPLAYED" ou algo parecido. Prefiro que seja totalmente explícito.

Além disso, você pode ver a Saga como um ponto de acoplamento entre patos.
Por exemplo, você tem duck1 e duck2, com ações locais em cada um. Se você não gostar da ideia de acoplar os 2 patos (quero dizer, um pato usaria os criadores de ações do segundo pato), você pode apenas deixá-los descrever o que aconteceu e, em seguida, criar uma Saga que conecta algumas regras de negócios complexas entre os 2 patos.

Entããão, é um fio absurdamente longo e ainda há alguma solução para o problema?

Suponha que você tenha um action() assíncrono e seu código deve indicar um erro ou mostrar o resultado.

A primeira abordagem foi fazer como

// the action call
action().then(dispatch(SUCCESS)).catch(dispatch(FAILURE))

// the reducer
case SUCCESS:
    state.succeeded = true
    alert('Success')

case FAILURE:
    state.succeeded = false
    alert('Failure')

Mas acontece que não é o Redux-way porque agora o redutor contém "efeitos colaterais" (não sei o que isso quer dizer).
Resumindo, a maneira certa é mover esses alert() s para fora do redutor em algum lugar.

Esse em algum lugar pode ser o componente React que chama isso de action .
Então agora meu código se parece com:

// the reducer
case SUCCESS:
    state.succeeded = true

case FAILURE:
    state.succeeded = false

// the React component
on_click: function()
{
    action().then
    ({
        dispatch(SUCCESS)

        alert('Success')
        // do something else. maybe call another action
    })
    .catch
    ({
        dispatch(FAILURE)

        alert('Failure')
        // do something else. maybe call another action
    })
}

Esta é a maneira certa de fazer isso?
Ou preciso investigar mais e aplicar alguma biblioteca de terceiros?

Redux não é nada simples. Não é fácil de entender no caso de aplicativos do mundo real. Mabye, um exemplo de repositório de aplicativos do mundo real, é necessário como um exemplo canônico que ilustra a maneira certa.

@ halt-hammerzeit Redux em si é muito simples; a fragmentação confusa vem de diferentes necessidades ou opiniões sobre a integração / separação de efeitos colaterais de redutores ao usar Redux.

Além disso, a fragmentação vem do fato de que realmente não é tão difícil fazer os efeitos colaterais da maneira que você quiser com o Redux. Levei apenas cerca de 10 linhas de código para lançar meu próprio middleware de efeito colateral básico. Portanto, abrace a sua liberdade e não se preocupe com o caminho "certo".

@ jedwards1211 "Redux em si" não tem nenhum valor ou significado e não faz nenhum sentido porque seu objetivo principal é resolver os problemas diários dos desenvolvedores de Flux. Isso implica AJAX, belas animações e todas as outras coisas _ comuns e esperadas_

@ halt-hammerzeit você tem razão. O Redux certamente parece ter a intenção de fazer a maioria das pessoas concordar sobre como gerenciar as atualizações de estado, então é uma pena que não tenha levado a maioria das pessoas a concordar sobre como realizar os efeitos colaterais.

Você viu a pasta "examples" no repo?

Embora existam várias maneiras de realizar efeitos colaterais, geralmente a recomendação é fazer isso fora do Redux ou dentro dos criadores de ação, como fazemos em todos os exemplos.

Sim, eu sei ... tudo o que estou dizendo é que este tópico ilustrou uma falta de consenso (pelo menos entre as pessoas aqui) sobre a melhor maneira de realizar os efeitos colaterais.

Embora talvez a maioria dos usuários use criadores de ação thunk e sejamos apenas outliers.

^ o que ele disse.

Eu prefiro que todas as grandes mentes concordem com uma única solução Righteous
e esculpi-o na pedra para que eu não tenha que ler através de 9.000 telas
fio

Na quinta-feira, 7 de janeiro de 2016, Andy Edwards [email protected] escreveu:

Sim, eu sei ... tudo o que estou dizendo é que este tópico ilustrou a falta de
consenso (pelo menos entre as pessoas aqui) sobre a melhor maneira de realizar o lado
efeitos.

-
Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/rackt/redux/issues/291#issuecomment -169751432.

Então, eu acho, a solução atualmente acordada é fazer tudo em ação
criadores. Vou tentar usar essa abordagem e, no caso de encontrar alguma falha
vou postar de volta aqui

Na quinta-feira, 7 de janeiro de 2016, Николай Кучумов [email protected] escreveu:

^ o que ele disse.

Eu prefiro que todas as grandes mentes concordem com uma única solução Righteous
e esculpi-o na pedra para que eu não tenha que ler através de 9.000 telas
fio

Na quinta-feira, 7 de janeiro de 2016, Andy Edwards < notificaçõ[email protected]
<_e i = "16">

Sim, eu sei ... tudo o que estou dizendo é que este tópico ilustrou a falta de
consenso (pelo menos entre as pessoas aqui) sobre a melhor maneira de realizar o lado
efeitos.

-
Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/rackt/redux/issues/291#issuecomment -169751432.

Este e outros tópicos semelhantes existem porque 1% dos usuários do Redux gostam de procurar soluções mais poderosas / puras / declarativas para efeitos colaterais. Não fique com a impressão de que ninguém pode concordar com isso. A maioria silenciosa usa pensamentos e promessas e está mais feliz com essa abordagem. Mas, como qualquer tecnologia, tem desvantagens e desvantagens. Se você tem uma lógica assíncrona complexa, pode abandonar o Redux e explorar o Rx. O padrão Redux é facilmente expressivo em Rx.

Ok obrigada

Na quinta-feira, 7 de janeiro de 2016, Dan Abramov [email protected] escreveu:

Este e outros tópicos existem porque 1% dos usuários Redux gostam de procurar
soluções mais poderosas / puras / declarativas para efeitos colaterais. Por favor não
tenha a impressão de que ninguém pode concordar com isso. A maioria silenciosa usa
pensa e promete, e fica muito satisfeito com essa abordagem. Mas como qualquer
tecnologia tem desvantagens e desvantagens. Se você tiver lógica assíncrona complexa
você pode querer abandonar Redux e explorar Rx ao invés. O padrão Redux é
facilmente expressivo em Rx.

-
Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/rackt/redux/issues/291#issuecomment -169761410.

@gaearon sim, você está certo. E eu aprecio que o Redux seja flexível o suficiente para acomodar todas as nossas diferentes abordagens!

@ halt-hammerzeit dê uma olhada na minha resposta aqui, onde explico por que redux-saga pode ser melhor do que redux-thunk: http://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for- async-flow-in-redux / 34599594

@gaearon A propósito, sua resposta sobre stackoverflow tem a mesma falha da documentação - cobre apenas o caso mais simples
http://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux/34599594#34599594
Os aplicativos do mundo real rapidamente vão além de apenas buscar dados via AJAX: eles também devem lidar com erros e executar algumas ações, dependendo da saída da chamada de ação.
Seu conselho é apenas dispatch algum outro evento e pronto.
E daí se meu código quiser alert() um usuário na conclusão da ação?
É aí que essas thunk s não vão ajudar, mas as promessas vão.
Atualmente, escrevo meu código usando promessas simples e funciona assim.

Eu li o comentário adjacente sobre redux-saga .
Não entendi o que isso faz, lol.
Não gosto muito de monads e coisas do gênero, e ainda não sei o que é thunk , e não gosto dessa palavra estranha para começar.

Ok, então continuo usando os componentes do Promises no React.

Sugerimos retornar promessas de thunks em todos os documentos.

@gaearon Sim, é disso que estou falando: on_click() { dispatch(load_stuff()).then(show_modal('done')) }
Que vai fazer.

Acho que ainda está faltando alguma coisa.
Por favor, consulte o README de redux-thunk.
Ele mostra como encadear criadores de ação thunk na seção "Composição".

@gaearon Sim, é exatamente o que estou fazendo:

store.dispatch(
  makeASandwichWithSecretSauce('My wife')
).then(() => {
  console.log('Done!');
});

É exatamente o que escrevi acima:

on_click()
{
    dispatch(load_stuff())
        .then(() => show_modal('done'))
        .catch(() => show_error('not done'))
}

Essa seção README é bem escrita, thx

Não, não era esse o meu ponto. Dê uma olhada em makeSandwichesForEverybody . É um criador de ação thunk chamando outros criadores de ação thunk. É por isso que você não precisa colocar tudo em componentes. Veja também async exemplo neste repositório para mais informações.

@gaearon Mas acho que não seria apropriado colocar, digamos, meu código de animação sofisticado no criador de ação, não é?
Considere este exemplo:

button_on_click()
{
    this.props.dispatch(load_stuff())
        .then(() =>
        {
                const modal = ReactDOM.getDOMNode(this.refs.modal)
                jQuery(modal).fancy_animation(1200 /* ms */)
                setTimeout(() => jQuery.animate(modal, { background: 'red' }), 1500 /* ms */)
        })
        .catch(() =>
        {
                alert('Failed to bypass the root mainframe protocol to override the function call on the hyperthread's secondary firewall')
        })
}

Como você o reescreveria da maneira certa?

@ halt-hammerzeit você pode fazer o actionCreator retornar as promessas para que o componente possa mostrar algum spinner ou qualquer outra coisa usando o estado do componente local (difícil de evitar ao usar jquery de qualquer maneira)

Caso contrário, você pode gerenciar temporizadores complexos para conduzir animações com redux-saga.

Dê uma olhada nesta postagem do blog: http://jaysoo.ca/2016/01/03/managing-processes-in-redux-using-sagas/

Oh, neste caso é bom mantê-lo no componente.
(Observação: geralmente no React é melhor usar o modelo declarativo para tudo, em vez de mostrar modais imperativamente com jQuery. Mas não é grande coisa.)

@slorber Sim, já estou devolvendo promessas e coisas do meu "thunks" (ou como você os chame; errr, eu não gosto dessa palavra estranha), então posso lidar com spinners e tal.

@gaearon Ok, peguei você. No mundo da máquina ideal, é claro que seria possível ficar dentro do modelo de programação declarativo, mas a realidade tem suas próprias demandas, irracionais, digamos, do ponto de vista de uma máquina. As pessoas são seres irracionais não apenas operando com zeros e uns puros e isso requer o comprometimento da beleza e pureza do código em favor de ser capaz de fazer algumas coisas irracionais.

Estou satisfeito com o suporte neste tópico. Minhas dúvidas parecem estar resolvidas agora.

@gaearon explicação muito incrível! : troféu: Obrigado: +1:

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

Questões relacionadas

cloudfroster picture cloudfroster  ·  3Comentários

elado picture elado  ·  3Comentários

dmitry-zaets picture dmitry-zaets  ·  3Comentários

ms88privat picture ms88privat  ·  3Comentários

caojinli picture caojinli  ·  3Comentários