Prós / contras do uso de redux-saga com geradores ES6 vs redux-thunk com ES2017 assíncrono / aguardar


488

Há muita conversa sobre o garoto mais recente da cidade de redux agora, redux-saga / redux-saga . Ele usa funções de gerador para ouvir / despachar ações.

Antes de entender, gostaria de saber os prós / contras do uso, em redux-sagavez da abordagem abaixo, na qual estou usando o redux-thunkasync / wait.

Um componente pode se parecer com isso, despachar ações como de costume.

import { login } from 'redux/auth';

class LoginForm extends Component {

  onClick(e) {
    e.preventDefault();
    const { user, pass } = this.refs;
    this.props.dispatch(login(user.value, pass.value));
  }

  render() {
    return (<div>
        <input type="text" ref="user" />
        <input type="password" ref="pass" />
        <button onClick={::this.onClick}>Sign In</button>
    </div>);
  } 
}

export default connect((state) => ({}))(LoginForm);

Então, minhas ações são mais ou menos assim:

// auth.js

import request from 'axios';
import { loadUserData } from './user';

// define constants
// define initial state
// export default reducer

export const login = (user, pass) => async (dispatch) => {
    try {
        dispatch({ type: LOGIN_REQUEST });
        let { data } = await request.post('/login', { user, pass });
        await dispatch(loadUserData(data.uid));
        dispatch({ type: LOGIN_SUCCESS, data });
    } catch(error) {
        dispatch({ type: LOGIN_ERROR, error });
    }
}

// more actions...

// user.js

import request from 'axios';

// define constants
// define initial state
// export default reducer

export const loadUserData = (uid) => async (dispatch) => {
    try {
        dispatch({ type: USERDATA_REQUEST });
        let { data } = await request.get(`/users/${uid}`);
        dispatch({ type: USERDATA_SUCCESS, data });
    } catch(error) {
        dispatch({ type: USERDATA_ERROR, error });
    }
}

// more actions...

6
Veja também a minha resposta comparando redux-thunk e redux-saga aqui: stackoverflow.com/a/34623840/82609 #
Sebastien Lorber

22
Qual é o ::antes de você this.onClickfazer?
21416 Downhillski

37
@ZhenyangHua é uma abreviação para vincular a função ao objeto ( this), também conhecido como this.onClick = this.onClick.bind(this). A forma mais longa é geralmente recomendada no construtor, pois a mão curta é ligada novamente a cada renderização.
hampusohlsson

7
Eu vejo. obrigado! Vejo pessoas usando bind()muito para passar thispara a função, mas comecei a usar () => method()agora.
Downhillski

2
@Hosar eu usei redux & redux-saga em produção por um tempo, mas, na verdade, migrou para MobX depois de alguns meses, porque menos sobrecarga
hampusohlsson

Respostas:


461

Na redux-saga, o equivalente ao exemplo acima seria

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN_REQUEST)
    try {
      let { data } = yield call(request.post, '/login', { user, pass });
      yield fork(loadUserData, data.uid);
      yield put({ type: LOGIN_SUCCESS, data });
    } catch(error) {
      yield put({ type: LOGIN_ERROR, error });
    }  
  }
}

export function* loadUserData(uid) {
  try {
    yield put({ type: USERDATA_REQUEST });
    let { data } = yield call(request.get, `/users/${uid}`);
    yield put({ type: USERDATA_SUCCESS, data });
  } catch(error) {
    yield put({ type: USERDATA_ERROR, error });
  }
}

A primeira coisa a notar é que nós estamos chamando as funções da API utilizando o formulário yield call(func, ...args). callnão executa o efeito, apenas cria um objeto simples como {type: 'CALL', func, args}. A execução é delegada ao middleware redux-saga, que cuida da execução da função e da retomada do gerador com seu resultado.

A principal vantagem é que você pode testar o gerador fora do Redux usando simples verificações de igualdade

const iterator = loginSaga()

assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))

// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
  iterator.next(mockAction).value, 
  call(request.post, '/login', mockAction)
)

// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
  iterator.throw(mockError).value, 
  put({ type: LOGIN_ERROR, error: mockError })
)

Observe que estamos simulando o resultado da chamada da API simplesmente injetando os dados simulados no nextmétodo do iterador. Zombar de dados é muito mais simples que funções de zombaria.

