Mastering Multiple Actions in Redux Observables with RxJS
Written on
Chapter 1: Introduction to Redux and RxJS
After months of research, I've compiled this guide to help you navigate the complexities of spawning multiple actions using Redux Observables and RxJS.
If you're seeking a quick fix, here's the TL;DR: You can utilize either of() or from() combined with switchMap() or mergeMap() to initiate multiple actions.
For those interested in deeper insights, let's explore Redux, Redux-Observable, and RxJS.
Section 1.1: Understanding Redux and Redux-Observable
Many are familiar with Redux, a state management library rooted in the Flux architecture. However, Redux-Observable may be less known. In essence, Redux-Observable acts as an intermediary, enabling RxJS Observables to feed data into the Redux store.
What are RxJS Observables, you ask? They are primarily associated with Angular but can be employed across various JavaScript frameworks. Observables are entities that emit a sequence of values to subscribers, which can also include other observables. While I won't delve into the technical intricacies here, it's important to note that they serve a crucial purpose.
Observables shine in React and Redux when managing state changes that stem from non-user actions. For instance, in a gaming scenario with a timer, you might use an Observable to relay "ticks" back to Redux. In my work, we leverage Redux-Observable to handle real-time data streamed via WebSocket.
Section 1.2: The Structure of an Epic
In Redux-Observable, an epic is designed to create the observable and facilitate the publication of actions to the Redux reducer. Here's a straightforward example adapted from the RxJS documentation:
import { ajax } from 'rxjs/ajax';
// action creators
const fetchUser = username => ({ type: FETCH_USER, payload: username });
const fetchUserFulfilled = payload => ({ type: FETCH_USER_FULFILLED, payload });
// epic
const fetchUserEpic = action$ => action$.pipe(
ofType(FETCH_USER),
mergeMap(action =>
map(response => fetchUserFulfilled(response)))
)
);
// later...
dispatch(fetchUser('torvalds'));
This code listens for dispatched Redux actions, specifically looking for a FETCH_USER action type. Upon detection, it triggers a fetch request and subsequently dispatches a "fulfilled" action with the received data. While utilizing an RTK Query endpoint might be more effective, this example elucidates the core concept.
Chapter 2: Practical Examples
In the video titled "Ben Lesh - Async Redux Actions With RxJS," you can find an in-depth discussion about handling asynchronous actions in Redux using RxJS.
Section 2.1: Conditional Data Retrieval
Suppose you have the user data, but you need to fetch additional information only if the user has admin privileges. How can this be achieved? This post aims to clarify that process. It may seem straightforward, but the RxJS documentation can be overwhelming, making it challenging to pinpoint the appropriate method for your needs.
Consider the following example:
import { ajax } from 'rxjs/ajax';
// action creators
const fetchUser = username => ({ type: FETCH_USER, payload: username });
const fetchUserFulfilled = payload => ({ type: FETCH_USER_FULFILLED, payload });
// epic
const fetchUserEpic = action$ => action$.pipe(
ofType(FETCH_USER),
mergeMap(action =>
mergeMap(response => {
const newActions = [fetchUserFulfilled(response)];
if (response.isAdmin) {
newActions.push(getAdminStuff(response.id));}
return from(newActions);
})
)
)
);
// later...
dispatch(fetchUser('torvalds'));
The key here is switching from map to mergeMap. While map always returns a single value, mergeMap allows for emitting multiple values, which is essential for this scenario. By using an array to accumulate actions, we can dynamically determine whether to fetch admin data.
Section 2.2: Data Loading Post-Authentication
I faced a situation where I needed to reposition some data-loading actions to execute only after successful authentication and the establishment of a WebSocket connection. Previously, we occasionally dispatched data requests prematurely, leading to subtle bugs.
Here's how this can be structured:
import { ajax } from 'rxjs/ajax';
// action creators
const doAuth = (username, password) => ({ type: DO_AUTH, payload: { username, password } });
const userAuthorized = payload => ({ type: USER_AUTHORIZED, payload });
// epic
const fetchUserEpic = action$ => action$.pipe(
ofType(FETCH_USER),
mergeMap(action => {
const { username, password } = action.payload;
doAuthorization(username, password).pipe(
mergeMap(authData => {
return of(userAuthorized(authData), fetchUser(username), getCatalog());})
)
})
);
// later...
dispatch(doAuth(username, password));
In this example, we utilize of() to create our observable sequence, as we can anticipate the number of additional actions required. Furthermore, there's no need to manually dispatch the fetchUser action from a view; itโs automatically managed after logging in.
I hope this guide proves beneficial to those who have struggled to piece together available resources or for those new to Redux-Observables. If you found this helpful, please leave a clap or comment to support my work.
In Plain English ๐
Thank you for being part of the In Plain English community! Before you leave, consider following us on: X | LinkedIn | YouTube | Discord | Newsletter. Explore our other platforms: Stackademic | CoFeed | Venture. More content available at PlainEnglish.io.