What is redux-observable?
redux-observable is a middleware for Redux that allows you to handle asynchronous actions using RxJS observables. It enables complex async flows in your Redux applications by leveraging the power of reactive programming.
What are redux-observable's main functionalities?
Handling Asynchronous Actions
This feature allows you to handle asynchronous actions such as API calls. The example demonstrates an epic that listens for 'FETCH_USER' actions, makes an AJAX request to fetch user data, and dispatches either a 'FETCH_USER_FULFILLED' or 'FETCH_USER_FAILED' action based on the result.
const fetchUserEpic = action$ => action$.pipe(
ofType('FETCH_USER'),
mergeMap(action =>
ajax.getJSON(`/api/users/${action.payload}`).pipe(
map(response => ({ type: 'FETCH_USER_FULFILLED', payload: response })),
catchError(error => of({ type: 'FETCH_USER_FAILED', payload: error }))
)
)
);
Combining Multiple Epics
redux-observable allows you to combine multiple epics into a single root epic. This is useful for organizing your code and managing complex async flows. The example shows how to combine `fetchUserEpic` with another epic.
const rootEpic = combineEpics(
fetchUserEpic,
anotherEpic
);
Cancellation of Actions
redux-observable supports the cancellation of ongoing actions. The example demonstrates using `switchMap` to cancel any ongoing AJAX request if a new 'FETCH_USER' action is dispatched, ensuring only the latest request is processed.
const fetchUserEpic = action$ => action$.pipe(
ofType('FETCH_USER'),
switchMap(action =>
ajax.getJSON(`/api/users/${action.payload}`).pipe(
map(response => ({ type: 'FETCH_USER_FULFILLED', payload: response })),
catchError(error => of({ type: 'FETCH_USER_FAILED', payload: error }))
)
)
);
Other packages similar to redux-observable
redux-saga
redux-saga is a middleware for Redux that uses generator functions to handle side effects. It is similar to redux-observable in that it helps manage complex async flows, but it uses a different approach based on ES6 generators rather than RxJS observables.
redux-thunk
redux-thunk is a middleware that allows you to write action creators that return a function instead of an action. It is simpler than redux-observable and redux-saga, making it a good choice for handling basic async actions without the need for complex flow control.
redux-promise-middleware
redux-promise-middleware is a middleware that allows you to dispatch promises as actions. It automatically dispatches pending, fulfilled, and rejected actions based on the promise's state. It is less powerful than redux-observable but can be useful for straightforward async operations.
redux-observable (beta)
Creates RxJS 5-based middleware for
Redux.
- Dispatch a function that returns an observable of actions, a promise of action or iterable of actions.
- Function is provided a stream of all actions, useful for composition with the current dispatched observable
(think things like
takeUntil
or zip
) - Function is also provided a reference to the store which can be used to get the state or even dispatch.
Install
NOTE: This has a peer dependencies of rxjs@5.0.*
and redux
, which will have to be installed
as well.
npm i -S rx-ducks-middleware
Usage
Basic
With rxDucksMiddleware, you can dispatch any function that returns an observable,
a promise, an observable-like object or an iterable. The basic call looks like:
dispatch(() => Rx.Observable.of({ type: 'ASYNC_ACTION_FROM_RX' }).delay(1000));
dispatch(() => Promise.resolve({ type: 'ASYNC_ACTION_FROM_PROMISE'}));
dispatch(() => [{ type: 'ACTION_1' }, { type: 'ACTION_2' }]);
dispatch(() => (function* () {
for (let i = 0; i < 10; i++) {
yield { type: 'SOME_GENERATED_ACTION', value: i };
}
}()))
Cancellation
It's recommended to dispatch an action to cancel your async action with Rx. This can be done
by leveraging the first argument to your dispatched function, which returns all actions
. With that
you can use takeUntil
to abort the async action cleanly and via composition.
dispatch((actions) => Observable.timer(1000)
.map(() => ({ type: 'TIMER_COMPLETE'}))
.takeUntil(actions.filter(a => a.type === 'ABORT_TIMER')))
dispatch({ type: 'ABORT_TIMER' });
You can also cancel an async dispatch by using the return value from your dispatch, which is an
Rx Subscription. This works well for other types that don't have cancellation, like promises, but
internally will really use "disinterest" to stop the resolved value from propagating.
let subscription = dispatch(() => Promise.resolve({ type: 'DELAYED_ACTION' }));
subscription.unsubscribe();
Other API Notes
The second argument to your dispatched function will be the store
instance itself. This gives you
the ability to getState()
on your store in case you need to assert some condition before dispatching your
next async message. It also gives you the ability to dispatch()
explicitly.
Basic Dispatch Type Signature
If it helps to think about it this way, in a TypeScript-style type definition, the dispatch function would
look like this when used to dispatch an action asynchronously:
dispatch = ((actions?: Observable<Action>, store?: ReduxStore) => Observable<Action>) => Subscription;
Example
Below is a basic example of it how it might work in React.
import { Component } from 'react';
import { connect } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { rxDucksMiddleware } from 'rx-ducks-middleware';
import * as Rx from 'rxjs';
const reducer = (state = {}, action) => {
switch (action.type) {
case 'DATA_LOADING':
return { ...state, loading: true };
case 'DATA_LOADED':
return { ...state, loading: false, data: action.data };
case 'ABORT_LOAD':
return { ...state, loading: false };
}
return state;
};
const store = createStore(reducer, applyMiddleware(rxDucksMiddleware()));
const loadData = () => (actions, store) => Observable.of('hello world')
.delay(1000)
.map(data => ({ type: 'DATA_LOADED', data })
.startWith({ type: 'DATA_LOADING' })
.takeUntil(actions.filter(a => a.type === 'ABORT_LOAD'));
const abortLoad = () => ({ type: 'ABORT_LOAD' });
const mapStateToProps = ({ data, loading }) => ({ data, loading });
const mapDispatchToProps = (dispatch) => ({
loadData: () => dispatch(loadData()),
abortLoad: () => dispatch(abortLoad())
});
const MyComponent = ({ loading, data, loadData, abortLoad }) => (
<div>
<button onClick={loadData}>load data</button>
<button onClick={abortLoad}>abort load</button>
<div>Loading: {loading}</div>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);
:shipit: