Managing API calls with default error handling in react-redux application - redux

I've completed my application and I'm now integrating the real api calls for each async action. I use redux-thunk which returns a promise from an axios instance.
Currently I'm repeating so much of the same logic in my actions that I'm sure I'm missing something.
API response example
{
"ok": true,
"warnings": [],
"errors": [],
"response": {/* stuff */}
}
The Idea is that I need the same error handling if either the axios call fails (so another response status then 2xx). Additionally I need to also do the same thing when the api response returns "ok": false.
Preferably I would like to dispatch an action which shows a notification to users so they also know when something goes wrong. Aside from that I want to log the api response's warnings and error entities. This is mainly because I'll use sentry for monitoring.
Any Ideas on how to do this without doing a .catch() with the same logic on each api call in any of my action creators?
I've thought about using the onError of axios but that can't dispatch an action as far as I know.

You could use a response interceptor to dispatch appropriate actions. Just wire them up after you create the store
const store = createStore(...)
axios.interceptors.response.use((response) => {
if (!response.data.ok) {
store.dispatch({ type: "RESPONSE_NOT_OK", response }
}
return response
}, (error) => {
store.dispatch({ type: "RESPONSE_HAD_ERROR", error }
return Promise.reject(error)
})
Obviously, you can handle the response how ever you want, this was just for demonstration purposes.

Related

Are concurrent queries via same endpoint possible using useLazyQueryHook in Redux Toolkit

The problem: When doing multiple requests to the same endpoint, useLazyQueryHook will return a response only with the latest query with status set to 'fulfilled' and response data, while other requests made before are stuck with status 'pending' and without response data.
Desired functionality: The lazy query hook does not overwrite the previous returns (if overwrite is the case) but returns data from each query which can be accessed in a useEffect hook f.ex. Is this possibly not the correct approach for this use case when using RTK?
I have the following endpoint within createAPI...
getLoggerCheck: builder.query<any, any>({
query: ({ loggerId }) => ({
url: '/scan/check',
params: {
logger: loggerId,
}
}),
transformResponse(data: any) {
console.log(data); // <---- Correct data for each request is logged out here
return data;
}
})
... but when using the generated useLazyGetLoggerCheckQuery hook like so and making approx 5 requests / sec with runLoggerChecks({loggerId: XXXXX}) only the most recent query is returned with data and status 'fulfilled'.
const [runLoggerCheck, loggerCheckResponse] = useLazyGetLoggerCheckQuery();
useEffect(() => {
console.log(loggerCheckResponse)
}, [loggerCheckResponse]);

Watching for a redux-saga action in redux

All of my API calls are handled by redux-sagas. I'm creating a heartbeat modal in my app to detect inactivity. Each time a saga goes off I want to clear my setTimeout so I know that the user is active.
My middleware is a basic one at the moment:
const heartbeatMonitor => store => next => action {
if (action['##redux-saga/SAGA_ACTION']) {
clearTimeout(window.myTimeout);
}
window.myTimeout = window.setTimeout(function() {
// send off an action to tell user they are inactive
}, 100000);
}
It seems like looking for this symbol, ##redux-saga/SAGA_ACTION, is the only way to tell if the action is a saga. I see that redux-sagas has a createSagaMiddleware(options) and I tried using effectMiddlewares but it doesn't seem like you have access to the dispatch method in there so I can't send off a new actions.
but it doesn't seem like you have access to the dispatch method in there so I can't send off a new actions.
Not sure whether this is the kind of solution you wanted, but you do have access to the dispatch method where your comment // send off an action to tell user they are inactive is located in your code snippet, as it is exposed by the store object. (this is documented in the Store Methods Section of the store in the redux docs)
Therefore something like the following should satisfy your case:
const heartbeatMonitor => store => next => action {
if (action['##redux-saga/SAGA_ACTION']) {
clearTimeout(window.myTimeout);
}
const { dispatch } = store;
window.myTimeout = window.setTimeout(() => {
dispatch({ type: "USER_INACTIVE" });
}, 100000);
}
Note: I would probably implement this differently (using redux-sagas effects) Maybe this is an option for you too:
Example Saga
import { put, delay } from "redux-saga/effects";
function* inactiveSaga() {
yield delay(100000);
yield put({ type: "USER_INACTIVE" })
}
Example Integration of saga above:
(add the following in your root saga)
//import { takeLatest } from "redux-saga/effects";
takeLatest(() => true, inactiveSaga)
Explanation: Every action will trigger the inactiveSaga (cause () => true). The inactiveSaga will wait 100000ms before dispatching the "inactive action". If there is a new action within this waiting time the previous execution of the inactiveSaga will be canceled (cause takeLatest, see redux-saga effect docs for takeLatest) and started from the beginning again. (Therefore no "inactive action" will be sent and the inactiveSaga will start to wait for these 100000ms again, before being cancelled or completing the delay and dispatching the "inactive action")

ngrx/effects not dispatching mapped actions

I've got a pretty straight forward effect defined using the ngrx/effects library.
#Effect()
public Authorize$ = this._actions$.ofType(IdentityActionsService.AUTHORIZE_IDENTITY)
.switchMap(action => this._svc.Authorize$(action.payload))
.catch(err => Observable.of(null).do(() => console.error(err); }))
.map(identity => this._identity.OnIdentityAuthorized(identity))
The #Effect is triggered, authorize$() runs, and the OnIdentityAuthorized() method, which returns an Action ({type: payload: }) fires...
What I expect to happen is that the action returned by OnIdentityAuthorized() should get fed into the appropriate reducer - that is not happening.
I have a debugger call in OnIdentityAuthorized and in the corresponding reducer. The Action returned by OnIdentityAuthorized is not being dispatched. What might cause this? Am I misunderstanding something?
I feel like what I've got is basically identical to example 1 here: https://github.com/ngrx/effects/blob/master/docs/intro.md
EDIT
Added additional code sections... The effect triggers the OnIdentityAuthorized debugger statement, so the observable is emitting all the way through the async authorization call. The reducer case is not triggered...
Here is the OnIdentityAuthorized() implementation:
public static ON_IDENTITY_AUTHORIZED = '[IDENTITY] Authorized';
public OnIdentityAuthorized(identity: Identity | JWT): Action {
debugger;
return {
type: IdentityActionsService.ON_IDENTITY_AUTHORIZED,
payload: identity
};
}
Here is the reducer section:
switch (action.type) {
case IdentityActionsService.ON_IDENTITY_AUTHORIZED:
debugger;
return merge({}, action.payload);
Turns out to have been an issue with the way I was registering the reducers.
I was trying to do something exotic in order to add additional state endpoints for lazy loaded modules... turns out there is an issue there - but that is a different question.

Async Tests - Mocha and Chai - Ensure the done() callback is being called

I'm trying test my container component methods. My container had a async method that load all proposals and set in the state. Example.:
loadProposal(proposalId) {
return axios
.get("http://localhost:9292/api/proposal_drafts/1.json")
.then(response => {
this.setState({
proposal: Immutable.fromJS(response.data)
})
})
}
So, to test this method i get the component instance and call the method (the api url is mocked).
it("sets proposal in the state", (done) => {
const wrapper = shallow(<Container/>)
loadProposalRequest(1)
wrapper.instance().loadProposal(1).then(() => {
chai.expect(wrapper.state().proposal).to.be(Map)
done()
})
})
But i get this error from console:
Error: timeout of 2000ms exceeded. Ensure the done() callback is being
called in this test.
Ops: If i put a console.log(wrapper.state()) inside then() . The log shows my state correctly.
If chai.expect() throws an error (which I think is what's happening), two things will happen:
done won't get called, because of the thrown error;
the error won't get caught, because there's not additional error handling.
You should use Mocha's promise support instead to remove both issues:
it("sets proposal in the state", () => {
const wrapper = shallow(<Container/>)
loadProposalRequest(1)
return wrapper.instance().loadProposal(1).then(() => {
chai.expect(wrapper.state().proposal).to.be(Map)
})
})
You can also use chai-as-promised
you can write code that expresses what you really mean:
return doSomethingAsync().should.eventually.equal("foo");
or if you have a case where return is not preferable (e.g. style considerations) or not possible (e.g. the testing framework doesn't allow returning promises to signal asynchronous test completion), then you can use the following workaround (where done() is supplied by the test framework):
doSomethingAsync().should.eventually.equal("foo").notify(done);

Export / Log to error tracking service

Is there a way to get an export of the store state / actions programmatically in Production that could be imported back into dev tools?
For example I can setup middleware to capture the current state and send that to something like (Trackjs,Sentry, Rollbar) but that lacks all the previous state and actions.
I would like to capture in the same format as exporting from the Redux Dev Tools.
Sample export from Dev Tools
{"monitorState":{},"actionsById":{"0":{"type":"PERFORM_ACTION","action":{"type":"##INIT"},"timestamp":1471017239656},"1":{"type":"PERFORM_ACTION","action":{"type":"INCREMENT"},"timestamp":1471017242004}},"nextActionId":2,"stagedActionIds":[0,1],"skippedActionIds":[],"committedState":5,"currentStateIndex":1,"computedStates":[{"state":5},{"state":6}]}
This is currently in development but you can now push action history right in the extension see https://github.com/zalmoxisus/remotedev-server/pull/20
Another option is to save the actions to a JSON file as an array and import them back in.
That's possible as of https://github.com/zalmoxisus/redux-devtools-extension/issues/173
logger.js
let actions = []
export function logActions (stateSanitizer) {
return store => next => action => {
actions.push(action)
return next(action)
}
}
These actions can be saved to a file or database and can be imported back into the dev tools.
Sample actions
[{
"type": "INCREMENT"
}, {
"type": "DECREMENT"
}, {
"type": "DECREMENT"
}, {
"type": "DECREMENT"
}, {
"type": "DECREMENT"
}]
I created this repo which demos this in action https://github.com/timarney/redux-trackjs-logger it uses middleware to log the actions when an error happens.
I maintain a Redux middleware called Raven for Redux which attaches Redux data to Sentry error reports. Currently it adds the following context to each error report:
The complete state object.
The complete last action object.
The type of all actions that lead up to the current state. These are added as "breadcrumbs".
The Sentry blog has a writeup describing it in more detail: https://blog.sentry.io/2016/08/24/redux-middleware-error-logging.html
You can find the middleware as an NPM package here: https://github.com/captbaritone/raven-for-redux

Resources