Saltar al contenido

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

Solución:

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 debemos notar es que estamos llamando a las funciones api usando el formulario yield call(func, ...args). call no 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 usando simples verificaciones 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 next método del iterador. La burla de datos es mucho más simple que las funciones de burla.

La segunda cosa a tener en cuenta es la llamada a yield take(ACTION). El creador de la acción llama a los thunks en cada nueva acción (p. Ej. LOGIN_REQUEST). es decir, las acciones son continuamente empujado a los thunks, y los thunks no tienen control sobre cuándo dejar de manejar esas acciones.

En redux-saga, generadores jalar 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) loop, por lo que escuchará cada acción entrante, lo que de alguna manera imita el comportamiento de empuje del procesador.

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

  • Manejar la acción del usuario LOGOUT

  • en el primer inicio de sesión exitoso, el servidor devuelve un token que expira con cierto retraso almacenado en un expires_in campo. Tendremos que actualizar la autorización en segundo plano en cada expires_in milisegundos

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

¿Cómo implementarías eso con thunks? al mismo tiempo que proporciona una 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, expresamos nuestro requisito de simultaneidad usando race. Si take(LOGOUT) gana la carrera (es decir, el usuario hizo clic en un botón Cerrar sesión). La carrera cancelará automáticamente la authAndRefreshTokenOnExpiry tarea de fondo. Y si el authAndRefreshTokenOnExpiry fue bloqueado en medio de un call(authorize, {token}) también se cancelará la llamada. La cancelación se propaga hacia abajo automáticamente.

Puede encontrar una demostración ejecutable del flujo anterior

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

Pro (usando saga):

  • Testabilidad. Es muy fácil probar sagas ya que call () devuelve un objeto puro. Probar procesadores normalmente requiere que incluya un mockStore dentro de su prueba.

  • redux-saga viene con muchas funciones de ayuda útiles sobre tareas. Me parece que el concepto de saga es crear algún tipo de subproceso / trabajador en segundo plano para su aplicación, que actúa como una pieza faltante en la arquitectura react redux (actionCreators y redux 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, es más fácil de modificar y administrar que las acciones de procesador en mi experiencia.

Estafa:

  • Sintaxis del generador.

  • Muchos conceptos para aprender.

  • Estabilidad API. Parece que redux-saga todavía está agregando funciones (por ejemplo, ¿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.

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

Las sagas son geniales para probar:

  • No es necesario simular funciones envueltas con 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 de 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 lo puedes hacer 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 la rutina existentecancel, takeLatest, race)
  • múltiples rutinas pueden escuchar la misma acción (take, takeEvery, …)

Sagas también ofrece otras funciones útiles, que generalizan algunos patrones de aplicación comunes:

  • channels para escuchar en fuentes de eventos externas (por ejemplo, websockets)
  • modelo de horquillafork, 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 averiguar 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. La elección de uno u otro depende de muchos aspectos, como el tipo y el tamaño del proyecto, los tipos de efectos secundarios que debe manejar su proyecto o la preferencia del equipo de desarrollo. En cualquier caso, mantenga su aplicación simple y predecible.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *