Pros / contras de usar redux-saga con generadores ES6 versus redux-thunk con ES2017 async / await


488

Se habla mucho sobre el último niño en la ciudad de redux en este momento, redux-saga / redux-saga . Utiliza funciones generadoras para escuchar / despachar acciones.

Antes de comprenderlo, me gustaría saber las ventajas y desventajas de usar en redux-sagalugar del siguiente enfoque donde estoy usando redux-thunkasync / wait.

Un componente podría verse así, despachar acciones como de costumbre.

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);

Entonces mis acciones se parecen a esto:

// 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...

66
Vea también mi respuesta comparando redux-thunk con redux-saga aquí: stackoverflow.com/a/34623840/82609
Sebastien Lorber el

22
¿Cuál es el ::antes de this.onClickhacer?
Downhillski

37
@ZhenyangHua es una abreviatura para vincular la función al objeto ( this), también conocido como this.onClick = this.onClick.bind(this). La forma más larga generalmente se recomienda hacer en el constructor, ya que la mano corta se vuelve a unir en cada render.
hampusohlsson

77
Veo. ¡Gracias! Veo gente que usa bind()mucho para pasar thisa la función, pero comencé a usar () => method()ahora.
Downhillski

2
@Hosar Usé redux & redux-saga en producción durante un tiempo, pero en realidad migré a MobX después de un par de meses porque menos gastos generales
hampusohlsson

Respuestas:


461

En redux-saga, el equivalente del ejemplo anterior sería

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 });
  }
}

Lo primero que debe notar es que estamos llamando a las funciones de la API usando el formulario yield call(func, ...args). callno ejecuta el efecto, solo crea un objeto simple como {type: 'CALL', func, args}. La ejecución se delega al middleware redux-saga que se encarga de ejecutar la función y reanudar el generador con su resultado.

La principal ventaja es que puede probar el generador fuera de Redux utilizando simples comprobaciones de igualdad

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 })
)

Tenga en cuenta que nos estamos burlando del resultado de la llamada api simplemente inyectando los datos simulados en el nextmétodo del iterador. Los datos de burla son mucho más simples que las funciones de burla.

La segunda cosa a notar es la llamada a yield take(ACTION). Thunks son llamados por el creador de la acción en cada nueva acción (por ejemplo LOGIN_REQUEST). es decir, las acciones son continuamente empujados a procesadores y procesadores no tienen ningún control sobre cuándo dejar de manejar esas acciones.

En redux-saga, generadores de tirar de la siguiente acción. es decir, tienen control sobre cuándo escuchar alguna acción y cuándo no. En el ejemplo anterior, las instrucciones de flujo se colocan dentro de un while(true)bucle, por lo que escuchará cada acción entrante, que de alguna manera imita el comportamiento de empuje de golpe.

El enfoque de extracción permite implementar flujos de control complejos. Supongamos, por ejemplo, que queremos agregar los siguientes requisitos

  • Manejar la acción del usuario LOGOUT

  • Tras el primer inicio de sesión exitoso, el servidor devuelve un token que caduca con algún retraso almacenado en un expires_incampo. Tendremos que actualizar la autorización en segundo plano cada expires_inmilisegundos

  • Tenga en cuenta que al esperar el resultado de las llamadas API (ya sea inicio de sesión inicial o actualización), el usuario puede cerrar sesión en el medio.

¿Cómo implementaría eso con thunks; mientras que también proporciona cobertura de prueba completa para todo el flujo? Así es como puede verse con 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) )
    }
  }
}

En el ejemplo anterior, estamos expresando nuestro requisito de concurrencia usando race. Si take(LOGOUT)gana la carrera (es decir, el usuario hizo clic en el botón Cerrar sesión). La carrera cancelará automáticamente la authAndRefreshTokenOnExpirytarea en segundo plano. Y si authAndRefreshTokenOnExpiryse bloqueó en medio de una call(authorize, {token})llamada, también se cancelará. La cancelación se propaga hacia abajo automáticamente.

Puede encontrar una demostración ejecutable del flujo anterior


@yassine, ¿de dónde viene la delayfunción? Ah, lo encontré: github.com/yelouafi/redux-saga/blob/…
philk

122
El redux-thunkcódigo es bastante legible y se explica por sí mismo. Pero redux-sagasuno es realmente ilegible, principalmente a causa de los verbo-como funciones: call, fork, take, put...
SYG

