Redux-Observable

3.0.0-rc.3 · active · verified Wed Apr 22

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

Warnings

Install

Imports

Quickstart

Demonstrates setting up a Redux store with redux-observable middleware, defining an action, a reducer, and an 'epic' to handle an asynchronous user fetch with error handling, logging state changes over time.

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);

view raw JSON →