agraboso / agraboso/redux-api-middleware
API calls that dispatch multiple actions for keeping API response data and other stuff (like metadata, etc.) in separate parts of the store? (With sample code)
- Ngôn ngữ chính
- JavaScript
- Star
- 1.5k
- Fork
- 190
- Chỉ số merge pull request
- Không có pull request nào được merge trong 30 ngày
Mô tả
I'm working on what is basically a CMS and has about ~30 or so different model classes that are rather inter-related. The easiest way to work with them is definitely with `normalizr` as suggested. Now, for my redux store object, I'm thinking the best way to set things up will be:
``` js
store = {
// Any loaded entities from the API go here
entities: {
// merged directly from from normalizr `entities`
modelA: {
1: { // ...
// ...
},
modelB: { // ...
// ...
},
// Keep track of all other normal state for managing UI, pagination, etc...
modelAListView: {
page: 1,
selected: [1, 2, 3, 4, 5] // the ID numbers of the objects currently in view
// ...
},
modelBDetailView: {
result: 17 // id number of modelB in view
}
// etc
}
```
So, my question is, in order to get this to happen with `redux-api-middleware`, I need a reducer for the `state.entities` (the `apiReducer`), and then my individual reducers as normal for all of the different views and such.
But then, I have to dispatch two separate actions to make sure that (A) the `apiReducer` receives updates whenever the API gets called, and (B) the appropriate model reducer receives updates when the API call involves that particular model.
I have worked out a solution to do this using `redux-thunk`, but I would really appreciate any feedback on this approach. So far it's working very nicely, and means my actual API calls are super simple to make from within my redux action creators. I would love to know if there is a better way anyone else has come up with!
So, first, here's my helper utility to make API requests with secondary action effects:
``` js
// redux/helpers/makeApiRequest.js
// set a default API root (or headers, credentials, etc.) so we don't need to type these everywhere
const apiRoot = '/api/v1';
export function makeApiRequest (options) {
const {method = 'GET', path, query, schema, secondaryActionTypes, ...rest} = options;
const endpoint = url.format({query, pathname: path.join(apiRoot, path)});
let apiAction;
// return a function that takes dispatch and add the `redux-thunk` middleware
return dispatch => {
if (Array.isArray(secondaryActionTypes) && secondaryActionTypes.length === 3) {
// These are API hits that require a secondary update in a related reducer
apiAction = {
[CALL_API]: {
method, endpoint,
types: [{
type: secondaryActionTypes[0],
payload: () => dispatch({type: API_REQUEST}),
}, {
type: secondaryActionTypes[1],
payload: (action, state, res) => onSuccess(dispatch, res, schema),
// see helper function below
}, {
type: secondaryActionTypes[2],
payload: (action, state, res) => onFailure(dispatch, res),
// see helper function below
}],
},
};
} else {
// This is a normal `redux-api-middleware` type action for actions
// that don't require updates specifically to the API entities reducer
apiAction = {[CALL_API]: {method, endpoint, ...rest}};
}
return dispatch(apiAction);
};
}
function onSuccess (dispatch, res, schema) {
return getJSON(res)
.then(json => {
const data = normalize(json, schema);
// Dispatch the API Action (will merge with `entities` branch of store)
dispatch({
type: API_SUCCESS,
payload: {
entities: data.entities,
},
});
// Payload for the secondary action type, will typically be merged into
// a related model reducer somewhere else in the store
return {
result: data.result,
};
});
}
function onFailure (dispatch, res) {
return getJSON(res)
.then(json => {
// Same as the default error action from `redux-api-middleware`
const payload = new ApiError(res.status, res.statusText, json);
// Send to the API reducer and return for the secondary reducer
dispatch({type: API_FAILURE, payload});
return payload;
});
}
```
Now the next one is super simple, to update the `entities` branch of the store:
``` js
// redux/reducers/entities.js
const initialState = {
modelA: {},
modelB: {},
// etc.
};
function reducer (state = initialState, action) {
switch (action.type) {
case API_REQUEST:
// ...
case API_SUCCESS:
if (action.payload && action.payload.entities) {
return Object.assign({}, state, action.payload.entities);
}
break;
case API_FAILURE:
// ...
default: return state;
}
}
export default reducer;
```
Now calling the API from any action is as easy as:
``` js
function getModelA (query) {
const endpoint = 'model_a';
return apiRequest({
endpoint, query,
schema: Schemas.MODEL_A,
secondaryActionTypes: [MODEL_A_REQEST, MODEL_A_SUCCESS, MODEL_A_FAILURE],
});
}
```
and I will have access to all of the data I need in reducers that handle both API actions and MODEL_A related actions.
Comments/feedback/suggestions? Thanks!
Hướng dẫn đóng góp
Đánh giá
Issue này chưa được đánh giá.