11
@syg, estoy de acuerdo en que call, fork, take y put pueden ser más amigables semánticamente. Sin embargo, son esas funciones verbales las que hacen comprobables todos los efectos secundarios.
Downhillski

3
@syg sigue siendo una función con esos verbos raros, las funciones son más legibles que una función con una cadena de promesas profundas
Yasser Sinjab

3
esos verbos "extraños" también te ayudan a conceptualizar la relación de la saga con los mensajes que salen de redux. puede quitar los tipos de mensajes de redux, a menudo para activar la próxima iteración, y puede volver a colocar mensajes nuevos para transmitir el resultado de su efecto secundario.
worc

104

Agregaré mi experiencia usando saga en el sistema de producción, además de la respuesta bastante detallada del autor de la biblioteca.

Pro (usando saga):

  • Testabilidad Es muy fácil probar sagas ya que call () devuelve un objeto puro. La prueba de thunks normalmente requiere que incluyas un mockStore dentro de tu prueba.

  • redux-saga viene con muchas funciones útiles de ayuda sobre tareas. Me parece que el concepto de saga es crear algún tipo de trabajador / hilo de fondo para su aplicación, que actúe como una pieza faltante en la arquitectura react redux (actionCreators and reducers deben ser funciones puras). Lo que lleva al siguiente punto.

  • Las sagas ofrecen un lugar independiente para manejar todos los efectos secundarios. Por lo general, en mi experiencia, es más fácil modificar y administrar que las acciones thunk.

Estafa:

  • Sintaxis del generador.

  • Muchos conceptos para aprender.

  • Estabilidad API. Parece que redux-saga todavía está agregando características (¿canales?) Y la comunidad no es tan grande. Existe una preocupación si la biblioteca realiza una actualización no compatible con versiones anteriores algún día.


99
Solo quiero hacer algún comentario, el creador de la acción no necesita ser pura función, lo cual ha sido afirmado por el propio Dan muchas veces.
Marson Mao

14
A partir de ahora, las sagas redux son muy recomendadas a medida que el uso y la comunidad se han expandido. Además, la API se ha vuelto más madura. Considere eliminar la Con para API stabilityuna actualización para reflejar la situación actual.
Denialos

1
la saga tiene más comienzos que thunk y su último commit también es posterior a thunk
amorenew

2
Sí, FWIW redux-saga ahora tiene 12k estrellas, redux-thunk tiene 8k
Brian Burns

3
Voy a agregar otro desafío de sagas, es que las sagas están completamente desacopladas de las acciones y creadores de acciones por defecto. Mientras que Thunks conecta directamente a los creadores de acción con sus efectos secundarios, las sagas dejan a los creadores de acción totalmente separados de las sagas que los escuchan. Esto tiene ventajas técnicas, pero puede hacer que el código sea mucho más difícil de seguir y puede difuminar algunos de los conceptos unidireccionales.
theaceofthespade

33

Solo me gustaría agregar algunos comentarios de mi experiencia personal (usando tanto sagas como thunk):

Las sagas son geniales para probar:

  • No necesitas burlarte de las funciones envueltas en efectos
  • Por lo tanto, las pruebas son limpias, legibles y fáciles de escribir.
  • Cuando se usan sagas, los creadores de acciones en su mayoría devuelven literales de objetos simples. También es más fácil probar y afirmar a diferencia de las promesas de thunk.

Las sagas son más poderosas. Todo lo que puedes hacer en el creador de acción de un thunk también puedes hacerlo en una saga, pero no al revés (o al menos no fácilmente). Por ejemplo:

  • esperar a que se envíe una acción / acciones ( take)
  • cancelar rutina existente ( cancel, takeLatest, race)
  • múltiples rutinas pueden escuchar la misma acción ( take, takeEvery, ...)

Sagas también ofrece otra funcionalidad útil, que generaliza algunos patrones de aplicación comunes:

  • channels para escuchar en fuentes de eventos externas (por ejemplo, websockets)
  • modelo de horquilla ( fork, spawn)
  • acelerador
  • ...

Las sagas son una gran y poderosa herramienta. Sin embargo, con el poder viene la responsabilidad. Cuando su aplicación crece, puede perderse fácilmente al descubrir quién está esperando que se envíe la acción o qué sucede cuando se envía alguna acción. Por otro lado, thunk es más simple y más fácil de razonar. Elegir uno u otro depende de muchos aspectos, como el tipo y el tamaño del proyecto, qué tipos de efectos secundarios debe manejar su proyecto o las preferencias del equipo de desarrollo. En cualquier caso, simplemente mantenga su aplicación simple y predecible.


