Redux-Observable
redux-observable is an RxJS-based middleware for Redux, designed to manage complex asynchronous side effects and compose/cancel async actions using "Epics." Epics are functions that take a stream of actions (action$) and the current state as an observable (state$), returning a stream of actions, enabling powerful reactive programming patterns within a Redux application. It offers a declarative alternative to redux-thunk or redux-saga by leveraging RxJS operators for filtering, transforming, and orchestrating action streams. The current latest version is 3.0.0-rc.3, actively in development, which maintains compatibility with RxJS v7. Previous stable versions like 2.x.x also supported RxJS v7. The project generally has an as-needed release cadence, focusing on critical fixes and peer dependency compatibility. Key differentiators include its tight integration with the RxJS ecosystem, providing robust tools for cancellation, debouncing, and complex observable-based logic that might be more verbose or imperative with other middleware solutions.
Common errors
-
TypeError: action$.ofType(...).switchMap is not a function
cause Using RxJS v5/6 prototype operators with `redux-observable` v1+ that expects pipeable operators, or using old `ofType` syntax with v2+.fixEnsure `redux-observable` version matches your RxJS version (v1 for RxJS 6, v2+ for RxJS 7). Use pipeable operators like `action$.pipe(ofType(...), switchMap(...))` and import `ofType` from `redux-observable/operators`. -
Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported
cause Attempting to `require()` an ESM-only module or a package configured for ESM in a CommonJS context (e.g., Node.js with default module resolution).fixIf your project is ESM, use `import` statements. If CJS, check if `redux-observable` or its dependencies offer a CJS build or configure your build system (e.g., Webpack, Rollup) to handle module resolution correctly. Ensure `package.json` `type: 'module'` is set if using ESM in Node. -
TypeError: Cannot read property 'subscribe' of undefined
cause An RxJS operator or function that expects an Observable was provided `undefined`, often due to an API call returning nothing or a stream terminating unexpectedly.fixStep through the epic logic with a debugger to identify which observable source or operator is producing `undefined` instead of an Observable. Ensure all branches of your epic return a valid Observable. -
Type 'Observable<any>' is not assignable to type 'Epic<Action, Action, State>'
cause Incorrect TypeScript typing for an Epic, or an Epic function returning a type that doesn't match the `Epic` generic signature.fixEnsure your Epic function explicitly returns `Observable<Action>` and adheres to the `Epic<InputActions, OutputActions, State>` generic signature. For example, use `ofType<ActionType, 'SOME_ACTION'>('SOME_ACTION')` for better type narrowing.
Warnings
- breaking The `ofType()` operator, introduced in v1, was a method on `action$` (`ActionsObservable`). In v2 and later, `ofType` became a pipeable RxJS operator and must be explicitly imported from `redux-observable/operators`.
- breaking The `createEpicMiddleware` API changed in v1. You no longer pass your `rootEpic` directly to `createEpicMiddleware()`. Instead, you must call `epicMiddleware.run(rootEpic)` after the Redux store has been created and middleware applied.
- breaking redux-observable v1 requires RxJS v6.x, and v2/v3 require RxJS v7.x. Incompatible RxJS versions will lead to runtime errors or missing operators.
- gotcha Errors thrown or unhandled within an Epic's observable stream can terminate the entire stream, preventing the epic from reacting to future actions. Proper error handling (e.g., using `catchError` within inner streams) is crucial.
- gotcha Epics receive actions *after* they have passed through reducers. An Epic that simply returns its input `action$` stream (e.g., `action$ => action$`) will create an infinite loop of dispatches.
- breaking Direct access to `store.dispatch()` and `store.getState()` as epic arguments was removed in v1. Instead, epics receive a `StateObservable` (aliased as `state$`) which provides the current state via `state$.value` and can be composed as an Observable.
Install
-
npm install redux-observable -
yarn add redux-observable -
pnpm add redux-observable
Imports
- createEpicMiddleware
const { createEpicMiddleware } = require('redux-observable')import { createEpicMiddleware } from 'redux-observable' - combineEpics
const { combineEpics } = require('redux-observable')import { combineEpics } from 'redux-observable' - ofType
import { ofType } from 'redux-observable'import { ofType } from 'redux-observable/operators' - Epic
import { Epic } from 'redux-observable'import { type Epic } from 'redux-observable'
Quickstart
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { createEpicMiddleware, combineEpics, Epic } from 'redux-observable';
import { of, from } from 'rxjs';
import { catchError, mergeMap, ofType, tap, map } from 'rxjs/operators';
// --- 1. Define Actions ---
interface FetchUserRequestAction { type: 'FETCH_USER_REQUEST'; payload: string; }
interface FetchUserSuccessAction { type: 'FETCH_USER_SUCCESS'; payload: { id: string; name: string; }; }
interface FetchUserFailureAction { type: 'FETCH_USER_FAILURE'; payload: string; }
type UserAction = FetchUserRequestAction | FetchUserSuccessAction | FetchUserFailureAction;
const fetchUserRequest = (userId: string): FetchUserRequestAction => ({
type: 'FETCH_USER_REQUEST',
payload: userId,
});
const fetchUserSuccess = (user: { id: string; name: string; }): FetchUserSuccessAction => ({
type: 'FETCH_USER_SUCCESS',
payload: user,
});
const fetchUserFailure = (error: string): FetchUserFailureAction => ({
type: 'FETCH_USER_FAILURE',
payload: error,
});
// --- 2. Define Reducer ---
interface UserState {
user: { id: string; name: string; } | null;
loading: boolean;
error: string | null;
}
const initialState: UserState = {
user: null,
loading: false,
error: null,
};
const userReducer = (state: UserState = initialState, action: UserAction): UserState => {
switch (action.type) {
case 'FETCH_USER_REQUEST':
return { ...state, loading: true, error: null };
case 'FETCH_USER_SUCCESS':
return { ...state, loading: false, user: action.payload };
case 'FETCH_USER_FAILURE':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
};
const rootReducer = combineReducers({
user: userReducer,
});
export type RootState = ReturnType<typeof rootReducer>;
// --- 3. Define Epic ---
// A mock API call
const fetchUserApi = (userId: string): Promise<{ id: string; name: string; }> => {
console.log(`[Epic] Simulating API call for user: ${userId}`);
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId === '123') {
resolve({ id: '123', name: 'John Doe' });
} else if (userId === 'error') {
reject('User not found or network error');
} else {
resolve({ id: userId, name: `User ${userId}` });
}
}, 1000);
});
};
const fetchUserEpic: Epic<UserAction, UserAction, RootState> = (action$, state$) => action$.pipe(
ofType<UserAction, 'FETCH_USER_REQUEST'>('FETCH_USER_REQUEST'),
tap(action => console.log(`[Epic] Caught FETCH_USER_REQUEST for ID: ${action.payload}`)),
mergeMap(action =>
from(fetchUserApi(action.payload)).pipe( // Convert Promise to Observable
map(user => fetchUserSuccess(user)),
catchError(error => of(fetchUserFailure(error.toString())))
)
)
);
// --- Combine all epics ---
const rootEpic = combineEpics(
fetchUserEpic
);
// --- 4. Create Store and run Epic Middleware ---
const epicMiddleware = createEpicMiddleware<UserAction, UserAction, RootState>();
const store = createStore(
rootReducer,
applyMiddleware(epicMiddleware)
);
epicMiddleware.run(rootEpic);
// --- 5. Dispatch Actions ---
console.log('Initial state:', store.getState());
store.dispatch(fetchUserRequest('123'));
setTimeout(() => {
console.log('State after first request:', store.getState());
store.dispatch(fetchUserRequest('456'));
}, 1500);
setTimeout(() => {
console.log('State after second request:', store.getState());
store.dispatch(fetchUserRequest('error'));
}, 3000);
setTimeout(() => {
console.log('Final state:', store.getState());
}, 4500);