Side Effects Approaches
- What "side effects" are and how they fit into Redux
- Common tools for managing side effects with Redux
- Our recommendations for which tools to use for different use cases
Redux and Side Effects
Side Effects Overview
By itself, a Redux store doesn't know anything about async logic. It only knows how to synchronously dispatch actions, update the state by calling the root reducer function, and notify the UI that something has changed. Any asynchronicity has to happen outside the store.
Redux reducers must never contain "side effects".A "side effect" is any change to state or behavior that can be seen outside of returning a value from a function. Some common kinds of side effects are things like:
- Logging a value to the console
- Saving a file
- Setting an async timer
- Making an AJAX HTTP request
- Modifying some state that exists outside of a function, or mutating arguments to a function
- Generating random numbers or unique random IDs (such as
Math.random()orDate.now())
However, any real app will need to do these kinds of thingssomewhere. So, if we can't put side effects in reducers, wherecan we put them?
Middleware and Side Effects
Redux middleware were designed to enable writing logic that has side effects.
A Redux middleware can doanything when it sees a dispatched action: log something, modify the action, delay the action, make an async call, and more. Also, since middleware form a pipeline around the realstore.dispatch function, this also means that we could actually pass something thatisn't a plain action object todispatch, as long as a middleware intercepts that value and doesn't let it reach the reducers.
Middleware also have access todispatch andgetState. That means you could write some async logic in a middleware, and still have the ability to interact with the Redux store by dispatching actions.
Because of this,Redux side effects and async logic are normally implemented through middleware.
Side Effects Use Cases
In practice,the single most common use case for side effects in a typical Redux app is fetching and caching data from the server.
Another use case more specific to Redux is writing logic thatresponds to a dispatched action or state change by executing additional logic, such as dispatching more actions.
Recommendations
We recommend using the tools that best fit each use case (see below for the reasons for our recommendations, as well as further details on each tool):
Data Fetching
- Use RTK Query as the default approach for data fetching and caching
- If RTKQ doesn't fully fit for some reason, use
createAsyncThunk - Only fall back to handwritten thunks if nothing else works
- Don't use sagas or observables for data fetching!
Reacting to Actions / State Changes, Async Workflows
- Use RTK listeners as the default for responding to store updates and writing long-running async workflows
- Only use sagas / observables if listeners don't solve your use case well enough
Logic with State Access
- Use thunks for complex sync and moderate async logic, including access to
getStateand dispatching multiple actions
Why RTK Query for Data Fetching
Perthe React docs section on "alternatives for data fetching in Effects", youshould use either data fetching approaches that are built into a server-side framework, or a client-side cache.You shouldnot write data fetching and cache management code yourself.
RTK Query was specifically designed to be a complete data fetching and caching layer for Redux-based applications. It manages all the fetching, caching, and loading status logic for you, and covers many edge cases that are typically forgotten or hard to handle if you write data fetching code yourself, as well as having cache lifecycle management built-in. It also makes it simple to fetch and use data via the auto-generated React hooks.
We specifically recommendagainst sagas for data fetching because the complexity of sagas is not helpful, and you would still have to write all of the caching + loading status management logic yourself.
Why Listeners for Reactive Logic
We've intentionally designed the RTK listener middleware to be straightforward to use. It uses standardasync/await syntax, covers most common reactive use cases (responding to actions or state changes, debouncing, delays), and even several advanced cases (launching child tasks). It has a small bundle size (~3K), is included with Redux Toolkit, and works great with TypeScript.
We specifically recommendagainst sagas or observables for most reactive logic for multiple reasons:
- Sagas: require understanding generator function syntax as well as the saga effects behaviors; add multiple levels of indirection due to needing extra actions dispatched; have poor TypeScript support; and the power and complexity is simply not needed for most Redux use cases.
- Observables: require understanding the RxJS API and mental model; can be difficult to debug; can add significant bundle size
Common Side Effects Approaches
The lowest-level technique for managing side effects with Redux is to write your own custom middleware that listens for specific actions and runs logic. However, that's rarely used. Instead, most apps have historically used one of the common pre-built Redux side effects middleware available in the ecosystem: thunks, sagas, or observables. Each of these has its own different use cases and tradeoffs.
More recently, our official Redux Toolkit package has added two new APIs for managing side effects: the "listener" middleware for writing reactive logic, and RTK Query for fetching and caching server state.
Thunks
TheRedux "thunk" middleware has traditionally been the most widely used middleware for writing async logic.
Thunks work by passing a function intodispatch. The thunk middleware intercepts the function, calls it, and passes intheThunkFunction(dispatch, getState). The thunk function can now do any sync/async logic and interact with the store.
Thunk Use Cases
Thunks are best used for complex sync logic that needs access todispatch andgetState, or moderate async logic such as one-shot "fetch some async data and dispatch an action with the result" requests.
We have traditionally recommended thunks as the default approach, and Redux Toolkit specifically includes thecreateAsyncThunk API for the "request and dispatch" use case. For other use cases, you can write your own thunk functions.
Thunk Tradeoffs
- 👍: Just write functions; may contain any logic
- 👎: Can't respond to dispatched actions; imperative; can't be cancelled
constthunkMiddleware=
({ dispatch, getState})=>
next=>
action=>{
if(typeof action==='function'){
returnaction(dispatch, getState)
}
returnnext(action)
}
// Original "hand-written" thunk fetch request pattern
constfetchUserById=userId=>{
returnasync(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',asyncuserId=>{
const user=await userApi.getUserById(userId)
return user
})
Sagas
TheRedux-Saga middleware has traditionally been the second most common tool for side effects, after thunks. It's inspired by the backend "saga" pattern, where long-running workflows can respond to events triggered throughout the system.
Conceptually, you can think of sagas as "background threads" inside the Redux app, which have the ability to listen to dispatched actions and run additional logic.
Sagas are written using generator functions. Saga functions returndescriptions of side effects and pause themselves, and the saga middleware is responsible for executing the side effect and resuming the saga function with the result. Theredux-saga library includes a variety of effects definitions such as:
call: executes an async function and returns the result when the promise resolves:put: dispatches a Redux actionfork: spawns a "child saga", like an additional thread that can do more worktakeLatest: listens for a given Redux action, triggers a saga function to execute, and cancels previous running copies of the saga if it's dispatched again
Saga Use Cases
Sagas are extremely powerful, and are best used for highly complex async workflows that require "background thread"-type behavior or debouncing/cancelling.
Saga users have often pointed to the fact that saga functions only returndescriptions of the desired effects as a major positive that makes them more testable.
Saga Tradeoffs
- 👍: Sagas are testable because they only return descriptions of effects; powerful effects model; pause/cancel capabilities
- 👎: generator functions are complex; unique saga effects API; saga tests often only test implementation results and need to be rewritten every time the saga is touched, making them a lot less valuable; do not work well with TypeScript;
import{ call, put, takeEvery}from'redux-saga/effects'
// "Worker" saga: will be fired on USER_FETCH_REQUESTED actions
function*fetchUser(action){
yieldput(fetchUserStarted())
try{
const user=yieldcall(userApi.getUserById, action.payload.userId)
yieldput(fetchUserSucceeded(user))
}catch(err){
yieldput(fetchUserFailed(err.message))
}
}
// "Watcher" saga: starts fetchUser on each `USER_FETCH_REQUESTED` action
function*fetchUserWatcher(){
yieldtakeEvery('USER_FETCH_REQUESTED', fetchUser)
}
// Can use also use sagas for complex async workflows with "child tasks":
function*fetchAll(){
const task1=yieldfork(fetchResource,'users')
const task2=yieldfork(fetchResource,'comments')
yielddelay(1000)
}
function*fetchResource(resource){
const{ data}=yieldcall(api.fetch, resource)
yieldput(receiveData(data))
}
Observables
TheRedux-Observable middleware lets you use RxJS observables to create processing pipelines called "epics".
Since RxJS is a framework-agnostic library, observable users point to the fact that you can reuse knowledge of how to use it across different platforms as a major selling point. In addition, RxJS lets you construct declarative pipelines that handle timing cases like cancellation or debouncing.
Observable Use Cases
Similar to sagas, observables are powerful and best used for highly complex async workflows that require "background thread"-type behavior or debouncing/cancelling.
Observable Tradeoffs
- 👍: Observables are a highly powerful data flow model; RxJS knowledge can be used separate from Redux; declarative syntax
- 👎: RxJS API is complex; mental model; can be hard to debug; bundle size
// Typical AJAX example:
constfetchUserEpic=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:
constfetchReposEpic=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 includesthecreateListenerMiddleware API to handle "reactive" logic. It's specifically intended to be a lighter-weight alternative to sagas and observables that handles 90% of the same use cases, with a smaller bundle size, simpler API, and better TypeScript support.
Conceptually, this is similar to React'suseEffect hook, but for Redux store updates.
The listener middleware lets you add entries that match against actions to determine when to run theeffect callback. Similar to thunks, aneffect callback can be sync or async, and have access todispatch andgetState. They also receive alistenerApi object with several primitives for building async workflows, such as:
condition(): pauses until a certain action is dispatched or state change occurscancelActiveListeners(): cancel existing in-progress instances of the effectfork(): creates a "child task" that can do additional work
These primitives allow listeners to replicate almost all of the effects behaviors from Redux-Saga.
Listener Use Cases
Listeners can be used for a wide variety of tasks, such as lightweight store persistence, triggering additional logic when an action is dispatched, watching for state changes, and complex long-running "background thread"-style async workflows.
In addition, listener entries can be added and removed dynamically at runtime by dispatching specialadd/removeListener actions. This integrates nicely with React'suseEffect hook, and can be used for adding additional behavior that corresponds to a component's lifetime.
Listener Tradeoffs
- 👍: Built into Redux Toolkit;
async/awaitis more familiar syntax; similar to thunks; lightweight concepts and size; works great with TypeScript - 👎: Relatively new and not as "battle-tested" yet; notquite as flexible as sagas/observables
// 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=awaitfetchData()
// 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(asyncforkApi=>{
// Can pause execution
await forkApi.delay(5)
// Complete the child by returning a value
return42
})
// 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 includesRTK Query, a purpose-built data fetching and caching solution for Redux apps. It's designed to simplify common cases for loading data in a web application, eliminating the need to hand-write data fetching & caching logic yourself.
RTK Query relies on creating an API definition consisting of many "endpoints". An endpoint can be a "query" for fetching data, or a "mutation" for sending an update to the server. RTKQ manages fetching and caching data internally, including tracking usage of each cache entry and removing cached data that's no longer needed. It features a unique "tag" system for triggering automatic refetches of data as mutations update state on the server.
Like the rest of Redux, RTKQ is UI-agnostic at its core, and can be used with any UI framework. However, it also comes with React integration built in, and can automatically generate React hooks for each endpoint. This provides a familiar and simple API for fetching and updating data from React components.
RTKQ provides afetch-based implementation out of the box, and works great with REST APIs. It's also flexible enough to be used with GraphQL APIs, and can even be configured to work with arbitrary async functions, allowing integration with external SDKs such as Firebase, Supabase, or your own async logic.
RTKQ also has powerful capabilities such as endpoint "lifecycle methods", allowing you to run logic as cache entries are added and removed. This can be used for scenarios like fetching initial data for a chat room, then subscribing to a socket for additional messages that are used to update the cache.
RTK Query Use Cases
RTK Query is specifically built to solve the use case of data fetching and caching of server state.
RTK Query Tradeoffs
- 👍: Built into RTK; eliminates the need to writeany code (thunks, selectors, effects, reducers) for managing data fetching and loading state; works great with TS; integrates into the rest of the Redux store; built-in React hooks
- 👎: Intentionally a "document"-style cache, rather than "normalized"; Adds a one-time additional bundle size cost
import{ createApi, fetchBaseQuery}from'@reduxjs/toolkit/query/react'
importtype{ Pokemon}from'./types'
// Create an API definition using a base URL and expected endpoints
exportconst 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
exportconst{ useGetPokemonByNameQuery}= api
exportdefaultfunctionApp(){
// 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
}
Other Approaches
Custom Middleware
Given that thunks, sagas, observables, and listeners are all forms of Redux middleware (and RTK Query includes its own custom middleware), it's always possible to write your own custom middleware if none of these tools sufficiently handles your use cases.
Note thatwe specifically recommendagainst trying to use custom middleware as a technique for managing the bulk of your app's logic! Some users have tried creating dozens of custom middleware, one per specific app feature. This adds significant overhead, as each middleware has to run as part of each call todispatch. It's better to use a general-purpose middleware such as thunks or listeners instead, where there's a single middleware instance added that can handle many different chunks of logic.
constdelayedActionMiddleware=storeAPI=>next=>action=>{
if(action.type==='todos/todoAdded'){
setTimeout(()=>{
// Delay this action by one second
next(action)
},1000)
return
}
returnnext(action)
}
Websockets
Many apps use websockets or some other form of persistent connection, primarily to receive streaming updates from the server.
We generally recommend thatmost websocket usage in a Redux app should live inside a custom middleware, for several reasons:
- Middleware exist for the lifetime of the application
- Like with the store itself, you probably only need a single instance of a given connection that the whole app can use
- Middleware can see all dispatched actions and dispatch actions themselves. This means a middleware can take dispatched actions and turn those into messages sent over the websocket, and dispatch new actions when a message is received over the websocket.
- A websocket connection instance isn't serializable, soit doesn't belong in the store state itself
Depending on the needs of the application, you could create the socket as part of the middleware init process, create the socket on demand in the middleware by dispatching an initialization action, or create it in a separate module file so it can be accessed elsewhere.
Websockets can also be used in an RTK Query lifecycle callback, where they could respond to messages by applying updates to the RTKQ cache.
XState
State machines can be very useful for defining possible known states for a system and the possible transitions between each state, as well as triggering side effects when a transition occurs.
Redux reducerscan be written as true Finite State Machines, but RTK doesn't include anything to help with this. In practice, they tend to bepartial state machines that really only care about the dispatched action to determine how to update the state. Listeners, sagas, and observables can be used for the "run side effects after dispatch" aspect, but can sometimes require more work to ensure a side effect only runs at a specific time.
XState is a powerful library for defining true state machines and executing them, including managing state transitions based on events and triggering related side effects. It also has related tools for creating state machine definitions via a graphical editor, which can then be loaded into the XState logic for execution.
While there currently is no official integration between XState and Redux, itis possible to use an XState machine as a Redux reducer, and the XState developers have created a useful POC demonstrating using XState as a Redux side effects middleware:
Further Information
- Presentation:The Evolution of Redux Async Logic
- Reason for middleware and side effects:
- Docs and Tutorials:
- Articles and comparisons