Ir al contenido principal

Enfoques para Efectos Secundarios

Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Qué aprenderás
  • Qué son los "efectos secundarios" y cómo encajan en Redux
  • Herramientas comunes para gestionar efectos secundarios con Redux
  • Nuestras recomendaciones sobre qué herramientas usar para diferentes casos de uso

Redux y efectos secundarios

Resumen de efectos secundarios

Por sí mismo, un almacén Redux no sabe nada sobre lógica asíncrona. Solo sabe cómo enviar acciones de forma síncrona, actualizar el estado llamando a la función reductora raíz, y notificar a la UI que algo ha cambiado. Cualquier asincronía debe ocurrir fuera del almacén.

Los reductores de Redux nunca deben contener "efectos secundarios". Un "efecto secundario" es cualquier cambio en el estado o comportamiento que pueda observarse fuera de devolver un valor desde una función. Algunos tipos comunes de efectos secundarios son:

  • Registrar un valor en la consola

  • Guardar un archivo

  • Establecer un temporizador asíncrono

  • Realizar una petición HTTP AJAX

  • Modificar algún estado que exista fuera de una función, o mutar argumentos de una función

  • Generar números aleatorios o IDs únicos aleatorios (como Math.random() o Date.now())

Sin embargo, cualquier aplicación real necesitará hacer este tipo de cosas en algún sitio. Entonces, si no podemos poner efectos secundarios en los reductores, ¿dónde podemos ponerlos?

Middleware y efectos secundarios

El middleware de Redux fue diseñado para permitir escribir lógica que tiene efectos secundarios.

Un middleware de Redux puede hacer cualquier cosa cuando detecta una acción despachada: registrar algo, modificar la acción, retrasar la acción, hacer una llamada asíncrona y más. Además, dado que los middleware forman una canalización alrededor de la función real store.dispatch, esto también significa que podríamos pasar algo que no sea un objeto de acción simple a dispatch, siempre que un middleware intercepte ese valor y no lo deje llegar a los reductores.

Los middleware también tienen acceso a dispatch y getState. Esto significa que puedes escribir lógica asíncrona en un middleware y aún tener la capacidad de interactuar con el almacén de Redux mediante el despacho de acciones.

Por esto, los efectos secundarios y la lógica asíncrona en Redux normalmente se implementan mediante middleware.

Casos de uso de efectos secundarios

En la práctica, el caso de uso más común para efectos secundarios en una aplicación típica de Redux es obtener y almacenar en caché datos desde el servidor.

Otro caso de uso más específico de Redux es escribir lógica que responde a una acción despachada o cambio de estado ejecutando lógica adicional, como despachar más acciones.

Recomendaciones

Recomendamos usar las herramientas que mejor se adapten a cada caso de uso (ver más abajo las razones de nuestras recomendaciones, así como detalles adicionales sobre cada herramienta):

consejo

Obtención de datos

  • Usa RTK Query como enfoque predeterminado para obtener datos y almacenarlos en caché
  • Si RTKQ no encaja completamente por alguna razón, usa createAsyncThunk
  • Solo recurre a thunks manuales si nada más funciona
  • ¡No uses sagas u observables para obtener datos!

Reacción a acciones / cambios de estado, flujos de trabajo asíncronos

  • Usa los listeners de RTK como opción predeterminada para responder a actualizaciones del almacén y escribir flujos de trabajo asíncronos de larga duración
  • Solo usa sagas / observables si los listeners no resuelven bien tu caso de uso

Lógica con acceso al estado

  • Usa thunks para lógica síncrona compleja y asíncrona moderada, incluyendo acceso a getState y despacho de múltiples acciones

Por qué RTK Query para obtener datos

Según la sección de la documentación de React sobre "alternativas para la obtención de datos en Efectos", deberías usar enfoques de obtención de datos integrados en un framework del lado del servidor o una caché del lado del cliente. No deberías escribir tú mismo el código de obtención de datos y gestión de la caché.

RTK Query se diseñó específicamente como una capa completa de obtención y almacenamiento en caché de datos para aplicaciones basadas en Redux. Gestiona toda la lógica de obtención, almacenamiento en caché y estados de carga por ti, cubre muchos casos extremos que normalmente se olvidan o son difíciles de manejar si escribes tú mismo el código de obtención de datos, y además incluye gestión del ciclo de vida de la caché. También simplifica la obtención y uso de datos mediante los hooks de React generados automáticamente.

Recomendamos específicamente no usar sagas para obtener datos porque su complejidad no aporta valor y aún tendrías que escribir toda la lógica de gestión de caché + estados de carga tú mismo.