A segunda coisa a notar é a chamada para yield take(ACTION). Thunks são chamados pelo criador da ação em cada nova ação (por exemplo LOGIN_REQUEST). ou seja, as ações são continuamente enviadas para thunks, e os thunks não têm controle sobre quando parar de manipular essas ações.

Na redux-saga, os geradores executam a próxima ação. isto é, eles têm controle sobre quando ouvir alguma ação e quando não. No exemplo acima, as instruções de fluxo são colocadas dentro de um while(true)loop, para que ele escute cada ação recebida, o que imita um pouco o comportamento de empurrar a thunk.

A abordagem pull permite a implementação de fluxos de controle complexos. Suponha, por exemplo, que desejemos adicionar os seguintes requisitos

  • Lidar com a ação do usuário LOGOUT

  • após o primeiro login bem-sucedido, o servidor retorna um token que expira em algum atraso armazenado em um expires_incampo. Teremos que atualizar a autorização em segundo plano a cada expires_inmilissegundo

  • Leve em consideração que, ao aguardar o resultado das chamadas da API (login inicial ou atualização), o usuário poderá efetuar logout no meio.

Como você implementaria isso com thunks; enquanto também fornece cobertura de teste completa para todo o fluxo? Aqui está como isso pode parecer com Sagas:

function* authorize(credentials) {
  const token = yield call(api.authorize, credentials)
  yield put( login.success(token) )
  return token
}

function* authAndRefreshTokenOnExpiry(name, password) {
  let token = yield call(authorize, {name, password})
  while(true) {
    yield call(delay, token.expires_in)
    token = yield call(authorize, {token})
  }
}

function* watchAuth() {
  while(true) {
    try {
      const {name, password} = yield take(LOGIN_REQUEST)

      yield race([
        take(LOGOUT),
        call(authAndRefreshTokenOnExpiry, name, password)
      ])

      // user logged out, next while iteration will wait for the
      // next LOGIN_REQUEST action

    } catch(error) {
      yield put( login.error(error) )
    }
  }
}

No exemplo acima, estamos expressando nosso requisito de simultaneidade usando race. Se take(LOGOUT)vencer a corrida (ou seja, o usuário clicou no botão Logout). A corrida cancelará automaticamente a authAndRefreshTokenOnExpirytarefa em segundo plano. E se o authAndRefreshTokenOnExpiryfoi bloqueado no meio de uma call(authorize, {token})chamada, também será cancelado. O cancelamento se propaga para baixo automaticamente.

Você pode encontrar uma demonstração executável do fluxo acima


@yassine De onde vem a delayfunção? Ah, encontrei: github.com/yelouafi/redux-saga/blob/…
philk 2/16

122
O redux-thunkcódigo é bastante legível e auto-explicado. Mas redux-sagasé realmente ilegível, principalmente por causa daqueles verbo-como funções: call, fork, take, put...
syg

11
@ syy, eu concordo que call, fork, take e put podem ser mais semanticamente amigáveis. No entanto, são essas funções verbais que tornam todos os efeitos colaterais testáveis.
21416 Downhillski

3
@syg ainda uma função com esses verbos estranhas funções são mais legíveis do que uma função com corrente promessas profundas
Yasser Sinjab

3
esses verbos "estranhos" também ajudam a conceituar o relacionamento da saga com as mensagens que saem do redux. você pode tirar os tipos de mensagem do redux - geralmente para acionar a próxima iteração e inserir novas mensagens para transmitir o resultado do seu efeito colateral.
WORC

104

Acrescentarei minha experiência usando saga no sistema de produção, além da resposta bastante completa do autor da biblioteca.

Pro (usando saga):

  • Testabilidade. É muito fácil testar sagas, pois call () retorna um objeto puro. Testar thunks normalmente exige que você inclua uma mockStore em seu teste.

  • O redux-saga vem com muitas funções auxiliares úteis sobre tarefas. Parece-me que o conceito de saga é criar algum tipo de trabalhador / thread em segundo plano para o seu aplicativo, que atua como uma peça que falta na arquitetura de redux de reação (actionCreators e reducers devem ser funções puras.) O que leva ao próximo ponto.

  • Sagas oferecem lugar independente para lidar com todos os efeitos colaterais. Geralmente, é mais fácil modificar e gerenciar do que ações thunk na minha experiência.

Vigarista:

  • Sintaxe do gerador.

  • Muitos conceitos para aprender.

  • Estabilidade da API. Parece que a redux-saga ainda está adicionando recursos (por exemplo, canais?) E a comunidade não é tão grande. Existe uma preocupação se a biblioteca fizer uma atualização não compatível com versões anteriores algum dia.


9
Só quero fazer algum comentário, o criador da ação não precisa ser uma função pura, o que já foi reivindicado pelo próprio Dan muitas vezes.
Marson Mao

14
A partir de agora, as redux-sagas são muito recomendadas, pois o uso e a comunidade se expandiram. Além disso, a API se tornou mais madura. Considere remover o Con API stabilitycomo uma atualização para refletir a situação atual.
Denialos 13/09/17

1
saga tem mais partidas do que conversão e seu último commit é após conversão demasiado
amorenew

2
Sim, FWIW redux-saga agora tem 12k estrelas, redux-thunk tem 8k
Brian Burns

3
Vou acrescentar outro desafio das sagas: as sagas são totalmente dissociadas das ações e criadores de ações por padrão. Enquanto Thunks conecta diretamente criadores de ação com seus efeitos colaterais, as sagas deixam os criadores de ação totalmente separados das sagas que os ouvem. Isso tem vantagens técnicas, mas pode tornar o código muito mais difícil de seguir e pode desfocar alguns dos conceitos unidirecionais.
theaceofthespade

33

Gostaria apenas de adicionar alguns comentários da minha experiência pessoal (usando sagas e thunk):

As sagas são ótimas para testar:

  • Você não precisa zombar de funções envolvidas com efeitos
  • Portanto, os testes são limpos, legíveis e fáceis de escrever
  • Ao usar sagas, os criadores de ação geralmente retornam literais de objeto simples. Também é mais fácil testar e afirmar ao contrário das promessas de thunk.

As sagas são mais poderosas. Tudo o que você pode fazer no criador de ações de um thunk também pode fazer em uma saga, mas não vice-versa (ou pelo menos não facilmente). Por exemplo:

  • aguarde uma ação / ações serem despachadas ( take)
  • cancelar rotina existente ( cancel, takeLatest, race)
  • várias rotinas pode ouvir a mesma ação ( take, takeEvery...)

O Sagas também oferece outras funcionalidades úteis, que generalizam alguns padrões de aplicativos comuns:

  • channels para ouvir em fontes externas de eventos (por exemplo, websockets)
  • modelo de garfo ( fork, spawn)
  • acelerador
  • ...

Sagas são uma ferramenta grande e poderosa. No entanto, com o poder vem a responsabilidade. Quando seu aplicativo cresce, você pode se perder facilmente, descobrindo quem está esperando a ação ser despachada ou o que acontece quando uma ação está sendo despachada. Por outro lado, o thunk é mais simples e mais fácil de raciocinar. A escolha de um ou de outro depende de muitos aspectos, como tipo e tamanho do projeto, que tipos de efeito colateral seu projeto deve lidar ou preferência da equipe de desenvolvimento. De qualquer forma, basta manter seu aplicativo simples e previsível.


8

