Por que precisamos de middleware para fluxo assíncrono no Redux?

684

De acordo com os documentos, "Sem o middleware, o armazenamento Redux suporta apenas o fluxo de dados síncrono" . Não entendo por que esse é o caso. Por que o componente de contêiner não pode chamar a API assíncrona e dispatchas ações?

Por exemplo, imagine uma interface simples: um campo e um botão. Quando o usuário aperta o botão, o campo é preenchido com dados de um servidor remoto.

Um campo e um botão

import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';

const ActionTypes = {
    STARTED_UPDATING: 'STARTED_UPDATING',
    UPDATED: 'UPDATED'
};

class AsyncApi {
    static getFieldValue() {
        const promise = new Promise((resolve) => {
            setTimeout(() => {
                resolve(Math.floor(Math.random() * 100));
            }, 1000);
        });
        return promise;
    }
}

class App extends React.Component {
    render() {
        return (
            <div>
                <input value={this.props.field}/>
                <button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
                {this.props.isWaiting && <div>Waiting...</div>}
            </div>
        );
    }
}
App.propTypes = {
    dispatch: React.PropTypes.func,
    field: React.PropTypes.any,
    isWaiting: React.PropTypes.bool
};

const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
    switch (action.type) {
        case ActionTypes.STARTED_UPDATING:
            return { ...state, isWaiting: true };
        case ActionTypes.UPDATED:
            return { ...state, isWaiting: false, field: action.payload };
        default:
            return state;
    }
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
    (state) => {
        return { ...state };
    },
    (dispatch) => {
        return {
            update: () => {
                dispatch({
                    type: ActionTypes.STARTED_UPDATING
                });
                AsyncApi.getFieldValue()
                    .then(result => dispatch({
                        type: ActionTypes.UPDATED,
                        payload: result
                    }));
            }
        };
    })(App);
export default class extends React.Component {
    render() {
        return <Provider store={store}><ConnectedApp/></Provider>;
    }
}

Quando o componente exportado é renderizado, posso clicar no botão e a entrada é atualizada corretamente.

Observe a updatefunção na connectchamada. Ele despacha uma ação que informa ao aplicativo que está atualizando e executa uma chamada assíncrona. Após o término da chamada, o valor fornecido é despachado como carga de outra ação.

O que há de errado com essa abordagem? Por que eu gostaria de usar o Redux Thunk ou o Redux Promise, como sugere a documentação?

EDIT: Procurei no repositório Redux por pistas e descobri que os Action Creators eram exigidos como funções puras no passado. Por exemplo, aqui está um usuário tentando fornecer uma explicação melhor para o fluxo de dados assíncrono:

O criador da ação em si ainda é uma função pura, mas a função thunk que ele retorna não precisa ser, e pode fazer nossas chamadas assíncronas

Os criadores de ações não precisam mais ser puros. Portanto, o middleware thunk / promessa era definitivamente necessário no passado, mas parece que esse não é mais o caso?

sbichenko
fonte
53
Os criadores de ação nunca precisaram ser funções puras. Foi um erro nos documentos, não uma decisão que mudou.
Dan Abramov 04/01
1
@ DanAbramov por testabilidade, pode ser uma boa prática. Redux-saga permite isso: stackoverflow.com/a/34623840/82609
Sebastien Lorber

Respostas:

699

O que há de errado com essa abordagem? Por que eu gostaria de usar o Redux Thunk ou o Redux Promise, como sugere a documentação?

Não há nada de errado com essa abordagem. É apenas inconveniente em um aplicativo grande, porque você terá componentes diferentes executando as mesmas ações, poderá renunciar a algumas ações ou manter algum estado local, como IDs de incremento automático, perto dos criadores de ações, etc. Portanto, é mais fácil o ponto de vista de manutenção para extrair criadores de ação em funções separadas.

Você pode ler minha resposta em "Como despachar uma ação Redux com um tempo limite" para obter uma explicação mais detalhada.

Middleware como Redux Thunk ou Redux Promise oferecem apenas "açúcar de sintaxe" para o envio de thunks ou promessas, mas você não precisa usá-lo.

Portanto, sem nenhum middleware, seu criador de ações pode parecer

// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}

Mas com o Thunk Middleware, você pode escrever assim:

// action creator
function loadData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}

Portanto, não há grande diferença. Uma coisa que eu gosto na última abordagem é que o componente não se importa que o criador da ação seja assíncrono. É apenas chamado dispatchnormalmente, também pode ser usado mapDispatchToPropspara vincular esse criador de ação a uma sintaxe curta etc. Os componentes não sabem como os criadores de ação são implementados e você pode alternar entre diferentes abordagens assíncronas (Redux Thunk, Redux Promise, Redux Saga ) sem alterar os componentes. Por outro lado, com a abordagem explícita anterior, seus componentes sabem exatamente que uma chamada específica é assíncrona e precisa dispatchser aprovada por alguma convenção (por exemplo, como um parâmetro de sincronização).