Por qué listeners para lógica reactiva

Hemos diseñado intencionadamente el middleware de escucha de RTK para que sea sencillo de usar. Utiliza sintaxis estándar async/await, cubre la mayoría de casos de uso reactivos comunes (responder a acciones o cambios de estado, debouncing, retrasos), e incluso varios casos avanzados (lanzar tareas hijas). Tiene un tamaño de paquete pequeño (~3K), está incluido en Redux Toolkit y funciona muy bien con TypeScript.

Recomendamos específicamente no usar sagas u observables para la mayoría de la lógica reactiva por varias razones:

  • Sagas: requieren comprender la sintaxis de funciones generadoras así como los comportamientos de efectos de saga; añaden múltiples niveles de indirección al necesitar acciones adicionales despachadas; tienen soporte deficiente en TypeScript; y la potencia y complejidad simplemente no son necesarias para la mayoría de casos de uso de Redux.

  • Observables: requieren comprender la API y modelo mental de RxJS; pueden ser difíciles de depurar; pueden añadir un tamaño de paquete significativo.

Enfoques Comunes para Efectos Secundarios

La técnica de más bajo nivel para gestionar efectos secundarios con Redux es escribir tu propio middleware personalizado que escuche acciones específicas y ejecute lógica. Sin embargo, esto rara vez se usa. En su lugar, la mayoría de aplicaciones históricamente han usado uno de los middleware de efectos secundarios preconstruidos comunes disponibles en el ecosistema: thunks, sagas u observables. Cada uno tiene sus propios casos de uso y compensaciones.

Más recientemente, nuestro paquete oficial Redux Toolkit ha añadido dos nuevas APIs para gestionar efectos secundarios: el middleware de "escucha" para escribir lógica reactiva, y RTK Query para obtener y cachear estado del servidor.

Thunks

El middleware "thunk" de Redux tradicionalmente ha sido el middleware más utilizado para escribir lógica asíncrona.

Los thunks funcionan pasando una función a dispatch. El middleware thunk intercepta la función, la llama y pasa theThunkFunction(dispatch, getState). La función thunk ahora puede hacer cualquier lógica síncrona/asíncrona e interactuar con el almacén.

Casos de Uso de Thunks

Los thunks son más adecuados para lógica síncrona compleja que necesita acceso a dispatch y getState, o lógica asíncrona moderada como solicitudes únicas de "obtener datos asíncronos y despachar una acción con el resultado".

Tradicionalmente hemos recomendado thunks como enfoque por defecto, y Redux Toolkit incluye específicamente la API createAsyncThunk para el caso de uso "solicitar y despachar". Para otros casos, puedes escribir tus propias funciones thunk.

Compensaciones de Thunks

  • 👍: Solo escribes funciones; pueden contener cualquier lógica

  • 👎: No pueden responder a acciones despachadas; son imperativos; no pueden cancelarse

Thunk Examples
const thunkMiddleware =
({ dispatch, getState }) =>
next =>
action => {
if (typeof action === 'function') {
return action(dispatch, getState)
}

return next(action)
}

// Original "hand-written" thunk fetch request pattern
const fetchUserById = userId => {
return async (dispatch, getState) => {
// Dispatch "pending" action to help track loading state
dispatch(fetchUserStarted())
// Need to pull this out to have correct error handling
let lastAction
try {
const user = await userApi.getUserById(userId)
// Dispatch "fulfilled" action on success
lastAction = fetchUserSucceeded(user)
} catch (err) {
// Dispatch "rejected" action on failure
lastAction = fetchUserFailed(err.message)
}
dispatch(lastAction)
}
}

// Similar request with `createAsyncThunk`
const fetchUserById2 = createAsyncThunk('fetchUserById', async userId => {
const user = await userApi.getUserById(userId)
return user
})

Sagas

El middleware Redux-Saga tradicionalmente ha sido la segunda herramienta más común para efectos secundarios, después de los thunks. Está inspirado en el patrón "saga" de backend, donde flujos de trabajo de larga duración pueden responder a eventos desencadenados en todo el sistema.

Conceptualmente, puedes pensar en las sagas como "hilos en segundo plano" dentro de la aplicación Redux, que tienen la capacidad de escuchar acciones despachadas y ejecutar lógica adicional.

Las sagas se escriben usando funciones generadoras. Las funciones saga devuelven descripciones de efectos secundarios y se pausan a sí mismas, y el middleware saga es responsable de ejecutar el efecto secundario y reanudar la función saga con el resultado. La biblioteca redux-saga incluye varias definiciones de efectos como:

  • call: ejecuta una función asíncrona y devuelve el resultado cuando se resuelve la promesa

  • put: despacha una acción de Redux

  • fork: crea una "saga hija", como un hilo adicional que puede hacer más trabajo

  • takeLatest: escucha una acción de Redux dada, desencadena una función saga para ejecutarse y cancela copias previas en ejecución si se vuelve a despachar

Casos de Uso de Sagas

Las sagas son extremadamente potentes y son más adecuadas para flujos de trabajo asíncronos altamente complejos que requieren comportamiento tipo "hilo en segundo plano" o debouncing/cancelación.

Los usuarios de Sagas suelen destacar que estas funciones solo devuelven descripciones de los efectos deseados como un aspecto positivo clave que las hace más testables.

Ventajas y Desventajas de Sagas

  • 👍: Las Sagas son testables porque solo devuelven descripciones de efectos; modelo de efectos potente; capacidades de pausa/cancelación

  • 👎: Las funciones generadoras son complejas; API única de efectos saga; las pruebas de sagas a menudo solo verifican resultados de implementación y deben reescribirse cada vez que se modifica la saga, reduciendo su valor; no funcionan bien con TypeScript;

Saga Examples
import { call, put, takeEvery } from 'redux-saga/effects'

// "Worker" saga: will be fired on USER_FETCH_REQUESTED actions
function* fetchUser(action) {
yield put(fetchUserStarted())
try {
const user = yield call(userApi.getUserById, action.payload.userId)
yield put(fetchUserSucceeded(user))
} catch (err) {
yield put(fetchUserFailed(err.message))
}
}

// "Watcher" saga: starts fetchUser on each `USER_FETCH_REQUESTED` action
function* fetchUserWatcher() {
yield takeEvery('USER_FETCH_REQUESTED', fetchUser)
}

// Can use also use sagas for complex async workflows with "child tasks":
function* fetchAll() {
const task1 = yield fork(fetchResource, 'users')
const task2 = yield fork(fetchResource, 'comments')
yield delay(1000)
}

function* fetchResource(resource) {
const { data } = yield call(api.fetch, resource)
yield put(receiveData(data))
}

Observables

El middleware Redux-Observable permite usar observables RxJS para crear pipelines de procesamiento llamados "epics".

Como RxJS es una biblioteca agnóstica a frameworks, sus usuarios destacan que puedes reutilizar el conocimiento de su uso en diferentes plataformas como ventaja principal. Además, RxJS permite construir pipelines declarativos que manejan casos de temporización como cancelación o debouncing.

Casos de Uso para Observables

Al igual que las sagas, los observables son potentes y mejores para flujos de trabajo asíncronos altamente complejos que requieren comportamiento tipo "hilo en segundo plano" o debouncing/cancelación.

Ventajas y Desventajas de Observables

  • 👍: Modelo de flujo de datos muy potente; conocimiento de RxJS reusable fuera de Redux; sintaxis declarativa

  • 👎: API de RxJS compleja; modelo mental difícil; puede ser complicado depurar; tamaño del bundle

Observable Examples
// Typical AJAX example:
const fetchUserEpic = action$ =>
action$.pipe(
filter(fetchUser.match),
mergeMap(action =>
ajax
.getJSON(`https://api.github.com/users/${action.payload}`)
.pipe(map(response => fetchUserFulfilled(response)))
)
)

// Can write highly complex async pipelines, including delays,
// cancellation, debouncing, and error handling:
const fetchReposEpic = action$ =>
action$.pipe(
filter(fetchReposInput.match),
debounceTime(300),
switchMap(action =>
of(fetchReposStart()).pipe(
concat(
searchRepos(action.payload).pipe(
map(payload => fetchReposSuccess(payload.items)),
catchError(error => of(fetchReposError(error)))
)
)
)
)
)

Listeners

Redux Toolkit incluye la API createListenerMiddleware para manejar lógica "reactiva". Específicamente diseñada como alternativa más ligera a sagas y observables, cubre el 90% de los mismos casos de uso con menor tamaño de bundle, API más simple y mejor soporte para TypeScript.

Conceptualmente, es similar al hook useEffect de React, pero para actualizaciones del almacén Redux.

