javascript - react - Pros/contras de usar redux-saga con generadores ES6 versus redux-thunk con ES2017 async/await
take sagas (9)
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.
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
redux-saga
lugar del siguiente enfoque donde estoy usando
redux-thunk
con async /
redux-thunk
.
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...
Aquí hay un proyecto que combina las mejores partes (profesionales) de
redux-saga
y
redux-thunk
: puede manejar todos los efectos secundarios en las sagas mientras obtiene una promesa al
dispatching
la acción correspondiente:
https://github.com/diegohaz/redux-saga-thunk
function* mySaga() {
// ...
}
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)
.
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 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
next
método del iterador.
Los datos de burla son mucho más simples que las funciones de burla.
La segunda cosa a tener en cuenta 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 se envían continuamente a thunks, y los thunks no tienen control sobre cuándo dejar de manejar esas acciones.
En redux-saga, los generadores hacen 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 bucle
while(true)
, 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 campo
expires_in
. Tendremos que actualizar la autorización en segundo plano cada vez queexpires_in
milisegundos -
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 un Botón Cerrar sesión).
La carrera cancelará automáticamente la tarea en segundo plano
authAndRefreshTokenOnExpiry
.
Y si
authAndRefreshTokenOnExpiry
se bloqueó en medio de una
call(authorize, {token})
, también se cancelará.
La cancelación se propaga hacia abajo automáticamente.
Puede encontrar una demostración ejecutable del flujo anterior
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 les digo a las personas que no se preocupen por el
yield
para comenzar y que una vez que escriban un par de pruebas, les 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 sencillo escribir middleware para clientes y usar la metapropiedad de una acción de FSA para activarlo.
Solo alguna experiencia personal:
-
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 / catch. 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, que puede considerarse como una mejora en redux-think.
-
Es posible que sea necesario escribir mucho más código repetitivo cuando se usa redux-saga, especialmente en Typecript. Por ejemplo, si se desea implementar una función de recuperación asíncrona, el manejo de datos y errores se podría 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.
-
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 necesitaría verlo en acciones.
-
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.
-
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 / en espera 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 concurrencia / 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 redux en sí, y no tiene una curva de aprendizaje pronunciada 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.
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 la 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 todo 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.
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".
- No es necesario otro middleware asincrónico Redux. por ejemplo, thunk, promesa-middleware, saga
- Le permite pasar fácilmente una promesa a Redux y hacer que se gestione por usted
- Le permite ubicar conjuntamente llamadas de servicio externas con las que se transformarán
- 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.
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.
Thunks versus Sagas
Redux-Thunk
y
Redux-Saga
difieren 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
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)
})
}
}
Redux-Thunk
Además de despachar acciones estándar, el middleware
Redux-Thunk
permite despachar funciones especiales, llamadas
thunks
.
Thunks (en Redux) generalmente tienen la siguiente estructura:
const loginRequest = {
type: ''LOGIN_REQUEST'',
payload: {
name: ''admin'',
password: ''123'',
}, };
Es decir, un
thunk
es una función que (opcionalmente) toma algunos parámetros y devuelve otra función.
La función interna toma una
dispatch function
y una función
getState
; ambas serán suministradas por el middleware
Redux-Thunk
.
Redux-Saga
Redux-Saga
middleware
Redux-Saga
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 declaración de
yield
como causante de 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 al
yield
.
Una función generadora se define así. Observe el asterisco después de la palabra clave de función.
export const thunkName =
parameters =>
(dispatch, getState) => {
// Your application logic goes here
};
Una vez que la saga de inicio de sesión se registra con
Redux-Saga
.
Pero luego la toma de
yield
en la primera línea detendrá la saga hasta que se
''LOGIN_REQUEST''
una acción con el tipo
''LOGIN_REQUEST''
a la tienda.
Una vez que eso suceda, la ejecución continuará.