8

Solo alguna experiencia personal:

  1. Para codificar el estilo y la legibilidad, una de las ventajas más significativas de usar redux-saga en el pasado es evitar el infierno de devolución de llamada en redux-thunk: ya no es necesario usar muchos anidamientos / capturas. Pero ahora, con la popularidad de async / await thunk, también se podría escribir código asíncrono en estilo de sincronización cuando se utiliza redux-thunk, lo que puede considerarse como una mejora en redux-think.

  2. Es posible que sea necesario escribir mucho más código repetitivo cuando se usa redux-saga, especialmente en Typecript. Por ejemplo, si se quiere implementar una función de recuperación asíncrona, el manejo de datos y errores se puede realizar directamente en una unidad thunk en action.js con una sola acción FETCH. Pero en redux-saga, uno puede necesitar definir las acciones FETCH_START, FETCH_SUCCESS y FETCH_FAILURE y todas sus verificaciones de tipo relacionadas, porque una de las características en redux-saga es usar este tipo de mecanismo rico de "token" para crear efectos e instruir tienda redux para pruebas fáciles. Por supuesto, uno podría escribir una saga sin usar estas acciones, pero eso lo haría similar a un thunk.

  3. En términos de estructura de archivos, redux-saga parece ser más explícito en muchos casos. Uno podría encontrar fácilmente un código asíncrono relacionado en cada sagas.ts, pero en redux-thunk, uno debería verlo en acciones.

  4. Las pruebas fáciles pueden ser otra característica ponderada en redux-saga. Esto es realmente conveniente. Pero una cosa que debe aclararse es que la prueba de "llamada" de redux-saga no realizaría una llamada API real en la prueba, por lo tanto, sería necesario especificar el resultado de la muestra para los pasos que pueden usar después de la llamada API. Por lo tanto, antes de escribir en redux-saga, sería mejor planificar una saga y sus correspondientes sagas.spec.ts en detalle.

  5. Redux-saga también proporciona muchas características avanzadas, como ejecutar tareas en paralelo, ayudantes de concurrencia como takeLatest / takeEvery, fork / spawn, que son mucho más potentes que los thunks.

En conclusión, personalmente, me gustaría decir: en muchos casos normales y aplicaciones de tamaño pequeño a mediano, vaya con estilo asíncrono / espera estilo redux-thunk. Le ahorraría muchos códigos / acciones / typedefs repetitivos, y no necesitaría cambiar muchos sagas.ts diferentes y mantener un árbol de sagas específico. Pero si está desarrollando una aplicación grande con una lógica asincrónica muy compleja y la necesidad de características como la simultaneidad / patrón paralelo, o si tiene una gran demanda de pruebas y mantenimiento (especialmente en el desarrollo basado en pruebas), redux-sagas posiblemente podría salvarle la vida .

De todos modos, redux-saga no es más difícil y complejo que el redux en sí mismo, y no tiene la llamada curva de aprendizaje empinada porque tiene conceptos básicos y API bien limitados. Pasar una pequeña cantidad de tiempo aprendiendo redux-saga puede beneficiarse algún día en el futuro.


5

Habiendo revisado algunos proyectos diferentes de React / Redux a gran escala en mi experiencia, Sagas proporciona a los desarrolladores una forma más estructurada de escribir código que es mucho más fácil de probar y más difícil de equivocarse.

Sí, es un poco extraño para empezar, pero la mayoría de los desarrolladores lo entienden lo suficiente en un día. Siempre le digo a la gente que no se preocupe por lo yieldque debe comenzar y que, una vez que escriba un par de pruebas, le llegará.

He visto un par de proyectos en los que los thunks han sido tratados como si fueran controladores de la plataforma MVC y esto rápidamente se convierte en un desastre indestructible.

Mi consejo es usar Sagas donde necesites A desencadena cosas de tipo B relacionadas con un solo evento. Para cualquier cosa que pueda atravesar una serie de acciones, considero que es más simple escribir middleware para clientes y usar la metapropiedad de una acción de FSA para activarlo.


2

Thunks versus Sagas

Redux-Thunky Redux-Sagadifieren en algunas formas importantes, ambas son bibliotecas de middleware para Redux (el middleware de Redux es un código que intercepta las acciones que ingresan a la tienda a través del método dispatch ()).