Este middleware permite añadir entradas que coinciden con acciones para determinar cuándo ejecutar el callback effect. Como los thunks, un callback effect puede ser síncrono o asíncrono, con acceso a dispatch y getState. También recibe un objeto listenerApi con primitivas para construir flujos asíncronos, como:

  • condition(): pausa hasta que se despache cierta acción u ocurra cambio de estado

  • cancelActiveListeners(): cancela instancias existentes en curso del efecto

  • fork(): crea una "tarea hija" para trabajo adicional

Estas primitivas permiten replicar casi todos los comportamientos de efectos de Redux-Saga.

Casos de Uso para Listeners

Útiles para diversas tareas: persistencia ligera del almacén, disparar lógica adicional al despachar acciones, observar cambios de estado, y flujos asíncronos complejos de larga duración estilo "hilo en segundo plano".

Además, las entradas pueden añadirse/eliminarse dinámicamente durante la ejecución mediante acciones add/removeListener. Se integra bien con useEffect de React para añadir comportamientos vinculados al ciclo de vida de componentes.

Ventajas y Desventajas de Listeners

  • 👍: Integrado en Redux Toolkit; sintaxis async/await más familiar; similar a thunks; conceptos y tamaño livianos; excelente con TypeScript

  • 👎: Relativamente nuevo y menos "probado en batalla"; no tan flexible como sagas/observables

Listener Examples
// Create the middleware instance and methods
const listenerMiddleware = createListenerMiddleware()

// Add one or more listener entries that look for specific actions.
// They may contain any sync or async logic, similar to thunks.
listenerMiddleware.startListening({
actionCreator: todoAdded,
effect: async (action, listenerApi) => {
// Run whatever additional side-effect-y logic you want here
console.log('Todo added: ', action.payload.text)

// Can cancel other running instances
listenerApi.cancelActiveListeners()

// Run async logic
const data = await fetchData()

// Use the listener API methods to dispatch, get state,
// unsubscribe the listener, start child tasks, and more
listenerApi.dispatch(todoAdded('Buy pet food'))
}
})

listenerMiddleware.startListening({
// Can match against actions _or_ state changes/contents
predicate: (action, currentState, previousState) => {
return currentState.counter.value !== previousState.counter.value
},
// Listeners can have long-running async workflows
effect: async (action, listenerApi) => {
// Pause until action dispatched or state changed
if (await listenerApi.condition(matchSomeAction)) {
// Spawn "child tasks" that can do more work and return results
const task = listenerApi.fork(async forkApi => {
// Can pause execution
await forkApi.delay(5)
// Complete the child by returning a value
return 42
})

// Unwrap the child result in the listener
const result = await task.result
if (result.status === 'ok') {
console.log('Child succeeded: ', result.value)
}
}
}
})

RTK Query

Redux Toolkit incluye RTK Query, solución especializada para obtener datos y cachear en apps Redux. Diseñada para simplificar casos comunes de carga de datos, eliminando la necesidad de escribir manualmente lógica de fetching y caché.

RTK Query se basa en crear una definición de API compuesta por múltiples "endpoints". Un endpoint puede ser una "query" para obtener datos o una "mutation" para enviar actualizaciones al servidor. RTKQ gestiona internamente la obtención y caché de datos, incluyendo el seguimiento del uso de cada entrada en caché y la eliminación de datos en caché que ya no son necesarios. Cuenta con un sistema único de "tags" para activar recargas automáticas de datos cuando las mutaciones actualizan el estado en el servidor.

Como el resto de Redux, RTKQ es agnóstico a la UI en su núcleo y puede usarse con cualquier framework. Sin embargo, también incluye integración con React incorporada y puede generar automáticamente hooks de React para cada endpoint. Esto proporciona una API familiar y sencilla para obtener y actualizar datos desde componentes de React.

RTKQ proporciona una implementación basada en fetch lista para usar y funciona excelente con APIs REST. También es lo suficientemente flexible para usarse con APIs GraphQL, e incluso puede configurarse para trabajar con funciones asíncronas arbitrarias, permitiendo integración con SDKs externos como Firebase, Supabase o tu propia lógica asíncrona.

RTKQ también tiene capacidades potentes como los "métodos de ciclo de vida" de endpoints, que permiten ejecutar lógica cuando se añaden o eliminan entradas de caché. Esto puede usarse para escenarios como obtener datos iniciales de una sala de chat y luego suscribirse a un socket para mensajes adicionales que actualizan la caché.

Casos de uso de RTK Query

RTK Query está específicamente diseñado para resolver el caso de uso de obtención de datos y almacenamiento en caché del estado del servidor.

Compromisos de RTK Query

  • 👍: Integrado en RTK; elimina la necesidad de escribir cualquier código (thunks, selectores, efectos, reductores) para gestionar obtención de datos y estado de carga; funciona genial con TS; se integra con el resto del almacén Redux; hooks de React incorporados

  • 👎: Intencionalmente una caché de estilo "documento", no "normalizada"; Añade un coste único adicional en tamaño de bundle

RTK Query Examples
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Pokemon } from './types'

// Create an API definition using a base URL and expected endpoints
export const api = createApi({
reducerPath: 'pokemonApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: builder => ({
getPokemonByName: builder.query<Pokemon, string>({
query: name => `pokemon/${name}`
}),
getPosts: builder.query<Post[], void>({
query: () => '/posts'
}),
addNewPost: builder.mutation<void, Post>({
query: initialPost => ({
url: '/posts',
method: 'POST',
// Include the entire post object as the body of the request
body: initialPost
})
})
})
})

// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const { useGetPokemonByNameQuery } = api

export default function App() {
// Using a query hook automatically fetches data and returns query values
const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur')

// render UI based on data and loading state
}

Otros enfoques

Middleware personalizado

Dado que thunks, sagas, observables y listeners son formas de middleware de Redux (y RTK Query incluye su propio middleware personalizado), siempre es posible escribir tu propio middleware si ninguna de estas herramientas maneja adecuadamente tus casos de uso.

Nota: ¡Recomendamos específicamente no intentar usar middleware personalizado como técnica principal para gestionar la lógica de tu aplicación! Algunos usuarios han intentado crear docenas de middleware personalizados, uno por cada funcionalidad. Esto añade sobrecarga significativa, ya que cada middleware debe ejecutarse en cada llamada a dispatch. Es mejor usar middleware de propósito general como thunks o listeners, donde una única instancia de middleware puede manejar múltiples fragmentos de lógica.

Custom Middleware Example
const delayedActionMiddleware = storeAPI => next => action => {
if (action.type === 'todos/todoAdded') {
setTimeout(() => {
// Delay this action by one second
next(action)
}, 1000)
return
}

return next(action)
}

Websockets

Muchas apps usan websockets u otras conexiones persistentes, principalmente para recibir actualizaciones en tiempo real del servidor.

Generalmente recomendamos que el uso de websockets en apps Redux viva dentro de un middleware personalizado, por varias razones:

  • El middleware existe durante toda la vida de la aplicación

  • Al igual que con el almacén, probablemente solo necesitas una única instancia de conexión que toda la aplicación pueda usar

  • El middleware puede ver todas las acciones despachadas y despachar acciones él mismo. Esto significa que un middleware puede tomar acciones despachadas y convertirlas en mensajes enviados a través del websocket, y despachar nuevas acciones cuando se recibe un mensaje por el websocket.

  • Una instancia de conexión websocket no es serializable, por lo que no pertenece al estado del almacén

Dependiendo de las necesidades de la aplicación, podrías crear el socket durante la inicialización del middleware, crearlo bajo demanda en el middleware mediante una acción de inicialización, o crearlo en un archivo de módulo separado para accederlo desde otros lugares.

Los websockets también pueden usarse en callbacks del ciclo de vida de RTK Query, donde podrían responder a mensajes aplicando actualizaciones a la caché de RTKQ.

XState

Las máquinas de estado pueden ser muy útiles para definir posibles estados conocidos de un sistema y las transiciones entre ellos, así como para activar efectos secundarios cuando ocurre una transición.

Los reductores de Redux pueden escribirse como verdaderas Máquinas de Estado Finito, pero RTK no incluye nada para ayudar con esto. En la práctica, tienden a ser máquinas de estado parciales que realmente solo se preocupan por la acción despachada para determinar cómo actualizar el estado. Los listeners, sagas y observables pueden usarse para el aspecto de "ejecutar efectos secundarios después del despacho", pero a veces pueden requerir más trabajo para garantizar que un efecto secundario se ejecute solo en un momento específico.

XState es una biblioteca potente para definir verdaderas máquinas de estado y ejecutarlas, incluyendo la gestión de transiciones de estado basadas en eventos y la activación de efectos secundarios relacionados. También tiene herramientas relacionadas para crear definiciones de máquinas de estado mediante un editor gráfico, que luego pueden cargarse en la lógica de XState para su ejecución.

Aunque actualmente no existe una integración oficial entre XState y Redux, es posible usar una máquina de XState como reductor de Redux, y los desarrolladores de XState han creado una prueba de concepto útil que demuestra el uso de XState como middleware de efectos secundarios de Redux:

Más información