Apenas alguma experiência pessoal:

  1. Para estilo de codificação e legibilidade, uma das vantagens mais significativas do uso de redux-saga no passado é evitar o inferno de retorno de chamada em redux-thunk - não é necessário usar muitos aninhamentos e / ou capturar mais. Mas agora, com a popularidade do async / waiting thunk, também se pode escrever código assíncrono no estilo de sincronização ao usar o redux-thunk, o que pode ser considerado uma melhoria no redux-think.

  2. Pode ser necessário escrever muito mais código padrão ao usar o redux-saga, especialmente no Typescript. Por exemplo, se alguém quiser implementar uma função de busca assíncrona, o tratamento de dados e erros pode ser realizado diretamente em uma unidade de thunk no action.js com uma única ação FETCH. Mas, no redux-saga, pode ser necessário definir as ações FETCH_START, FETCH_SUCCESS e FETCH_FAILURE e todas as verificações de tipo relacionadas, porque um dos recursos do redux-saga é usar esse tipo de mecanismo rico de "token" para criar efeitos e instruir redux store para testes fáceis. É claro que se poderia escrever uma saga sem usar essas ações, mas isso a tornaria semelhante a um thunk.

  3. Em termos de estrutura de arquivos, o redux-saga parece ser mais explícito em muitos casos. Pode-se encontrar facilmente um código relacionado à assíncrona em todos os sagas.ts, mas no redux-thunk, seria necessário vê-lo em ações.

  4. Testes fáceis podem ser outro recurso ponderado na redux-saga. Isso é realmente conveniente. Mas uma coisa que precisa ser esclarecida é que o teste de chamada de redux-saga não executaria a chamada de API real no teste; portanto, seria necessário especificar o resultado da amostra para as etapas que podem ser usadas após a chamada de API. Portanto, antes de escrever em redux-saga, seria melhor planejar uma saga e suas sagas.spec.ts correspondentes em detalhes.

  5. O Redux-saga também oferece muitos recursos avançados, como executar tarefas em paralelo, auxiliares de simultaneidade como takeLatest / takeEvery, fork / spawn, que são muito mais poderosos que os thunks.

Concluindo, pessoalmente, eu gostaria de dizer: em muitos casos normais e aplicativos de tamanho pequeno a médio, use o estilo assíncrono / aguardado redux-thunk. Isso pouparia muitos códigos / ações / typedefs padrão, e você não precisaria trocar muitos sagas.ts diferentes e manter uma árvore de sagas específica. Mas se você estiver desenvolvendo um aplicativo grande com lógica assíncrona muito complexa e a necessidade de recursos como padrão de simultaneidade / paralelo, ou tiver uma alta demanda por testes e manutenção (especialmente no desenvolvimento orientado a testes), o redux-sagas possivelmente salvará sua vida .

De qualquer forma, o redux-saga não é mais difícil e complexo do que o próprio redux, e não possui uma chamada curva de aprendizado íngreme porque possui conceitos e APIs bem limitados. Passar um pouco de tempo aprendendo redux-saga pode se beneficiar um dia no futuro.


5

Tendo analisado alguns projetos React / Redux diferentes em larga escala, na minha experiência, o Sagas fornece aos desenvolvedores uma maneira mais estruturada de escrever código, que é muito mais fácil de testar e mais difícil de errar.

Sim, é um pouco estranho para começar, mas a maioria dos desenvolvedores entende o suficiente em um dia. Eu sempre digo às pessoas para não se preocuparem com o que yieldfazer para começar e que, depois de escrever alguns testes, ele chegará até você.

Eu já vi alguns projetos em que os thunks foram tratados como se fossem controladores do MVC patten e isso rapidamente se tornou uma bagunça insustentável.

Meu conselho é usar Sagas, onde você precisa de A dispara coisas do tipo B relacionadas a um único evento. Para qualquer coisa que possa abranger várias ações, acho mais simples escrever o middleware do cliente e usar a propriedade meta de uma ação da FSA para acioná-lo.


2

Thunks versus Sagas

Redux-Thunke Redux-Sagadiferem em algumas maneiras importantes, ambas são bibliotecas de middleware para Redux (o middleware Redux é um código que intercepta ações que entram na loja por meio do método dispatch ()).

Uma ação pode ser literalmente qualquer coisa, mas se você estiver seguindo as práticas recomendadas, uma ação é um objeto javascript simples com um campo de tipo e campos opcionais de carga útil, meta e erro. por exemplo

const loginRequest = {
    type: 'LOGIN_REQUEST',
    payload: {
        name: 'admin',
        password: '123',
    }, };

Redux-Thunk

Além de despachar ações padrão, o Redux-Thunkmiddleware permite despachar funções especiais, chamadas thunks.

Thunks (no Redux) geralmente têm a seguinte estrutura:

export const thunkName =
   parameters =>
        (dispatch, getState) => {
            // Your application logic goes here
        };

Ou seja, a thunké uma função que (opcionalmente) pega alguns parâmetros e retorna outra função. A função interna leva uma dispatch functione uma getStatefunção - ambos os quais serão fornecidos pelo Redux-Thunkmiddleware.

Redux-Saga