Pense também em como esse código será alterado. Digamos que queremos ter uma segunda função de carregamento de dados e combiná-los em um único criador de ações.

Com a primeira abordagem, precisamos estar atentos ao tipo de criador de ação que estamos chamando:

// action creators
function loadSomeData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(dispatch, userId) {
  return Promise.all(
    loadSomeData(dispatch, userId), // pass dispatch first: it's async
    loadOtherData(dispatch, userId) // pass dispatch first: it's async
  );
}


// component
componentWillMount() {
  loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first
}

Com o Redux Thunk, os criadores de ações podem dispatchresultar de outros criadores de ações e nem pensar se são síncronos ou assíncronos:

// action creators
function loadSomeData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(userId) {
  return dispatch => Promise.all(
    dispatch(loadSomeData(userId)), // just dispatch normally!
    dispatch(loadOtherData(userId)) // just dispatch normally!
  );
}


// component
componentWillMount() {
  this.props.dispatch(loadAllData(this.props.userId)); // just dispatch normally!
}

Com essa abordagem, se você desejar posteriormente que seus criadores de ação analisem o estado atual do Redux, basta usar o segundo getStateargumento passado aos thunks sem modificar o código de chamada:

function loadSomeData(userId) {
  // Thanks to Redux Thunk I can use getState() here without changing callers
  return (dispatch, getState) => {
    if (getState().data[userId].isLoaded) {
      return Promise.resolve();
    }

    fetch(`http://data.com/${userId}`)
      .then(res => res.json())
      .then(
        data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
        err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
      );
  }
}

Se você precisar alterá-lo para que seja síncrono, também poderá fazer isso sem alterar nenhum código de chamada:

// I can change it to be a regular action creator without touching callers
function loadSomeData(userId) {
  return {
    type: 'LOAD_SOME_DATA_SUCCESS',
    data: localStorage.getItem('my-data')
  }
}

Portanto, o benefício do uso de middleware como Redux Thunk ou Redux Promise é que os componentes não sabem como os criadores de ação são implementados e se eles se importam com o estado Redux, sejam síncronos ou assíncronos e se chamam ou não outros criadores de ação . A desvantagem é um pouco indireta, mas acreditamos que vale a pena em aplicações reais.

Por fim, Redux Thunk e amigos é apenas uma abordagem possível para solicitações assíncronas em aplicativos Redux. Outra abordagem interessante é o Redux Saga, que permite definir daemons de longa duração (“sagas”) que executam ações à medida que chegam e transformam ou executam solicitações antes de gerar ações. Isso move a lógica dos criadores de ação para as sagas. Você pode dar uma olhada e depois escolher o que melhor combina com você.

Procurei no repositório Redux por pistas e descobri que os Action Creators eram obrigados a ter funções puras no passado.

Isto está incorreto. Os documentos disseram isso, mas estavam errados.
Os criadores de ação nunca precisaram ser funções puras.
Corrigimos os documentos para refletir isso.

Dan Abramov
fonte
57
Talvez seja a maneira mais curta de dizer o pensamento de Dan: o middleware é uma abordagem centralizada, dessa forma, você pode manter seus componentes mais simples e generalizados e controlar o fluxo de dados em um só lugar. Se você manter o aplicativo grande você vai se divertir =)
Sergey Lapin
3
@asdfasdfads Não vejo por que não funcionaria. Funcionaria exatamente da mesma maneira; colocar alertdepois dispatch()da ação.
Dan Abramov 08/01
9
Penúltima linha em seu primeiro exemplo de código: loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch. Por que preciso passar na expedição? Se por convenção há apenas uma única loja global, por que não faço referência diretamente a ela e store.dispatchsempre que preciso, por exemplo loadData?
Søren Debois
10
@ SørenDebois Se o seu aplicativo for apenas do lado do cliente, isso funcionaria. Se ele for renderizado no servidor, convém ter uma storeinstância diferente para cada solicitação, para não poder defini-la com antecedência.
Dan Abramov 25/01
3
Só quero salientar que esta resposta tem 139 linhas, 9,92 vezes mais que o código-fonte do redux-thunk, que consiste em 14 linhas: github.com/gaearon/redux-thunk/blob/master/src/index.js
Guy
447

Você não

Mas ... você deve usar o redux-saga :)

A resposta de Dan Abramov está certa, redux-thunkmas vou falar um pouco mais sobre a redux-saga que é bem parecida, mas mais poderosa.

Imperativo VS declarativo

  • DOM : jQuery é imperativo / React é declarativo
  • Mônadas : IO é imperativa / Livre é declarativa
  • Efeitos redux : redux-thunké imperativo / redux-sagaé declarativo

