Define Redux modules for fetching data from API endpoints
I found myself writing a lot of boilerplate code every time I wanted to fetch data and ingest it into my Redux store. First, I would define some actions for requesting and ingesting the data. Then I would define a reducer to process those actions. Then I would define some middleware or a saga to intercept the "request" action and fire off the "ingest" one once the API call was complete. Establishing all the cross references and integrating the module into my Redux setup was tedious and error prone. More importantly, I quickly realized that for simple cases I was doing the exact same thing every time, with minor variations caused only by slips of memory or spells of laziness. I made redux-endpoints as a way to standardize this type of module definition.
// src/redux-modules/resourceApi/index.js
import { createEndpoint } from 'redux-endpoints';
const endpoint = createEndpoint({
// Module name (required)
name: 'resourceApi',
// Receives the url as a parameter and must return a promise (required)
request: (url, params) => new Promise((resolve, reject) => (
fetch(url, { credentials: 'include' })
.then(resp => resp.json())
.then(json => resolve(json))
)),
// Receives the url params as an argument and returns the key in the state
// where the request data will be stored (optional)
resolver: ({ id }) => id,
// Where in the root-level state the selector function should look for request
// data (optional)
rootSelector: state => state.resourceApi,
// Url pattern for requests - can be function or string (required)
url: '/api/resource/:id', // OR url: ({ id }) => `/api/resource/${id}`,
});
const {
actionCreators,
middleware,
selector,
selectors,
reducer,
} = endpoints;
export {
actionCreators,
middleware,
selector,
selectors,
};
export default reducer;
// src/redux-store/index.js
import { applyMiddleware, createStore } from 'redux';
import endpointReducer, { middleware } from 'redux-modules/resourceApi';
const middleware = applyMiddleware(
middleware,
);
const reducer = combineReducers({
resourceApi: endpointReducer,
});
const store = createStore(reducer, {}, middleware);
// whereveryouwant.js
import store from 'redux-store';
import { actionCreators } from 'redux-modules/resourceApi';
store.dispatch(actionCreators.request({ id: 1 }));
The code above triggers:
- An action,
resourceApi/MAKE_REQUEST
, - A fetch to the url
/api/resource/1
, and - An action,
resourceApi/INGEST_RESPONSE
.
When you call store.dispatch
and pass in the makeRequest
action, the endpoint’s middleware function returns a promise which will be resolved with the ingest action that was subsequently dispatched to the store. (Note that the ingest
action is dispatched before the promise returned from the dispatch
function is resolved.)
// whereveryouwant.js
import store from 'redux-store';
import { actionCreators } from 'redux-modules/resourceApi';
store.dispatch(actionCreators.request({ id: 1 }))
.then(action => {
if (action.error) {
const error = action.payload;
// handle error
}
const result = action.payload;
// handle success
});
If your request is successful, your state will look as follows:
{
resourceApi: {
"1": {
pendingRequests: 0,
completedRequests: 1,
successfulRequests: 1,
data: {
id: 1,
server_attribute: "server_value"
}
}
}
}
If something went wrong with your request and the Promise were rejected, your state would look as follows:
{
resourceApi: {
"1": {
pendingRequests: 0,
completedRequests: 1,
successfulRequests: 0,
data: null,
error: {
message: "Something went wrong with the request",
name: "Error"
}
}
}
}
You can then retrieve information about the request using the selectors generated by the endpoint. See below for a full list of generated selectors.
// whereveryouwant.js
import store from 'redux-store';
import { selectors } from 'redux-modules/resourceApi';
const isPending = selectors.isPendingSelector(store.getState(), { id: 1 });
const data = selectors.dataSelector(store.getState(), { id: 1 });
const error = selectors.errorSelector(store.getState(), { id: 1 });
Required. The name of this redux module. Should be unique in your app. Used to construct the action names, i.e., ${name}/MAKE_REQUEST
and ${name}/INGEST_RESPONSE
.
Required. A function that returns a Promise. The request
function is called by the endpoint's middleware (endpoint.middleware
) when the request
action is fired (endpoint.actionCreators.request
). It takes two arguments:
- The url to request
- The parameters passed the the
makeRequest
action creator.
The data that the Promise resolves (or rejects) with will be passed to the ingest
action creator (endpoint.actionCreators.ingest
) and incorporated into the store at the path determined by the resolver
option (see below). Data the Promise resolves with is stored under the 'data'
key; data the Promise rejects with is stored under the 'error'
key.
Required. A string or a function. If a string, optionally has colon-prefixed url parameters. If a function, takes the parameters passed to the makeRequest
action creator. Should return the url.
Optional. A function that takes the parameters passed to the makeRequest
action creator. Should return the key where the endpoint's data will be stored.
Defaults to a function which returns a default string ('__default__'
).
E.g. in the code above, requesting data with endpoint.actionCreators.request({ id: 1000 })
would result in the data stored under they key '1000'
by the reducer.
Optional. A function that takes the state as its sole parameter and returns the branch of the state the endpoint’s reducer (endpoint.reducer
) is responsible for. So if you call combineReducers({ my_key: endpoint.reducer })
, your rootSelector
would be (state => state.my_key)
. It’s called by the selector
function when retrieving request data from the top-level state. Defaults to (state => state)
.
A reducer to manage the slice of state where you choose to store your data from this url or set of urls.
A redux middleware function. Pass it into your createStore
call to enable the request
action creator to trigger your data requests.
Action creators for this endpoint. See below.
Creates an request
action. The action type is namespaced according to the name
of your endpoint. E.g. in the code above, resourceApi/MAKE_REQUEST
. Takes as its sole argument the parameters used to create both the url
(either via a function or a url string with colon-prefixed parameters) and when calling the resolver
function, if one exists. E.g.,
dispatch(actionCreators.request({ id: 1000 }));
The request
action creator's toString
method returns its action type.
Creates an ingest action. This action creator is called by the middleware once your endpoint's request
Promise resolves or rejects. The ingest action creator is primarily for internal use, but it is exported because its toString
method returns its action type.
A selector for retrieving request data. Its first argument is the state. Its second argument is the parameters passed to the resolver
to determine the path at which the request’s data is stored. The selector
function calls the rootSelector
, if one is provided, and then the resolver
to determine which piece of state you want. E.g. in the code above:
// Retrieve endpoint data for url /api/resource/1000
const endpointData = selector(state, { id: 1000 });
Selectors for working with request data.
Takes the same arguments as endpoint.selector
. Returns the number of completed (either successful or failed) requests.
// Retrieve the number of completed requests to url /api/resources/1000
const numCompletedRequests = endpoint.selectors.completedRequestsSelector(staet, { id: 1000 });
Takes the same arguments as endpoint.selector
. Returns the data returned by the last successful request (or null).
Takes the same arguments as endpoint.selector
. Returns the error thrown by the last request, if it failed. Otherwise, returns null.
Takes the same arguments as endpoint.selector
. Returns true
if there is a request (or multiple requests) pending. Otherwise, returns false
.
Takes the same arguments as endpoint.selector
. Returns the number of successful requests.
Takes the same arguments as endpoint.selector
. Returns true
if any request has been initiated or completed to this url.
Takes the same arguments as endpoint.selector
. Returns true
if any request, whether failed or successful, has been completed o this url.
Takes the same arguments as endpoint.selector
. Returns the number of requests currently pending to this url.