Una acción puede ser literalmente cualquier cosa, pero si sigue las mejores prácticas, una acción es un objeto javascript simple con un campo de tipo y campos opcionales de carga útil, meta y error. p.ej

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

Redux-Thunk

Además de despachar acciones estándar, el Redux-Thunkmiddleware le permite despachar funciones especiales, llamadas thunks.

Thunks (en Redux) generalmente tienen la siguiente estructura:

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

Es decir, a thunkes una función que (opcionalmente) toma algunos parámetros y devuelve otra función. La función interna toma una dispatch functiony una getStatefunción, las cuales serán proporcionadas por el Redux-Thunkmiddleware.

Redux-Saga

Redux-SagaEl middleware le permite expresar una lógica de aplicación compleja como funciones puras llamadas sagas. Las funciones puras son deseables desde el punto de vista de la prueba porque son predecibles y repetibles, lo que las hace relativamente fáciles de probar.

Las sagas se implementan a través de funciones especiales llamadas funciones generadoras. Estas son una nueva característica de ES6 JavaScript. Básicamente, la ejecución salta dentro y fuera de un generador donde sea que vea una declaración de rendimiento. Piense en una yielddeclaración que hace que el generador haga una pausa y devuelva el valor producido. Más tarde, la persona que llama puede reanudar el generador en la declaración que sigue a yield.

Una función generadora se define así. Observe el asterisco después de la palabra clave de función.

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

Una vez que la saga de inicio de sesión esté registrada en Redux-Saga. Pero luego, la yieldtoma de la primera línea detendrá la saga hasta que se envíe una acción con tipo 'LOGIN_REQUEST'a la tienda. Una vez que eso suceda, la ejecución continuará.

Para más detalles ver este artículo .


1

Una nota rápida Los generadores son cancelables, asíncronos / aguardan, no. Entonces, para un ejemplo de la pregunta, realmente no tiene sentido qué elegir. Pero para flujos más complicados a veces no hay mejor solución que usar generadores.

Entonces, otra idea podría ser usar generadores con redux-thunk, pero para mí, parece que trata de inventar una bicicleta con ruedas cuadradas.

Y, por supuesto, los generadores son más fáciles de probar.


0

Aquí hay un proyecto que combina las mejores partes (pros) de ambos redux-sagay redux-thunk: puede manejar todos los efectos secundarios en las sagas mientras obtiene una promesa por dispatchingla acción correspondiente: 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 un componente Reaccionar va en contra del paradigma. Debe manejar el estado modificado en componentDidUpdatelugar de esperar a que se resuelva una promesa.

3
@ Maxincredible52 No es cierto para la representación del lado del servidor.
Diego Haz

En mi experiencia, el punto de Max sigue siendo cierto para la representación del lado del servidor. Esto probablemente debería manejarse en algún lugar de la capa de enrutamiento.
ThinkingInBits

3
@ Maxincredible52 ¿por qué está en contra del paradigma? ¿Dónde has leído eso? Por lo general, hago algo similar a @Diego Haz, pero lo hago en componentDidMount (según React docs, es preferible que las llamadas de red se realicen allí), así que tenemoscomponentDidlMount() { this.props.doSomething().then((detail) => { this.setState({isReady: true})} }
user3711421

0

Una forma más fácil es usar redux-auto .

de la documentación

redux-auto solucionó este problema asincrónico simplemente permitiéndole crear una función de "acción" que devuelve una promesa. Para acompañar su lógica de acción de función "predeterminada".

  1. No es necesario otro middleware asincrónico Redux. por ejemplo, thunk, promesa-middleware, saga
  2. Le permite pasar fácilmente una promesa a Redux y hacer que se gestione por usted
  3. Le permite ubicar conjuntamente llamadas de servicio externas con las que se transformarán
  4. Nombrar el archivo "init.js" lo llamará una vez al inicio de la aplicación. Esto es bueno para cargar datos desde el servidor al inicio

La idea es tener cada acción en un archivo específico . co-ubicar la llamada al servidor en el archivo con funciones reductoras para "pendiente", "cumplida" y "rechazada". Esto hace que el manejo de las promesas sea muy fácil.

También adjunta automáticamente un objeto auxiliar (llamado "asíncrono") al prototipo de su estado, lo que le permite rastrear en su UI las transiciones solicitadas.


2
Hice +1 incluso es una respuesta irrelevante porque también se deben considerar diferentes soluciones
Amorenew

12
Creo que están allí porque no reveló que él es el autor del proyecto
jreptak
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.