Quando você tem um thunk em suas mãos, como uma mônada de IO ou uma promessa, não pode saber facilmente o que ele fará depois de executar. A única maneira de testar um thunk é executá-lo e zombar do expedidor (ou de todo o mundo exterior, se ele interagir com mais coisas ...).

Se você estiver usando zombarias, não estará fazendo programação funcional.

Visto pelas lentes dos efeitos colaterais, as zombarias são uma bandeira de que seu código é impuro e, aos olhos do programador funcional, prova que algo está errado. Em vez de baixar uma biblioteca para nos ajudar a verificar se o iceberg está intacto, devemos navegar nele. Um cara de TDD / Java hardcore uma vez me perguntou como você zomba de Clojure. A resposta é que geralmente não o fazemos. Geralmente vemos isso como um sinal de que precisamos refatorar nosso código.

Fonte

As sagas (como foram implementadas redux-saga) são declarativas e, como os componentes Mônada livre ou React, são muito mais fáceis de testar sem qualquer simulação.

Veja também este artigo :

no FP moderno, não devemos escrever programas - devemos escrever descrições de programas, que podemos então introspectar, transformar e interpretar à vontade.

(Na verdade, Redux-saga é como um híbrido: o fluxo é imperativo, mas os efeitos são declarativos)

Confusão: ações / eventos / comandos ...

Há muita confusão no mundo frontend sobre como alguns conceitos de back-end como CQRS / EventSourcing e Flux / Redux podem estar relacionados, principalmente porque no Flux usamos o termo "ação", que às vezes pode representar tanto código imperativo ( LOAD_USER) quanto eventos ( USER_LOADED) Acredito que, como na fonte de eventos, você só deve despachar eventos.

Usando sagas na prática

Imagine um aplicativo com um link para um perfil de usuário. A maneira idiomática de lidar com isso com cada middleware seria:

redux-thunk

<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>

function loadUserProfile(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
      err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
    );
}

redux-saga

<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>


function* loadUserProfileOnNameClick() {
  yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}

function* fetchUser(action) {
  try {
    const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
    yield put({ type: 'USER_PROFILE_LOADED', userProfile })
  } 
  catch(err) {
    yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
  }
}

Esta saga se traduz em:

toda vez que um nome de usuário é clicado, busque o perfil do usuário e despache um evento com o perfil carregado.

Como você pode ver, existem algumas vantagens redux-saga.

O uso de takeLatestlicenças para expressar que você só está interessado em obter os dados do último nome de usuário (lida com problemas de simultaneidade caso o usuário clique muito rapidamente em muitos nomes de usuário). Esse tipo de coisa é difícil com thunks. Você poderia ter usado takeEveryse não quiser esse comportamento.

Você mantém os criadores de ação puros. Observe que ainda é útil manter os actionCreators (em sagas pute componentes dispatch), pois pode ajudá-lo a adicionar a validação de ações (asserções / fluxo / texto datilografado) no futuro.

Seu código se torna muito mais testável, pois os efeitos são declarativos

Você não precisa mais acionar chamadas semelhantes a rpc actions.loadUser(). Sua interface do usuário só precisa despachar o que ACONTECEU. Só disparamos eventos (sempre no passado!) E não mais ações. Isso significa que você pode criar "patos" dissociados ou contextos limitados e que a saga pode atuar como o ponto de acoplamento entre esses componentes modulares.

Isso significa que suas visualizações são mais fáceis de gerenciar, porque não precisam mais conter a camada de conversão entre o que aconteceu e o que deve acontecer como efeito

Por exemplo, imagine uma exibição de rolagem infinita. CONTAINER_SCROLLEDpode levar a isso NEXT_PAGE_LOADED, mas é realmente da responsabilidade do contêiner rolável decidir se devemos ou não carregar outra página? Então ele precisa estar ciente de coisas mais complicadas, como se a última página foi carregada com êxito ou se já existe uma página que tenta carregar ou se não há mais itens para carregar? Acho que não: para máxima reutilização, o contêiner rolável deve apenas descrever que foi rolado. O carregamento de uma página é um "efeito comercial" desse pergaminho

Alguns podem argumentar que os geradores podem ocultar inerentemente o estado fora do repositório redux com variáveis ​​locais, mas se você começar a orquestrar coisas complexas dentro de thunks iniciando temporizadores etc., você terá o mesmo problema de qualquer maneira. E há um selectefeito que agora permite obter algum estado da sua loja Redux.

As sagas podem ser viajadas no tempo e também permitem o registro de fluxo complexo e ferramentas de desenvolvimento que estão sendo trabalhadas no momento. Aqui está um log de fluxo assíncrono simples que já está implementado:

saga flow log

Dissociação

As sagas não estão apenas substituindo os redux thunks. Eles vêm de back-end / sistemas distribuídos / fornecimento de eventos.