Redux-SagaO middleware permite que você expresse lógicas complexas de aplicativos como funções puras chamadas sagas. As funções puras são desejáveis ​​do ponto de vista de teste porque são previsíveis e repetíveis, o que as torna relativamente fáceis de testar.

As sagas são implementadas através de funções especiais chamadas funções de gerador. Estes são um novo recurso do ES6 JavaScript. Basicamente, a execução entra e sai de um gerador em todos os lugares em que você vê uma declaração de rendimento. Pense em uma yielddeclaração como fazendo com que o gerador faça uma pausa e retorne o valor gerado. Posteriormente, o chamador pode retomar o gerador na declaração a seguir a yield.

Uma função geradora é definida como esta. Observe o asterisco após a palavra-chave da função.

function* mySaga() {
    // ...
}

Uma vez que a saga de login é registrada Redux-Saga. Mas então a yieldtomada na primeira linha pausará a saga até que uma ação do tipo 'LOGIN_REQUEST'seja despachada para a loja. Quando isso acontecer, a execução continuará.

Para mais detalhes, consulte este artigo .


1

Uma nota rápida. Os geradores são canceláveis, assíncronos / aguardam - não. Portanto, para um exemplo da pergunta, realmente não faz sentido o que escolher. Mas, para fluxos mais complicados, às vezes não há solução melhor do que usar geradores.

Então, outra idéia poderia ser é usar geradores com redux-thunk, mas, para mim, parece como tentar inventar uma bicicleta com rodas quadradas.

E, é claro, os geradores são mais fáceis de testar.


0

Aqui está um projeto que combina as melhores partes (profissionais) de ambos redux-sagae redux-thunk: você pode lidar com todos os efeitos colaterais das sagas enquanto recebe uma promessa pela dispatchingação correspondente: https://github.com/diegohaz/redux-saga-thunk

class MyComponent extends React.Component {
  componentWillMount() {
    // `doSomething` dispatches an action which is handled by some saga
    this.props.doSomething().then((detail) => {
      console.log('Yaay!', detail)
    }).catch((error) => {
      console.log('Oops!', error)
    })
  }
}

1
usar then()dentro de um componente React é contra o paradigma. Você deve lidar com o estado alterado em componentDidUpdatevez de esperar que uma promessa seja resolvida.

3
@ Maxincredible52 Não é verdade para a renderização no servidor.
Diego Haz

Na minha experiência, o argumento de Max ainda é verdadeiro para a renderização no servidor. Provavelmente, isso deve ser tratado em algum lugar da camada de roteamento.
ThinkingInBits

3
@ Maxincredible52 Por que é contra o paradigma, onde você leu isso? Eu costumo fazer semelhante ao @Diego Haz, mas fazê-lo em componentDidMount (como por Reagir docs, chamadas de rede deve preferível ser feito lá) por isso temoscomponentDidlMount() { this.props.doSomething().then((detail) => { this.setState({isReady: true})} }
user3711421

0

Uma maneira mais fácil é usar o redux-auto .

da documantasion

O redux-auto corrigiu esse problema assíncrono simplesmente permitindo que você crie uma função de "ação" que retorna uma promessa. Para acompanhar sua lógica de ação da função "padrão".

  1. Não há necessidade de outro middleware assíncrono do Redux. por exemplo, thunk, middleware de promessa, saga
  2. Facilmente permite que você passe uma promessa para o redux e faça com que ela seja gerenciada por você
  3. Permite co-localizar chamadas de serviço externas com onde elas serão transformadas
  4. Nomear o arquivo "init.js" o chamará uma vez no início do aplicativo. Isso é bom para carregar dados do servidor no início

A idéia é ter cada ação em um arquivo específico . co-localizando a chamada do servidor no arquivo com funções redutoras para "pendente", "cumprida" e "rejeitada". Isso facilita muito o manuseio das promessas.

Também anexa automaticamente um objeto auxiliar (chamado "assíncrono") ao protótipo do seu estado, permitindo rastrear na sua interface do usuário as transições solicitadas.


2
Fiz um mesmo é resposta irrelevante porque diferentes soluções devem ser consideradas também
amorenew

12
Eu acho que os -'s estão lá porque ele não revelou que ele é o autor do projeto
jreptak
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.