{"id":17469,"library":"redux-observable","title":"Redux-Observable","description":"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.","status":"active","version":"3.0.0-rc.3","language":"javascript","source_language":"en","source_url":"https://github.com/redux-observable/redux-observable","tags":["javascript","Rx","Ducks","Reducks","Redux","middleware","observable","thunk","async","typescript"],"install":[{"cmd":"npm install redux-observable","lang":"bash","label":"npm"},{"cmd":"yarn add redux-observable","lang":"bash","label":"yarn"},{"cmd":"pnpm add redux-observable","lang":"bash","label":"pnpm"}],"dependencies":[{"reason":"Core state management library that redux-observable integrates with. Version 5.x is a peer dependency for v3.","package":"redux","optional":false},{"reason":"Core reactive programming library providing Observables and operators. Version 7.x is a peer dependency for v3.","package":"rxjs","optional":false}],"imports":[{"note":"ESM import is preferred. CommonJS require() pattern is for older Node.js environments or build setups that transpile ESM to CJS. Since v1, createEpicMiddleware no longer takes the root epic directly; you must call .run(rootEpic) on the middleware instance after store creation.","wrong":"const { createEpicMiddleware } = require('redux-observable')","symbol":"createEpicMiddleware","correct":"import { createEpicMiddleware } from 'redux-observable'"},{"note":"Used to compose multiple individual epics into a single root epic. ESM import is standard.","wrong":"const { combineEpics } = require('redux-observable')","symbol":"combineEpics","correct":"import { combineEpics } from 'redux-observable'"},{"note":"Since v2, ofType is a pipeable operator and must be imported from 'redux-observable/operators', not directly from 'redux-observable'. In v1.x, it was available directly on the action$ observable via ActionsObservable.","wrong":"import { ofType } from 'redux-observable'","symbol":"ofType","correct":"import { ofType } from 'redux-observable/operators'"},{"note":"This is a TypeScript type for defining an Epic. Use 'type' import for clarity and to ensure it's removed at compile-time if your TypeScript version and configuration supports it.","wrong":"import { Epic } from 'redux-observable'","symbol":"Epic","correct":"import { type Epic } from 'redux-observable'"}],"quickstart":{"code":"import { createStore, applyMiddleware, combineReducers } from 'redux';\nimport { createEpicMiddleware, combineEpics, Epic } from 'redux-observable';\nimport { of, from } from 'rxjs';\nimport { catchError, mergeMap, ofType, tap, map } from 'rxjs/operators';\n\n// --- 1. Define Actions ---\ninterface FetchUserRequestAction { type: 'FETCH_USER_REQUEST'; payload: string; }\ninterface FetchUserSuccessAction { type: 'FETCH_USER_SUCCESS'; payload: { id: string; name: string; }; }\ninterface FetchUserFailureAction { type: 'FETCH_USER_FAILURE'; payload: string; }\n\ntype UserAction = FetchUserRequestAction | FetchUserSuccessAction | FetchUserFailureAction;\n\nconst fetchUserRequest = (userId: string): FetchUserRequestAction => ({\n  type: 'FETCH_USER_REQUEST',\n  payload: userId,\n});\n\nconst fetchUserSuccess = (user: { id: string; name: string; }): FetchUserSuccessAction => ({\n  type: 'FETCH_USER_SUCCESS',\n  payload: user,\n});\n\nconst fetchUserFailure = (error: string): FetchUserFailureAction => ({\n  type: 'FETCH_USER_FAILURE',\n  payload: error,\n});\n\n// --- 2. Define Reducer ---\ninterface UserState {\n  user: { id: string; name: string; } | null;\n  loading: boolean;\n  error: string | null;\n}\n\nconst initialState: UserState = {\n  user: null,\n  loading: false,\n  error: null,\n};\n\nconst userReducer = (state: UserState = initialState, action: UserAction): UserState => {\n  switch (action.type) {\n    case 'FETCH_USER_REQUEST':\n      return { ...state, loading: true, error: null };\n    case 'FETCH_USER_SUCCESS':\n      return { ...state, loading: false, user: action.payload };\n    case 'FETCH_USER_FAILURE':\n      return { ...state, loading: false, error: action.payload };\n    default:\n      return state;\n  }\n};\n\nconst rootReducer = combineReducers({\n  user: userReducer,\n});\n\nexport type RootState = ReturnType<typeof rootReducer>;\n\n// --- 3. Define Epic ---\n// A mock API call\nconst fetchUserApi = (userId: string): Promise<{ id: string; name: string; }> => {\n  console.log(`[Epic] Simulating API call for user: ${userId}`);\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      if (userId === '123') {\n        resolve({ id: '123', name: 'John Doe' });\n      } else if (userId === 'error') {\n        reject('User not found or network error');\n      } else {\n        resolve({ id: userId, name: `User ${userId}` });\n      }\n    }, 1000);\n  });\n};\n\nconst fetchUserEpic: Epic<UserAction, UserAction, RootState> = (action$, state$) => action$.pipe(\n  ofType<UserAction, 'FETCH_USER_REQUEST'>('FETCH_USER_REQUEST'),\n  tap(action => console.log(`[Epic] Caught FETCH_USER_REQUEST for ID: ${action.payload}`)),\n  mergeMap(action =>\n    from(fetchUserApi(action.payload)).pipe( // Convert Promise to Observable\n      map(user => fetchUserSuccess(user)),\n      catchError(error => of(fetchUserFailure(error.toString())))\n    )\n  )\n);\n\n// --- Combine all epics ---\nconst rootEpic = combineEpics(\n  fetchUserEpic\n);\n\n// --- 4. Create Store and run Epic Middleware ---\nconst epicMiddleware = createEpicMiddleware<UserAction, UserAction, RootState>();\n\nconst store = createStore(\n  rootReducer,\n  applyMiddleware(epicMiddleware)\n);\n\nepicMiddleware.run(rootEpic);\n\n// --- 5. Dispatch Actions ---\nconsole.log('Initial state:', store.getState());\n\nstore.dispatch(fetchUserRequest('123'));\n\nsetTimeout(() => {\n  console.log('State after first request:', store.getState());\n  store.dispatch(fetchUserRequest('456'));\n}, 1500);\n\nsetTimeout(() => {\n  console.log('State after second request:', store.getState());\n  store.dispatch(fetchUserRequest('error'));\n}, 3000);\n\nsetTimeout(() => {\n  console.log('Final state:', store.getState());\n}, 4500);\n","lang":"typescript","description":"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."},"warnings":[{"fix":"Change `action$.ofType(...)` to `action$.pipe(ofType(...))` and `import { ofType } from 'redux-observable/operators';`","message":"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`.","severity":"breaking","affected_versions":">=2.0.0"},{"fix":"Update your middleware setup from `createEpicMiddleware(rootEpic)` to `const epicMiddleware = createEpicMiddleware(); const store = createStore(reducer, applyMiddleware(epicMiddleware)); epicMiddleware.run(rootEpic);`","message":"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.","severity":"breaking","affected_versions":">=1.0.0"},{"fix":"Ensure your RxJS version matches the peer dependency of your installed `redux-observable` version. For v2+, install `rxjs@^7.0.0`.","message":"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.","severity":"breaking","affected_versions":">=1.0.0"},{"fix":"Always place `catchError()` within `mergeMap` or `switchMap` operators, *after* the asynchronous operation, to prevent the main `action$` stream from terminating. Emit an error action or handle the error gracefully.","message":"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.","severity":"gotcha","affected_versions":">=0.14.0"},{"fix":"Epics must transform or filter actions. If an epic doesn't need to produce an output action, use `ignoreElements()` or ensure it completes/filters appropriately to prevent feedback loops.","message":"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.","severity":"gotcha","affected_versions":">=0.1.0"},{"fix":"Use `state$.value` for imperative state access or compose `state$` with other observables for reactive state changes. Emit new actions via the epic's return stream instead of `dispatch()`.","message":"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.","severity":"breaking","affected_versions":">=1.0.0"}],"env_vars":null,"last_verified":"2026-04-22T00:00:00.000Z","next_check":"2026-07-21T00:00:00.000Z","problems":[{"fix":"Ensure `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`.","cause":"Using RxJS v5/6 prototype operators with `redux-observable` v1+ that expects pipeable operators, or using old `ofType` syntax with v2+.","error":"TypeError: action$.ofType(...).switchMap is not a function"},{"fix":"If 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.","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).","error":"Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported"},{"fix":"Step 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.","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.","error":"TypeError: Cannot read property 'subscribe' of undefined"},{"fix":"Ensure 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.","cause":"Incorrect TypeScript typing for an Epic, or an Epic function returning a type that doesn't match the `Epic` generic signature.","error":"Type 'Observable<any>' is not assignable to type 'Epic<Action, Action, State>'"}],"ecosystem":"npm","meta_description":null}