É um equívoco muito comum que as sagas estejam aqui apenas para substituir seus thunks redux por uma melhor testabilidade. Na verdade, este é apenas um detalhe de implementação do redux-saga. O uso de efeitos declarativos é melhor que thunks para testabilidade, mas o padrão de saga pode ser implementado sobre o código imperativo ou declarativo.

Em primeiro lugar, a saga é um software que permite coordenar transações de longa duração (consistência eventual) e transações em diferentes contextos limitados (jargão de design orientado a domínio).

Para simplificar isso no mundo frontend, imagine que há widget1 e widget2. Quando se clica em algum botão no widget1, ele deve ter um efeito no widget2. Em vez de acoplar os 2 widgets juntos (por exemplo, o widget1 despacha uma ação que visa o widget2), o widget1 despacha apenas que seu botão foi clicado. Em seguida, a saga escuta esse botão, clique e atualize o widget2, desanexando um novo evento que o widget2 tenha conhecimento.

Isso adiciona um nível de indireção desnecessário para aplicativos simples, mas facilita a escalabilidade de aplicativos complexos. Agora você pode publicar o widget1 e o widget2 em diferentes repositórios npm para que eles nunca precisem se conhecer, sem que eles compartilhem um registro global de ações. Os 2 widgets agora são contextos limitados que podem viver separadamente. Eles não precisam ser consistentes e podem ser reutilizados em outros aplicativos. A saga é o ponto de acoplamento entre os dois widgets que os coordenam de maneira significativa para o seu negócio.

Alguns bons artigos sobre como estruturar seu aplicativo Redux, nos quais você pode usar o Redux-saga por razões de desacoplamento:

Um caso de uso concreto: sistema de notificação

Desejo que meus componentes possam acionar a exibição de notificações no aplicativo. Mas não quero que meus componentes sejam altamente acoplados ao sistema de notificação que possui suas próprias regras de negócios (no máximo 3 notificações exibidas ao mesmo tempo, enfileiramento de notificações, 4 segundos de exibição etc ...).

Não quero que meus componentes JSX decidam quando uma notificação será exibida / ocultada. Eu apenas lhe dou a capacidade de solicitar uma notificação e deixar as regras complexas dentro da saga. Esse tipo de coisa é bastante difícil de implementar com thunks ou promessas.

notificações

Eu descrevi aqui como isso pode ser feito com saga

Por que é chamado de saga?

O termo saga vem do mundo back-end. Inicialmente, apresentei Yassine (o autor da Redux-saga) a esse termo em uma longa discussão .

Inicialmente, esse termo foi introduzido com um artigo , o padrão saga deveria ser usado para lidar com a consistência eventual nas transações distribuídas, mas seu uso foi estendido a uma definição mais ampla pelos desenvolvedores de back-end, para que agora também cubra o "gerenciador de processos" padrão (de alguma forma, o padrão original da saga é uma forma especializada de gerenciador de processos).

Hoje, o termo "saga" é confuso, pois pode descrever duas coisas diferentes. Como é usado no redux-saga, ele não descreve uma maneira de lidar com transações distribuídas, mas uma maneira de coordenar ações no seu aplicativo. redux-sagatambém poderia ter sido chamado redux-process-manager.

Veja também:

Alternativas

Se você não gosta da idéia de usar geradores, mas está interessado no padrão saga e em suas propriedades de desacoplamento, também pode obter o mesmo com redux-observable, que usa o nome epicpara descrever exatamente o mesmo padrão, mas com o RxJS. Se você já conhece o Rx, vai se sentir em casa.

const loadUserProfileOnNameClickEpic = action$ =>
  action$.ofType('USER_NAME_CLICKED')
    .switchMap(action =>
      Observable.ajax(`http://data.com/${action.payload.userId}`)
        .map(userProfile => ({
          type: 'USER_PROFILE_LOADED',
          userProfile
        }))
        .catch(err => Observable.of({
          type: 'USER_PROFILE_LOAD_FAILED',
          err
        }))
    );

Alguns recursos úteis para redux-saga

2017 aconselha

  • Não use Redux-saga apenas por uma questão de usá-lo. Apenas chamadas de API testáveis ​​não valem a pena.
  • Não remova thunks do seu projeto para os casos mais simples.
  • Não hesite em enviar thunks yield put(someActionThunk)se fizer sentido.

Se você tem medo de usar o Redux-saga (ou Redux-observable), mas precisa apenas do padrão de dissociação, verifique redux-dispatch-subscribe : ele permite ouvir os despachos e acionar novos despachos no ouvinte.

const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});
Sebastien Lorber
fonte
64
Isso está melhorando cada vez que revisito. Considere transformá-lo em uma postagem no blog :).
RainerAtSpirit
4
Obrigado por uma boa redação. No entanto, eu não concordo em certos aspectos. Como o LOAD_USER é imperativo? Para mim, não é apenas declarativo - também fornece um ótimo código legível. Como por exemplo. "Quando pressiono este botão, quero ADD_ITEM". Eu posso olhar o código e entender exatamente o que está acontecendo. Se, em vez disso, fosse chamado algo com o efeito "BUTTON_CLICK", eu teria que procurar isso.
swelet
4
Boa resposta. Agora existe outra alternativa: github.com/blesh/redux-observable
swennemen
4
@ swelet desculpe pelo atraso na resposta. Quando você despacha ADD_ITEM, é imperativo porque você despacha uma ação que visa ter um efeito em sua loja: você espera que a ação faça alguma coisa. Ser declarativo adota a filosofia da fonte de eventos: você não envia ações para acionar alterações em seus aplicativos, mas envia eventos passados ​​para descrever o que aconteceu em seu aplicativo. O envio de um evento deve ser suficiente para considerar que o estado do aplicativo foi alterado. O fato de que há uma loja Redux que reage ao evento é um detalhe de implementação opcional
Sebastien Lorber
3
Não gosto dessa resposta porque ela se distrai da pergunta real para comercializar a própria biblioteca de alguém. Esta resposta fornece uma comparação das duas bibliotecas, que não era a intenção da pergunta. A questão real é perguntar se é necessário usar o middleware, o que é explicado pela resposta aceita.
Abhinav Manchanda
31

A resposta curta : parece uma abordagem totalmente razoável para o problema da assincronia para mim. Com algumas ressalvas.

Eu tinha uma linha de pensamento muito semelhante ao trabalhar em um novo projeto que começamos no meu trabalho. Eu era um grande fã do elegante sistema de baunilha Redux para atualizar a loja e renderizar componentes de uma maneira que fica fora dos ângulos de uma árvore de componentes do React. Pareceu-me estranho conectar-me a esse dispatchmecanismo elegante para lidar com assincronia.

Acabei adotando uma abordagem realmente semelhante à que você tem em uma biblioteca que considerei fora do nosso projeto, que chamamos de react-redux-controller .

Acabei não seguindo a abordagem exata que você tem acima por alguns motivos:

  1. Do jeito que você escreveu, essas funções de despacho não têm acesso à loja. Você pode contornar isso fazendo com que os componentes da interface do usuário passem todas as informações necessárias para a função de despacho. Mas eu argumentaria que isso combina esses componentes da interface do usuário com a lógica de envio desnecessariamente. E, mais problemático, não há maneira óbvia de a função de despacho acessar o estado atualizado nas continuações assíncronas.
  2. As funções de despacho têm acesso a dispatchsi mesmas por meio de escopo lexical. Isso limita as opções de refatoração uma vez que a connectdeclaração fica fora de controle - e ela parece bastante complicada com apenas esse updatemétodo. Portanto, você precisa de algum sistema para permitir que você componha essas funções do despachante se você as dividir em módulos separados.

Em conjunto, é necessário montar algum sistema para permitir que dispatcha loja seja injetada em suas funções de despacho, juntamente com os parâmetros do evento. Conheço três abordagens razoáveis ​​para essa injeção de dependência:

  • O redux-thunk faz isso de uma maneira funcional, passando-os para seus thunks (tornando-os não exatamente thunks, pelas definições de dome). Não trabalhei com outras dispatchabordagens de middleware, mas presumo que elas sejam basicamente as mesmas.
  • O react-redux-controller faz isso com uma corotina. Como um bônus, também fornece acesso aos "seletores", que são as funções pelas quais você pode ter passado como o primeiro argumento connect, em vez de precisar trabalhar diretamente com a loja normalizada bruta.
  • Você também pode fazer isso da maneira orientada a objetos, injetando-os no thiscontexto, através de uma variedade de mecanismos possíveis.

Atualizar

Ocorre-me que parte desse enigma é uma limitação do react-redux . O primeiro argumento para connectobter um instantâneo de estado, mas não despachar. O segundo argumento é despachado, mas não o estado. Nenhum argumento recebe uma conversão que se fecha sobre o estado atual, por poder ver o estado atualizado no momento de uma continuação / retorno de chamada.

acjay
fonte
22

O objetivo de Abramov - e o ideal para todos - é simplesmente encapsular a complexidade (e chamadas assíncronas) no local onde é mais apropriado .

