Cannot proper handle error in ajax from rxjs - redux
I write application in React an Redux and I have a case that I have to send request to some resource when specific action was called and response of type 'text/html' write to state. This resource can return status 200 or 404, and I cannot write correct test for case with response 404.
To run tests I use jest library.
Action.js:
export const actions = {
GET_RESOURCE_SUCCESS: 'GET_RESOURCE_SUCCESS',
GET_RESOURCE_FAILURE: 'GET_RESOURCE_FAILURE'
};
export const getResourceSuccess = (response) => ({
type: actions.GET_RESOURCE_SUCCESS,
payload: response
});
export const getResourceFailure = () => ({
type: actions.GET_RESOURCE_FAILURE
});
Reducer.js:
import { handleActions } from 'redux-actions';
import { actions } from './Action';
const initialState = {
content: ''
};
export const getResourceReducer = handleActions(
{
[actions.GET_RESOURCE_SUCCESS]: (state, action) => ({ ...state, content: action.payload })
},
{
[actions.GET_RESOURCE_FAILURE]: () => ({ initialState })
},
initialState
);
In brief: when resource returns status 200 and content exists I want to overwrite content from initialState and call action GET_RESOURCE_SUCCESS, when resource returns status 404 and content doesn't exist I want not to overwrite the content and call action GET_RESOURCE_FAILURE.
GetResourceEpic.js:
import { ajax } from 'rxjs/observable/dom/ajax';
import { combineEpics } from 'redux-observable';
import { Observable } from 'rxjs';
import { getResourceSuccess, getResourceFailure } from '../Action';
const specificActionTypes = [
'SPECIFIC_ACTION_ONE',
'SPECIFIC_ACTION_TWO'
];
const getResource = () => ajax({
method: 'GET',
url: 'http://example.com',
headers: {
Accept: 'text/html'
},
crossDomain: true,
responseType: 'text/html'
});
const getResourceEpic = (action$, store) => action$
.filter(action => specificActionTypes.includes(action.type))
.flatMap(() => getResource()
// when response has status 200 and field response call getResourceSuccess
.map(({ response }) => getResourceSuccess(response))
// when response has status 404 and doesn't have field response call getResourceFailure
.catch(() => {
// helper statement to show in browser that the .catch() was called
console.log('Error');
return getResourceFailure();
})
);
export default combineEpics(
getResourceEpic
);
And it works in generally, but I get two errors:
first:
Uncaught TypeError: You provided an invalid object where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
at Object.subscribeToResult (subscribeToResult.js:73)
at CatchSubscriber../node_modules/rxjs/operator/catch.js.CatchSubscriber.error (catch.js:111)
at MapSubscriber../node_modules/rxjs/Subscriber.js.Subscriber._error (Subscriber.js:128)
at MapSubscriber../node_modules/rxjs/Subscriber.js.Subscriber.error (Subscriber.js:102)
at AjaxSubscriber../node_modules/rxjs/Subscriber.js.Subscriber._error (Subscriber.js:128)
at AjaxSubscriber../node_modules/rxjs/Subscriber.js.Subscriber.error (Subscriber.js:102)
at XMLHttpRequest.xhrReadyStateChange (AjaxObservable.js:327)
at XMLHttpRequest.d (raven.js:363)
and second:
Could not consume error: TypeError: Cannot read property 'length' of null
at getLinesAround (http://localhost:3000/static/js/bundle.js:47197:74)
at http://localhost:3000/static/js/bundle.js:47537:402
at Array.map (native)
at _callee2$ (http://localhost:3000/static/js/bundle.js:47517:54)
at tryCatch (http://localhost:3000/static/js/bundle.js:58220:40)
at Generator.invoke [as _invoke] (http://localhost:3000/static/js/bundle.js:58458:22)
at Generator.prototype.(anonymous function) [as next] (http://localhost:3000/static/js/bundle.js:58272:21)
at step (http://localhost:3000/static/js/bundle.js:47553:191)
at http://localhost:3000/static/js/bundle.js:47553:361
raven.js:51
And mentioned above problems don't allow me to write a tests, because only responses with status 200 pass the tests, other throws errors.
it('should dispatch GET_RESOURCE_SUCCESS when SPECIFIC_ACTION_ONE was dispatched', async () => {
store = mockStore();
const response = 'some content';
nock('http://example.com')
.get('/')
.reply(200, response);
const payload = { type: 'SPECIFIC_ACTION_ONE' };
// specificActionOne() produces and action of type 'SPECFIC_ACTION_ONE'
const action$ = ActionsObservable.of(specificActionOne(payload));
const resultAction = await getResourceEpic(action$, store).toPromise();
expect(resultAction)
.toEqual(getResourceSuccess(response));
});
And test above pass, but case with status equal to 404 doesn't pass:
it('should dispatch GET_RESOURCE_FAILURE when SPECIFIC_ACTION_ONE was dispatched', async () => {
store = mockStore();
nock('http://example.com')
.get('/')
.reply(404);
const payload = { type: 'SPECIFIC_ACTION_ONE' };
const action$ = ActionsObservable.of(specificActionOne(payload));
const resultAction = await getResourceEpic(action$, store).toPromise();
expect(resultAction)
.toEqual(getResourceFailure());
});
And test above doesn't pass and I get the result:
TypeError: You provided an invalid object where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
at Object.subscribeToResult (node_modules/rxjs/util/subscribeToResult.js:73:27)
at CatchSubscriber.Object.<anonymous>.CatchSubscriber.error (node_modules/rxjs/operator/catch.js:111:42)
at MapSubscriber.Object.<anonymous>.Subscriber._error (node_modules/rxjs/Subscriber.js:128:26)
at MapSubscriber.Object.<anonymous>.Subscriber.error (node_modules/rxjs/Subscriber.js:102:18)
at AjaxSubscriber.Object.<anonymous>.Subscriber._error (node_modules/rxjs/Subscriber.js:128:26)
at AjaxSubscriber.Object.<anonymous>.Subscriber.error (node_modules/rxjs/Subscriber.js:102:18)
at XMLHttpRequest.xhrReadyStateChange [as onreadystatechange] (node_modules/rxjs/observable/dom/AjaxObservable.js:327:32)
at XMLHttpRequest.callback.(anonymous function) (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:289:32)
at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:219:27)
at invokeInlineListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:166:7)
at EventTargetImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:122:7)
at EventTargetImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:87:17)
at XMLHttpRequest.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:61:35)
at readyStateChange (node_modules/jsdom/lib/jsdom/living/xmlhttprequest.js:829:9)
at Request.properties.client.on (node_modules/jsdom/lib/jsdom/living/xmlhttprequest.js:947:7)
at Request.emit (events.js:132:15)
at IncomingMessage.<anonymous> (node_modules/request/request.js:1085:12)
at Object.onceWrapper (events.js:219:13)
at IncomingMessage.emit (events.js:132:15)
at endReadableNT (_stream_readable.js:1101:12)
at process._tickCallback (internal/process/next_tick.js:114:19)
The function passed to catch has to return an observable. You are returning an action.
Instead, you should do this:
import { of } from 'rxjs/observable/of';
...
.catch(() => {
console.log('Error');
return of(getResourceFailure());
})
Related
How to set error in Redux toolkit array if no data returned from API
i started implementing RTK QUERYnow i am facing one issue , i have an API,the API returns user Settings. Now from API if the there is data related to user it returns success=1 otherwise success=0 , but in both cases it returns response status 200. Now i want to show error to user if sucess=0 and set the set the error in the RTk Error object. How can i implement that. import { apiSlice } from "../../services/ApiSlice"; import * as URL from "../Urls"; export const userSlice = apiSlice.injectEndpoints({ endpoints: (builder) => ({ getUserSettings: builder.mutation({ query: (body) => ({ url: URL.GET_SETTINGS, method: "POST", body: body, responseHandler: (response) => response.json(), validateStatus: (response, result) => response.status === 200 ? result : "error in fetching data", }), transformResponse: (response) => { console.log({ response }); if (response.success === 0) return "Error in fetching data"; }, providesTags: (result, error, arg) => { if (result) { console.log(result); return [...result.ids.map((id) => ({ type: "users", id })), "users"]; } else return ["users"]; }, }), }), }); export const { useGetUserSettingMutation } = userSlice;
How to pass parameter from thunk to extraReducers?
I know extraReducers can receive payload data, but can it receive patameter directly from thunk method? Normally I have this in extraReducers: .addCase(loginEmail.fulfilled, (state, { payload }) => { state.authnRes = payload }) But I would get data from thunk itself, how can I pass it? export const logout = createAsyncThunk(`${namespace}/logout`, async () => { const { data } = await axios({ method: 'post', url: 'logout', headers: { crossDomain: true }, }) return data })
By returning it - your return data already does it. Whatever you return from the thunk will end up as action.payload in that reducer.
Dispatch an action from ApolloClient onError
I am using Redux and Apollo Client in my project. I would like to dispatch an action to trigger a message bar on top, stating the session is expired, when GraphQL endpoint returns 401 (Session expired). But the client object is not an component, I cannot use useDispatch here, is there any way I can dispatch an action in onError? import { InMemoryCache } from "apollo-cache-inmemory"; import ApolloClient from "apollo-client"; import { onError } from "apollo-link-error"; import { HttpLink } from "apollo-link-http"; const customFetch = (uri, options) => { return fetch(uri, options) .then(response => { console.log("response fetch", response) switch (response.status) { case 400: case 401: case 500: case 504: return Promise.reject(response); default: return response; } }); }; const requestLink = new HttpLink({ uri: "/graphql", fetch: customFetch, }); const errorLink = onError(({ graphQLErrors, networkError, response }) => { if (graphQLErrors) graphQLErrors.map(({ message, extensions }) => { console.log(`[GraphQL error]: Message: ${message}, Location: ${extensions.code}`); }); if (networkError) { // TODO: Dispatch error to trigger a message bar component appear console.log("[GraphQL networkError], ", networkError); } }); const link = errorLink.concat(requestLink); const client = new ApolloClient({ link: errorLink.concat(requestLink), cache: new InMemoryCache({ addTypename: false, }), }); export default client;
Api middleware with redux-observable
I'm refactoring my react/redux app to use redux-observable instead of redux-thunk. Using thunk, I have an api middleware set up to listen for any actions with a CALL_API key and do some manipulation of the data, prepare headers, prepare full url, perform an api call using axios, and also do some additional action dispatches related to an api call. Importantly, the api middleware dispatches a REQUEST_START action which gives the request an id and sets its status to pending in the network part of my state. When the promise from axios resolves or rejects, the middleware dispatches a REQUEST_END action, updating the state so that the current request is set to resolved or rejected. Then the response is returned to the calling action creator that initially dispatched the CALL_API action. I have not been able to figure out how to do this with redux-observable. The part about the api middleware described above that I want to replicate is the REQUEST_START and REQUEST_END action dispatches. It's very convenient to have a centralized place where all api call related stuff is handled. I know I can effectively dispatch the REQUEST_START and REQUEST_END actions in each of my epics that does an api call, but I don't want to have to repeat the same code in many places. I managed to partially solve this by creating an apiCallEpic which listens for actions with type CALL_API and does the above setup for api calls. However, an issue (or rather, something I don't like) is that the epic that initiates the api call (e.g. getCurrentUserEpic) essentially gives up control to apiCallEpic. So, for example, when the api call succeeds and has a response, I may want to format that response data in some way before dispatching an action to be handled by my reducer. That is, getCurrentUserEpic should do some formatting of data returned from api call before sending to reducer. I was able to achieve something close to this by passing a payloadHandler callback function defined in getCurrentUserEpic that the apiCallEpic can call if/when it gets a successful response. However, I don't like this callback architecture and it seems like there's got to be a better way. Here is some code that demonstrates my use of api middleware using thunk. import axios from 'axios'; // actionCreators.js // action types const CALL_API = "CALL_API"; const FETCH_CURRENT_USER = "FETCH_CURRENT_USER"; const RECEIVE_CURRENT_USER = "RECEIVE_CURRENT_USER"; // action creators for request start and end export const reqStart = (params = {}) => (dispatch) => { const reduxAction = { type: REQ_START, status: 'pending', statusCode: null, requestId: params.requestId, } dispatch(reduxAction); } export const reqEnd = (params = {}) => (dispatch) => { const { requestId, response = null, error = null, } = params; let reduxAction = {} if (response) { reduxAction = { type: REQ_END, status: 'success', statusCode: response.status, requestId, } } else if (error) { if (error.response) { reduxAction = { type: REQ_END, status: 'failed', statusCode: error.response.status, requestId, } } else { reduxAction = { type: REQ_END, status: 'failed', statusCode: 500, requestId, } } } dispatch(reduxAction); } // some api call to fetch data export const fetchCurrentUser = (params = {}) => (dispatch) => { const config = { url: '/current_user', method: 'get', } const apiCall = { [CALL_API]: { config, requestId: FETCH_CURRENT_USER, } } return dispatch(apiCall) .then(response => { dispatch({ type: RECEIVE_CURRENT_USER, payload: {response}, }) return Promise.resolve({response}); }) .catch(error => { return Promise.reject({error}); }) } // apiMiddleware.js // api endpoint const API_ENTRY = "https://my-api.com"; // utility functions for request preparation export const makeFullUrl = (params) => { // ...prepend endpoint url with API_ENTRY constant return fullUrl } export const makeHeaders = (params) => { // ...add auth token to headers, etc. return headers; } export default store => next => action => { const call = action[CALL_API]; if (call === undefined) { return next(action); } const requestId = call.requestId; store.dispatch(reqStart({requestId})); const config = { ...call.config, url: makeFullUrl(call.config), headers: makeHeaders(call.config); } return axios(config) .then(response => { store.dispatch(reqEnd({ response, requestId, })) return Promise.resolve(response); }) .catch(error => { store.dispatch(reqEnd({ error, requestId, })) return Promise.reject(error); }) } // reducers.js // Not included, but you can imagine reducers handle the // above defined action types and update the state // accordingly. Most usefully, components can always // subscribe to specific api calls and check the request // status. Showing loading indicators is one // use case. Here's the code I've implemented to accomplish a similar thing with redux-observable. export const fetchCurrentUserEpic = (action$, state$) => { const requestType = FETCH_CURRENT_USER; const successType = RECEIVE_CURRENT_USER; const requestConfig = { url: "/current_user", method: "get", } const payload = {requestConfig, requestType, successType}; const payloadNormalizer = ({response}) => { return {currentUser: response.data.data}; } return action$.ofType(FETCH_CURRENT_USER).pipe( switchMap((action) => of({ type: CALL_API, payload: {...payload, requestId: action.requestId, shouldFail: action.shouldFail, payloadNormalizer}, })), ) } export const apiEpic = (action$, state$) => { return action$.ofType(CALL_API).pipe( mergeMap((action) => ( concat( of({type: REQ_START, payload: {requestId: action.payload.requestId, requestType: action.payload.requestType}}), from(callApi(action.payload.requestConfig, action.payload.shouldFail)).pipe( map(response => { return { type: action.payload.successType, payload: action.payload.payloadNormalizer({response}) } }), map(() => { return { type: REQ_END, payload: {status: 'success', requestId: action.payload.requestId, requestType: action.payload.requestType}, } }) ) ) ).pipe( catchError(error => { console.log('error', error); return of({type: REQ_END, payload: {status: 'failed', requestId: action.payload.requestId, requestType: action.payload.requestType}, error}); }) ) ) ) } Any comments or suggestions are appreciated!
I've found redux-fetch-epic-builder (A lib for building "fetch actions" and generic epics handled by redux-observable) to be similar to what you are trying to achieve here (beware it uses rxjs 5, this guide to rescue). It uses fetch, not axios, but it's easy to replace that. Plus it has transformers for successful/failed actions. The library is a bit old, but the base idea to overcome boilerplate code is still valid: Generic epic-builder to fetch data with calls to API(s). I am a novice in React / Redux / RxJS, but the only problem I see with the redux-fetch-epic-builder is the way to configure the client (in axios terms). That is, I am not fully satisfied with (due to it being not FSA or RSAA): //action creators const getComments = (id, page = 1) => ({ type: GET_COMMENTS, host: 'http://myblog.com', path: `/posts/${id}/comments`, query: { page, }, }) // ... const epics = [ buildEpic(GET_COMMENTS), ] but this may still be an elegant way. And the license allow to develop the library further. I have not converted the example from the library documentation to your user-related example, but with react-observable there is certainly no need to introduce a separate "api middleware". (Also, I like /SUBACTION better than _SUBACTION, but it's trivial to change.)
Redux Async actions returns me an error: Actions must be plain objects. Use custom middleware for async actions
I am struggling with the async Redux (thunk). I trully don't understand what I am doing wrong with my async actions and why I get the error : Error: Actions must be plain objects. Use custom middleware for async actions. export async function startLocalizationFetchingAsync(currentLocalizationState) { return (dispatch) => { let payload = { request: { sent:true, } }; dispatch({ type: "NEW_LOCALIZATION_REQUEST_SENT2", payload: payload, }); return axios.get("http://freegeoip.net/json/"+currentLocalizationState.clientIP) .then(res => { res = res.data; var payload = { country: res.country_name||'', }; dispatch({ type: "NEW_LOCALIZATION", payload: payload, }); }) .catch(function (error) { console.log("Promise Rejected",error); dispatch({ type: "NEW_LOCALIZATION_REQUEST_ERROR", payload: null, }); }); }; } while in the index.js router i have the following code async action({ next, store }) { // Execute each child route until one of them return the result const route = await next(); await store.dispatch(startLocalizationFetchingAsync()); this generates me an error: Error: Actions must be plain objects. Use custom middleware for async actions. dispatch webpack:///~/redux/es/createStore.js:153 http://myskyhub.ddns.net:3000/assets/client.js:9796:16 http://myskyhub.ddns.net:3000/assets/vendor.js:46309:16 Object.dispatch webpack:///~/redux-thunk/lib/index.js:14 Object._callee$ webpack:///src/routes/index.js?a731:35 tryCatch webpack:///~/regenerator-runtime/runtime.js:65 Generator.invoke webpack:///~/regenerator-runtime/runtime.js:303 Generator.prototype.(anonymous webpack:///~/regenerator-runtime/runtime.js:117 http://myskyhub.ddns.net:3000/assets/3.9645f2aeaa83c71f5539.hot-update.js:8:361 while the config store is the following const middleware = [thunk.withExtraArgument(helpers), thunk.withExtraArgument(AsyncMiddleware)]; let enhancer; if (__DEV__) { middleware.push(createLogger()); //middleware.push(AsyncMiddleware()); enhancer = compose( applyMiddleware(...middleware), devToolsExtension, ); } else { enhancer = applyMiddleware(...middleware); } initialState.localization = defaultLocalization; //Location // See https://github.com/rackt/redux/releases/tag/v3.1.0 const store = createStore(rootReducer, initialState, enhancer); What I am doing wrong? I don't understand the redux-thunk...