redux-requests
Manages in-flight requests with a Redux reducer - avoid issuing duplicate requests without any special logic!
Live Example!
Avoiding the issue of multiple requests
Say your application has two views for the same set of data, and this data has not yet been fetched. A naïve approach would be to create an Action Creator which fetches the data from an HTTP API endpoint, and then have both of these views trigger this action as soon as they are rendered (componentWillMount
in React terms).
The problem with this approach is that you end up with two identical HTTP requests when you only need one! You waste bandwidth doing this, and you may also waste render cycles as the Store updates twice as a result of handling both identical responses.
How can we fix this?
You could wrap all your calls to fetch the data with if
statements, and keep track of that state somewhere, but who wants to do that by hand?
Enter: redux-requests
This library will not only keep track of all pending requests for you, but also provide a convenient middleware function that will avoid dispatching Actions to request data if there is already a pending HTTP request for this data in flight!
As a result, you can use the very same naïve approach outlined earlier with hardly any code changes and it will "just work"! Keep your views stateless and your Reducers ignorant of the notion of "pending requests"!
Before and after
The only changes you should have to make to your code are:
- Add
meta.httpRequest
fields to your Action objects
meta.httpRequest.url
is required, and will be used as the unique identifier for the requestmeta.httpRequest.done
is a boolean indiecating if this action corresponds to a beginning or ending part of the request sequence
- Typically a successful response Action, in addition to a failed response Action with an error, will both have
meta.httpRequest.done = true
- Check if the
dispatch
for your initial request Action was cancelled (dispatch
will return undefined
), and if so do not issue your request - Register the
createRequestMiddleware
middleware and the requestsReducer
reducer as part of your Redux configuration.
Original code:
class Repos extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
this.props.loadRepos(this.props.username);
}
render() {
return (
<div>
{ this.props.repos }
</div>
);
}
}
function mapStateToProps(state) {
return {
repos: state.githubRepos
};
}
function mapDispatchToProps(dispatch) {
return {
loadRepos: (userId) => { dispatch(loadRepos(userId)); }
};
}
export const ReposComponent = connect(mapStateToProps, mapDispatchToProps)(Repos);
export function loadRepos(userId) {
return function (dispatch, getState) {
const url = `https://api.github.com/users/${userId}/repos`;
dispatch({
type: 'LOAD_REPOS',
payload: {
userId
}
});
fetch(url)
.then(response => dispatch({
type: 'LOAD_REPOS',
payload: {
userId,
response
}
}))
.catch(error => dispatch({
type: 'LOAD_REPOS',
error: true,
payload: {
userId,
error
}
})
);
}
}
const createStoreWithMiddleware = applyMiddleware(thunkMiddleware)(createStore);
let store = createStoreWithMiddleware(combineReducers({ githubRepos }));
New code:
export function loadRepos(userId) {
return function (dispatch, getState) {
const url = `https://api.github.com/users/${userId}/repos`;
if (!dispatch({
type: 'LOAD_REPOS',
payload: {
userId
},
meta: {
httpRequest: { url, done: false }
}
})) {
return;
}
fetch(url)
.then(response => dispatch({
type: 'LOAD_REPOS',
payload: {
userId,
response
},
meta: {
httpRequest: { url, done: true }
}
}))
.catch(error => dispatch({
type: 'LOAD_REPOS',
error: true,
payload: {
userId,
error
},
meta: {
httpRequest: { url, done: true }
}
})
);
}
}
import { requestsReducer, createRequestMiddleware } from 'redux-requests';
const createStoreWithMiddleware = applyMiddleware(thunkMiddleware, createRequestMiddleware())(createStore);
let store = createStoreWithMiddleware(combineReducers({ requestsReducer, githubRepos }));
API
requestsReducer(state, action)
A reducer that keeps track of pending request state. It only operates on actions containing the meta.httpRequest
field.
createRequestMiddleware(stateSelectorFunction)
Returns a middleware function to pass to applyMiddleware
. Optionally pass a stateSelectorFunction
which returns where the requestsReducer
keeps its state in the Store (if not passed, will default to state => state.requests
).
Ex: applyMiddleware(createRequestMiddleware(state => state.pendingHttpRequests))(createStore)
Credits
Inspired by the Marty fetch API.