Qual é o melhor lugar para fazer isso no fluxo de dados padrão do Redux? E se:

  • Redutores ? De jeito nenhum. Eles devem ser funções puras, sem efeitos colaterais. Atualizar a loja é um negócio sério e complicado. Não o contamine.
  • Dumb View Components? Definitivamente não. Eles têm uma preocupação: apresentação e interação com o usuário e devem ser o mais simples possível.
  • Componentes de container? Possível, mas abaixo do ideal. Faz sentido que o contêiner seja um local onde encapsulemos alguma complexidade relacionada à exibição e interagimos com a loja, mas:
    • Os contêineres precisam ser mais complexos que os componentes burros, mas ainda é uma responsabilidade única: fornecer ligações entre a exibição e o estado / armazenamento. Sua lógica assíncrona é uma preocupação totalmente separada disso.
    • Ao colocá-lo em um contêiner, você bloqueará sua lógica assíncrona em um único contexto, para uma única exibição / rota. Péssima ideia. Idealmente, é tudo reutilizável e totalmente dissociado.
  • S ome outro módulo de serviço? Má ideia: você precisaria injetar acesso à loja, que é um pesadelo de manutenção / testabilidade. Melhor ir com o grão do Redux e acessar a loja apenas usando as APIs / modelos fornecidos.
  • Ações e Middlewares que as interpretam? Por que não?! Para iniciantes, é a única opção importante que nos resta. :-) Mais logicamente, o sistema de ação é uma lógica de execução desacoplada que você pode usar de qualquer lugar. Tem acesso à loja e pode despachar mais ações. Ele tem uma responsabilidade única: organizar o fluxo de controle e dados em torno do aplicativo, e a maioria das assinaturas se encaixa nesse aspecto.
    • E os criadores de ação? Por que não simplesmente assíncrona lá, em vez de nas próprias ações e no Middleware?
      • Primeiro e mais importante, os criadores não têm acesso à loja, como o middleware. Isso significa que você não pode despachar novas ações contingentes, não pode ler da loja para compor sua assíncrona etc.
      • Portanto, mantenha a complexidade em um local complexo e necessário e mantenha todo o resto simples. Os criadores podem então ser funções simples e relativamente puras, fáceis de testar.
XML
fonte
Componentes para contêineres - por que não? Devido ao papel que os componentes desempenham no React, um contêiner pode atuar como classe de serviço e já recebe uma loja via DI (adereços). Ao colocá-lo em um contêiner, você estaria bloqueando sua lógica assíncrona em um único contexto, para uma única visualização / rota - como assim? Um componente pode ter várias instâncias. Pode ser dissociado da apresentação, por exemplo, com render prop. Eu acho que a resposta poderia se beneficiar ainda mais de pequenos exemplos que provam o ponto.
Estus Flask
Eu amo essa resposta!
Mauricio Avendaño 03/04
13

Para responder à pergunta que é feita no começo:

Por que o componente de contêiner não pode chamar a API assíncrona e despachar as ações?

Lembre-se de que esses documentos são para Redux, não Redux plus React. As lojas Redux conectadas aos componentes React podem fazer exatamente o que você diz, mas uma loja Jane Redux simples sem middleware não aceita argumentos para dispatchexceção de objetos simples e antigos.

Sem o middleware, é claro que você ainda pode

const store = createStore(reducer);
MyAPI.doThing().then(resp => store.dispatch(...));

Mas é um caso semelhante em que a assincronia é envolvida no Redux, e não no Redux. Portanto, o middleware permite assincronia, modificando o que pode ser passado diretamente para dispatch.


Dito isto, o espírito da sua sugestão é, eu acho, válido. Certamente existem outras maneiras de lidar com assincronia em um aplicativo Redux + React.

Um benefício do uso de middleware é que você pode continuar usando os criadores de ação normalmente, sem se preocupar exatamente com como eles estão conectados. Por exemplo, usando redux-thunk, o código que você escreveu seria muito parecido com

function updateThing() {
  return dispatch => {
    dispatch({
      type: ActionTypes.STARTED_UPDATING
    });
    AsyncApi.getFieldValue()
      .then(result => dispatch({
        type: ActionTypes.UPDATED,
        payload: result
      }));
  }
}

const ConnectedApp = connect(
  (state) => { ...state },
  { update: updateThing }
)(App);

que não parece tão diferente do original - é apenas embaralhado um pouco - e connectnão sabe que updateThingé (ou precisa ser) assíncrono.

Se você também quis oferecer suporte a promessas , observáveis , sagas ou criadores de ação loucos e altamente declarativos , então o Redux pode fazer isso apenas alterando o que você passa dispatch(ou seja, o que você retorna dos criadores de ação). Não é necessário estragar os componentes (ou connectchamadas) do React .

Michelle Tilley
fonte
Você aconselha apenas enviar outro evento na conclusão da ação. Isso não funcionará quando você precisar exibir um alerta () após a conclusão da ação. As promessas dentro dos componentes do React funcionam embora. Atualmente, recomendo a abordagem Promessas.
catamphetamine
8

OK, vamos começar a ver como o middleware funciona primeiro, que responde bastante à pergunta, este é o código fonte da função pplyMiddleWare no Redux:

function applyMiddleware() {
  for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
    middlewares[_key] = arguments[_key];
  }

  return function (createStore) {
    return function (reducer, preloadedState, enhancer) {
      var store = createStore(reducer, preloadedState, enhancer);
      var _dispatch = store.dispatch;
      var chain = [];

      var middlewareAPI = {
        getState: store.getState,
        dispatch: function dispatch(action) {
          return _dispatch(action);
        }
      };
      chain = middlewares.map(function (middleware) {
        return middleware(middlewareAPI);
      });
      _dispatch = compose.apply(undefined, chain)(store.dispatch);

      return _extends({}, store, {
        dispatch: _dispatch
      });
    };
  };
}

Veja esta parte, veja como nosso despacho se torna uma função .

  ...
  getState: store.getState,
  dispatch: function dispatch(action) {
  return _dispatch(action);
}
  • Observe que cada middleware receberá dispatche getStatefuncionará como argumentos nomeados.

OK, é assim que o Redux-thunk como um dos middlewares mais usados ​​para o Redux se apresenta:

O middleware Redux Thunk permite escrever criadores de ação que retornam uma função em vez de uma ação. O thunk pode ser usado para atrasar o envio de uma ação ou para enviar somente se uma determinada condição for atendida. A função interna recebe os métodos de armazenamento expedidos e getState como parâmetros.

Então, como você vê, ele retornará uma função em vez de uma ação, significa que você pode esperar e chamá-lo sempre que quiser, pois é uma função ...

Então, o que diabos é thunk? É assim que é introduzido na Wikipedia:

Na programação de computadores, uma conversão é uma sub-rotina usada para injetar um cálculo adicional em outra sub-rotina. Os thunks são usados ​​principalmente para atrasar um cálculo até que seja necessário ou para inserir operações no início ou no final da outra sub-rotina. Eles têm uma variedade de outros aplicativos para a geração de código do compilador e na programação modular.

O termo se originou como um derivado jocular de "pensar".

Uma conversão é uma função que envolve uma expressão para atrasar sua avaliação.

//calculation of 1 + 2 is immediate 
//x === 3 
let x = 1 + 2;

//calculation of 1 + 2 is delayed 
//foo can be called later to perform the calculation 
//foo is a thunk! 
let foo = () => 1 + 2;

Veja como é fácil o conceito e como ele pode ajudá-lo a gerenciar suas ações assíncronas ...

É algo que você pode viver sem ele, mas lembre-se de que na programação sempre há maneiras melhores, mais organizadas e adequadas de fazer as coisas ...

Aplicar middleware Redux

Alireza
fonte
1
Primeira vez no SO, não leu nada. Mas gostei do post olhando a foto. Incrível, dica e lembrete.
Bhojendra Rauniyar
2

Usar o Redux-saga é o melhor middleware na implementação do React-redux.

Ex: store.js

  import createSagaMiddleware from 'redux-saga';
  import { createStore, applyMiddleware } from 'redux';
  import allReducer from '../reducer/allReducer';
  import rootSaga from '../saga';

  const sagaMiddleware = createSagaMiddleware();
  const store = createStore(
     allReducer,
     applyMiddleware(sagaMiddleware)
   )

   sagaMiddleware.run(rootSaga);

 export default store;

E então saga.js

import {takeLatest,delay} from 'redux-saga';
import {call, put, take, select} from 'redux-saga/effects';
import { push } from 'react-router-redux';
import data from './data.json';

export function* updateLesson(){
   try{
       yield put({type:'INITIAL_DATA',payload:data}) // initial data from json
       yield* takeLatest('UPDATE_DETAIL',updateDetail) // listen to your action.js 
   }
   catch(e){
      console.log("error",e)
     }
  }

export function* updateDetail(action) {
  try{
       //To write store update details
   }  
    catch(e){
       console.log("error",e)
    } 
 }

export default function* rootSaga(){
    yield [
        updateLesson()
       ]
    }

E então action.js

 export default function updateFruit(props,fruit) {
    return (
       {
         type:"UPDATE_DETAIL",
         payload:fruit,
         props:props
       }
     )
  }

E então reducer.js

import {combineReducers} from 'redux';

const fetchInitialData = (state=[],action) => {
    switch(action.type){
      case "INITIAL_DATA":
          return ({type:action.type, payload:action.payload});
          break;
      }
     return state;
  }
 const updateDetailsData = (state=[],action) => {
    switch(action.type){
      case "INITIAL_DATA":
          return ({type:action.type, payload:action.payload});
          break;
      }
     return state;
  }
const allReducers =combineReducers({
   data:fetchInitialData,
   updateDetailsData
 })
export default allReducers; 

E então main.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app/components/App.jsx';
import {Provider} from 'react-redux';
import store from './app/store';
import createRoutes from './app/routes';

const initialState = {};
const store = configureStore(initialState, browserHistory);

ReactDOM.render(
       <Provider store={store}>
          <App />  /*is your Component*/
       </Provider>, 
document.getElementById('app'));

tente isso .. está funcionando

SM Chinna
fonte
3
Isso é algo sério para alguém que apenas deseja chamar um ponto de extremidade da API para retornar uma entidade ou lista de entidades. Você recomenda, "apenas faça isso ... então isso, então isso, então essa outra coisa, então aquilo, então essas outras coisas, depois continue, e faça ..". Mas, cara, isso é FRONTEND, só precisamos chamar o BACKEND para nos fornecer dados prontos para serem usados ​​no frontend. Se este é o caminho a percorrer, algo está errado, algo está muito errado e alguém não está aplicando BEIJO hoje em dia
zameb
Olá, use o bloco try and catch para as chamadas de API. Depois que a API fornecer a resposta, chame os tipos de ação Redutor.
SM Chinna
1
@zameb Você pode estar certo, mas sua reclamação é com o próprio Redux e tudo o que ele ouve enquanto tenta reduzir a complexidade.
jorisw 26/04
1

Existem criadores de ações síncronas e, em seguida, criadores de ações assíncronas.

Um criador de ação síncrona é aquele que, quando o chamamos, retorna imediatamente um objeto Action com todos os dados relevantes anexados a esse objeto e está pronto para ser processado por nossos redutores.

Os criadores de ação assíncrona são aqueles em que será necessário um pouco de tempo antes de estar pronto para enviar uma ação.

Por definição, sempre que você tiver um criador de ações que faça uma solicitação de rede, ele sempre será qualificado como criador de ações assíncronas.

Se você deseja ter criadores de ação assíncronos dentro de um aplicativo Redux, é necessário instalar algo chamado middleware que permitirá que você lide com esses criadores de ação assíncronos.

Você pode verificar isso na mensagem de erro que indica o uso de middleware personalizado para ações assíncronas.

Então, o que é um middleware e por que precisamos dele para fluxo assíncrono no Redux?

No contexto do middleware redux, como o redux-thunk, um middleware nos ajuda a lidar com criadores de ação assíncronos, pois é algo que o Redux não pode lidar imediatamente.

Com um middleware integrado ao ciclo Redux, ainda estamos chamando criadores de ação, que retornará uma ação que será despachada, mas agora quando despacharmos uma ação, em vez de enviá-la diretamente para todos os nossos redutores, iremos dizer que uma ação será enviada através de todos os diferentes middlewares dentro do aplicativo.

Dentro de um único aplicativo Redux, podemos ter o middleware que desejar. Na maioria das vezes, nos projetos em que trabalhamos, teremos um ou dois middlewares conectados à nossa loja Redux.

Um middleware é uma função JavaScript simples que será chamada a cada ação que despacharmos. Dentro dessa função, um middleware tem a oportunidade de impedir que uma ação seja despachada para qualquer um dos redutores, ele pode modificar uma ação ou apenas mexer com uma ação de qualquer maneira que você, por exemplo, poderíamos criar um middleware que console logs cada ação que você despacha apenas para o seu prazer.

Há um número tremendo de middleware de código aberto que você pode instalar como dependências no seu projeto.

Você não está limitado apenas a usar o middleware de código aberto ou a instalá-lo como dependências. Você pode escrever seu próprio middleware personalizado e usá-lo dentro da sua loja Redux.

Um dos usos mais populares do middleware (e chegando à sua resposta) é para lidar com criadores de ação assíncronos, provavelmente o middleware mais popular existente no mercado é o redux-thunk e trata-se de ajudá-lo a lidar com criadores de ação assíncronos.

Existem muitos outros tipos de middleware que também ajudam a lidar com criadores de ações assíncronas.

Daniel
fonte
1

Para responder à pergunta:

Por que o componente de contêiner não pode chamar a API assíncrona e despachar as ações?

Eu diria por pelo menos duas razões:

A primeira razão é a separação de preocupações, não é tarefa do action creatorchamar apie recuperar dados, você precisa passar dois argumentos para o seu action creator function, action typeoe um payload.

A segunda razão é porque redux storeestá aguardando um objeto simples com o tipo de ação obrigatória e, opcionalmente, a payload(mas aqui você também deve passar a carga útil).

O criador da ação deve ser um objeto simples, como abaixo:

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

E o trabalho de Redux-Thunk midlewarepara dispacheo resultado do seu api callpara o apropriado action.

Mselmi Ali
fonte
0

Ao trabalhar em um projeto corporativo, existem muitos requisitos disponíveis no middleware, como (saga), não disponíveis no fluxo assíncrono simples, abaixo estão alguns:

  • Executando solicitação em paralelo
  • Realizando ações futuras sem a necessidade de esperar
  • Chamadas sem bloqueio Efeito de corrida, primeiro exemplo de captura
  • resposta para iniciar o processo Sequenciar suas tarefas (primeiro na primeira chamada)
  • Composição
  • Cancelamento da tarefa Bifurcar dinamicamente a tarefa.
  • Suporte à simultaneidade executando o Saga fora do middleware redux.
  • Usando canais

A lista é longa, basta revisar a seção avançada na documentação da saga

Feras
fonte