Redux: Unhandle Rejection (Error) on a try-catch statement - javascript

I have a reducer that is intended for handling notification banners.
const notifReducer = (state = { notifMessage: null, notifType: null, timeoutID: null },
action
) => {
switch (action.type) {
case 'SET_NOTIFICATION':
if (state.timeoutID) {
clearTimeout(state.timeoutID)
}
return {
notifMessage: action.notifMessage,
notifType: action.notifType,
timeoutID: null
}
case 'REMOVE_NOTIFICATION':
return {
notifMessage: null,
notifType: null,
timeoutID: null
}
case 'REFRESH_TIMEOUT':
return {
...state,
timeoutID: action.timeoutID
}
default:
return state
}
}
export const setNotification = (notifMessage, notifType) => {
return async dispatch => {
dispatch({
type: 'SET_NOTIFICATION',
notifMessage,
notifType
})
let timeoutID = await setTimeout(() => {
dispatch({
type: 'REMOVE_NOTIFICATION'
})
}, 5000)
dispatch({
type: 'REFRESH_TIMEOUT',
timeoutID
})
}
}
export default notifReducer
It works fully fine in the rest of my app, except in this one event handler that uses a try-catch. If I intentionally trigger the catch statement (by logging in with a bad username/password), I get "Unhandle Reject (Error): Actions must be plain objects. Use custom middleware for async action", but I am already using redux-thunk middleware!
const dispatch = useDispatch()
const handleLogin = async (event) => {
event.preventDefault()
try {
const user = await loginService.login({
username, password
})
//
} catch (exception) {
dispatch(setNotification(
'wrong username or password',
'error')
)
}
}
edit:
here is my store.js contents
const reducers = combineReducers({
blogs: blogReducer,
user: userReducer,
notification: notifReducer,
})
const store = createStore(
reducers,
composeWithDevTools(
applyMiddleware(thunk)
)
)

I hope your question is answered in a post already. Please check the below link
Error handling redux-promise-middleware

Related

Why my try catch block won't execute ? - React

I have a register modal which has 3 step.
Fill up the info, getActivate Code and Success Message.
I Want when user filled to inputs and clicked the submit button if there is no error move to next Step
but if there is error then React Toastify will show the message.
My problem is why try catch block in MyComponent dosen't work ?
P.S: I'm using Formik and Yup
httpService
const handleExpectedError = (response: any) => {
if (response?.status >= 400 && response?.status < 500) {
const errors = response?.data?.errors;
const errPropertyName: string[] = Object.keys(errors);
toast.error(errors?.[errPropertyName?.[0]]?.[0]);
}
};
export const handleRegister = async (user: User): Promise<void> => {
try {
await axios.post(`${config.apiEndPoint}/auth/register`, user, header);
} catch ({ response }) {
handleExpectedError(response);
}
};
MyComponent
const [step, setStep] = useState(1);
const formik = useFormik({
initialValues: {
firstName: "",
lastName: "",
email: "",
phoneNumber: "",
password: "",
},
onSubmit: (value) => {
if (terms) {
handleNextStep(value);
}
},
validationSchema: registerSchema,
});
// Event Handler
// Value below is a referance to Formik object
const handleNextStep = async (value: any) => {
if (step === 1) {
try {
await handleRegister(value);
setStep(step + 1);
await handeGetActivateCode({ email: value.email });
} catch (error) {
setStep(1);
}
}
if (step !== 1) return setStep(step - 1);
};
In httpService file, you have used try-catch. In that catch you are trying to get the error in the curly braces, instead of doing like that if you do the following thing. then the catch block will work fine
export const handleRegister = async (user: User): Promise<void> => {
try {
await axios.post(`${config.apiEndPoint}/auth/register`, user, header);
} catch (response) {
handleExpectedError(response);
}
};

Redux actions always in pending state

I am trying to create a scraping application using redux toolkit for learning purposes.Whenever I dispatch the action the data gets scraped and console logged but the action state is never fullfilled and is always pending
MY ASYNC THUNK
export const loadData = createAsyncThunk(
"alldata/getdata",
async ({ pageNo, language }, thunkAPI) => {
const data = await fetch(
`http://localhost:5000/scrape?pageNo=${encodeURIComponent(
pageNo
)}&language=${encodeURIComponent(language)}`
);
const res=await data.json()
return {
payload: res,
};
}
);
MY SLICE
const projectSlice = createSlice({
name: "allprojects",
initialState: {
projectState: [],
workingState: [],
isLoading: false,
hasError: false,
},
reducers: {
addProject: (state, action) => {
return state.workingState.push(action.payload);
},
removeProject: (state, action) => {
return state.workingState.filter(
(project) => project.link !== action.payload.link
);
},
},
extraReducers: {
[loadData.pending]: (state, action) => {
state.isLoading = true;
state.hasError = false;
},
[loadData.fulfilled]: (state, { payload }) => {
state.projectState = payload;
state.isLoading = false;
state.hasError = false;
},
[loadData.rejected]: (state, action) => {
state.isLoading = false;
state.hasError = true;
},
},
});
export const { addProject, removeProject } = projectSlice.actions;
const Projectreducer = projectSlice.reducer;
export default Projectreducer;
export const projectSelector = (state) => state.allprojects;
REACT COMPONENT
const { workingState, projectState, isLoading, hasError } =
useSelector(projectSelector);
const dispatch = useDispatch();
const [selectData, setSelectData] = React.useState({ languages: "" });
const [pageData, setPageData] = React.useState({ pageNo: 1 });
const handleClick = (event) => {
event.preventDefault();
dispatch(
loadData({ pageNo: pageData.pageNo, language: selectData.languages })
);
};
So how do I get the action to be fullfilled and push the data in the ProjectState array after the async request
EDIT:
API
app.get("/scrape", async (req, res) => {
const { pageNo, language } = req.query;
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto(
`https://github.com/search?p=${pageNo}&q=language%3A${language}`,
{
waitUntil: "domcontentloaded",
}
); // URL is given by the "user" (your client-side application)
const data = await page.evaluate(() => {
const list = [];
const items = document.querySelectorAll(".repo-list-item");
for (const item of items) {
list.push({
projectName: item.querySelector(".f4 > a").innerText,
about: item.querySelector("p").innerText,
link: item.querySelector("a").getAttribute("href"),
});
}
return list;
});
console.log(data);
await browser.close();
});
Store
import { configureStore } from "#reduxjs/toolkit";
import Projectreducer from "./Slices/slice";
export const store = configureStore({
reducer: {
allprojects: Projectreducer,
},
});
Its possible that the api that you are fetching is throwing an error so in this case it always recommended to have a catch block and throw an error to that its falls into loadData.rejected state.
So, do the check network tab in the dev tools of the browser that you are using, so that you can confirm if the api is responding or not.
Also can you share the projectSelector selector ? could be the something wrong in the selector.
action:
export const loadData = createAsyncThunk(
'alldata/getdata',
async ({ pageNo, language }, { rejectWithValue }) => {
try {
const data = await fetch(
`http://localhost:5000/scrape?pageNo=${encodeURIComponent(
pageNo
)}&language=${encodeURIComponent(language)}`
);
const res = await data.json();
return {
payload: res,
};
} catch (error) {
return rejectWithValue({ payload: error?.message || error });
}
}
);
reducer:
extraReducers: {
...,
[loadData.rejected]: (state, { payload }) => {
state.isLoading = false;
state.hasError = true;
state.message = payload;
},
},
One more thing to mention here is that redux toolkit recommends to use builder callback, for more details check here:
https://redux-toolkit.js.org/api/createslice#extrareducers
in your example it would be like
extraReducers: (builder) => {
builder
.addCase(loadData.pending, (state) => {
...
})
.addCase(loadData.fulfilled, (state, action) => {
...
})
.addCase(loadData.rejected, (state, action) => {
...
});

Unable to test redux-observable epic

I intend to write unit test for the following epic
// Actions
const actionCreator = actionCreatorFactory('PARENT_DIRECTORY');
export const fetchPage = actionCreator.async<Page, ParentPage>('FETCH_PAGE');
export const fetchParentDirectoryEpic: Epic = action$ =>
action$.pipe(
filter(fetchPage.started.match),
mergeMap((action) => {
return getDirectoryPage(action.payload).pipe(
map(response => fetchPage.done({ params: action.payload, result: response.response })),
catchError(error => of(fetchPage.failed({ params: action.payload, error: error })))
);
})
);
I mocked the getDirectoryPage like below -
import { AjaxResponse, AjaxError } from 'rxjs/ajax';
import { Observable, of } from 'rxjs';
export function getDirectoryPage(page: any): Observable<AjaxResponse> {
switch (page.index) {
case 0:
return Observable.create({'data': [], page: 0, pages: 1});
default:
return Observable.create(observer => {
return new AjaxError('Something bad happened!', null, null);
});
}
}
and following is how my unit test looks like -
describe('fetchParentDirectoryEpic Epic', () => {
it('dispatches the correct actions when it is successful', async (done) => {
const expectedOutputAction = outputAction;
fetchParentDirectoryEpic(inputAction, initialState, null)
.subscribe(actualOutputAction => {
expect(actualOutputAction).toBe(expectedOutputAction)
done()
}
);
});
});
Issue is that the call to fetchParentDirectoryEpic(inputAction, initialState, null) results in an Observable which doesn't have subscribe method. As I understand, the method is available with ActionObservable but I am unable to create its instance using a payload.
The issue was related to how I was creating expectedOutputAction. Its supposed to be an Action and not an ActionObservable.
After setting expectedOutputAction in the following manner, test worked out fine -
expectedOutputAction = {
type: fetchPage.done.type,
result: {'data': [], page: 0, pages: 1},
params: inputAction.payload
}

Use redux-thunk with redux-promise-middleware in the correct way

I'm working in a project with react and redux, I'm enough new so I'm trying to understand better how to use redux-thunk and redux-promise together.
Below you can see my files, in my actions I created a fetch generic function apiFetch() in order to use every time I need to fetch. This function return a promise, that I'm going to resolve in loadBooks(), the code is working and the records are uploaded but when I check the log of the actions I see that the first action is undefined, after there is BOOKS_LOADING, LOAD_BOOKS, BOOKS_LOADING and LOAD_BOOKS_SUCCESS.
I've 2 questions about that:
1) Why is the first action undefined and I've LOAD_BOOKS instead than LOAD_BOOKS_START?
action # 22:54:37.403 undefined
core.js:112 prev state Object {activeBook: null, booksListing: Object}
core.js:116 action function (dispatch) {
var url = './src/data/payload.json';
dispatch(booksIsLoading(true));
return dispatch({
type: 'LOAD_BOOKS',
payload: new Promise(function (resolve) {
…
core.js:124 next state Object {activeBook: null, booksListing: Object}
action # 22:54:37.404 BOOKS_LOADING
action # 22:54:37.413 LOAD_BOOKS
action # 22:54:39.420 BOOKS_LOADING
action # 22:54:39.425 LOAD_BOOKS_SUCCESS
2) If for example the url for the fetch is wrong, I expected to see the action LOAD_BOOKS_ERROR, instead this is the result of the log:
action # 23:06:06.837 undefined action # 23:06:06.837 BOOKS_LOADING
action # 23:06:06.846 LOAD_BOOKS GET
http://localhost:8000/src/data/payldoad.json 404 (Not Found) error
apiFetch Error: request failed at index.js:66 error
TypeError: Cannot read property 'json' of undefined at index.js:90
If I don't use apiFetch(), but normal fetch function, all is working correctly, also the part of the error, with the exception that anyway LOAD_BOOKS is not LOAD_BOOKS_START.
Thank you in advance for any help!
configureStore.js
import { createStore, applyMiddleware, compose, preloadedState } from 'redux';
import reducers from './configureReducer';
import configureMiddleware from './configureMiddleware';
const middleware = configureMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducers, preloadedState, composeEnhancers(applyMiddleware(...middleware)));
export default store;
actions/index.js
import fetch from 'isomorphic-fetch';
export const booksIsLoading = (bool) => {
return {
type: 'BOOKS_LOADING',
booksLoading: bool,
};
};
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
export const apiFetch = (url) => {
const getPromise = () => (
fetch(url, {
method: 'GET',
})
.then((response) => {
if (response.status !== 200) {
throw Error('request failed');
}
return response;
})
.catch((err) => {
console.log('error apiFetch', err);
// dispatch(fetchBooksError(true));
})
);
return getPromise();
};
export const loadBooks = () => (dispatch) => {
const url = './src/data/payload.json';
dispatch(booksIsLoading(true));
return dispatch({
type: 'LOAD_BOOKS',
payload: new Promise((resolve) => {
delay(2000).then(() => {
apiFetch(`${url}`)
// fetch(`${url}`, {
// method: 'GET',
// })
.then((response) => {
resolve(response.json());
dispatch(booksIsLoading(false));
}).catch((err) => {
console.log('error', err);
});
});
}),
});
};
constants/application.js
export const LOAD_BOOKS = 'LOAD_BOOKS';
reducers/reducer_book.js
import initialState from '../model.js';
import * as types from '../constants/application';
export default function (state = initialState, action) {
switch (action.type) {
case `${types.LOAD_BOOKS}_SUCCESS`: {
console.log('reducer', action.payload);
const data = action.payload.data.items;
const items = Object.values(data);
if (items.length > 0) {
return {
...state,
books: Object.values(data),
booksFetched: true,
booksError: false,
};
}
return state;
}
case `${types.LOAD_BOOKS}_ERROR`: {
return {
...state,
booksError: true,
};
}
case 'BOOKS_LOADING':
return {
...state,
booksLoading: action.booksLoading,
};
default:
return state;
}
}
In which order did you specify middlewares?
Following usage makes action go undefined:
'applyMiddleware(reduxPromiseMiddleware(), reduxThunk)'
Please change the order to: ( thunk first! )
'applyMiddleware(reduxThunk, reduxPromiseMiddleware())'

Ionic 2 with ngrx, AlertController, LoadController issue

Excuse my ignorance, I am fairly new to the reactive concepts.
My issue is with not knowing how to deal loading a Ionic 2 loader or an Ionic 2 alert based on the stores current state.
I have been able to achieve the loader behaviour I need by subscribing to the store slice it is reacting to. Although when it comes to an alert (thrown on a catched error), it never fires in the subscription block.
Any help pointing out a better direction, or what I have missed would be greatly appreciated.
This code is from the signin modals view.
signin(user) {
this.submitAttempt = true;
if (this.signinForm.valid) {
let loader = this.loadingCtrl.create({
content: "Signing In..."
});
let auth;
let signinSub = this.store.select(s => auth = s.auth).subscribe(() => {
if (auth.state) {
loader.dismiss();
} else if (auth.error) {
let alert = this.alertCtrl.create({
title: "Error",
subTitle: auth.error,
buttons: ['OK']
});
loader.dismiss();
alert.present();
}
});
loader.present();
this.store.dispatch(UserActions.UserActions.signinUser(user));
}
}
Effect
#Effect() signinUser$ = this.actions$
.ofType(UserActions.ActionTypes.SIGNIN_USER)
.map(toPayload)
.switchMap(user => {
return Observable.fromPromise(this.userService.signinUser(user))
.map(result => {
return ({ type: "GET_USER", payload: user});
})
.catch(err => {
return Observable.of({ type: "SIGNIN_USER_FAILED", payload: err });
});
});
Service
signinUser(user): Promise<any> {
return <Promise<any>>firebase.auth()
.signInWithEmailAndPassword(user.email, user.password);
}
Reducer
export const UserReducer: ActionReducer<Auth> = (state: Auth = initialState, action: Action) => {
switch(action.type) {
case UserActions.ActionTypes.SIGNIN_USER:
return state;
case UserActions.ActionTypes.SIGNIN_USER_FAILED:
return Object.assign(state, { apiState: "Failed", error: action.payload.message });
case UserActions.ActionTypes.STARTED_SIGNIN:
return Object.assign(state, { requested: true });
case UserActions.ActionTypes.GET_USER:
return Object.assign(state, { apiState: "Success", error: ""});
case UserActions.ActionTypes.GET_USER_SUCCESS:
return Object.assign({ user: action.payload.val() }, state, { state: true });
default:
return state;
};
}
store
export interface Auth {
state: boolean,
requested: boolean,
apiState: string,
error: {},
user?: {}
}
export interface AppState {
auth: Auth;
}
I just have a loadingState in my store and then I load and unload the spinner/loading UI based on that state.
I have a complete project here showing how I manage the state and the UI
https://github.com/aaronksaunders/ngrx-simple-auth
/**
* Keeping Track of the AuthenticationState
*/
export interface AuthenticationState {
inProgress: boolean; // are we taking some network action
isLoggedIn: boolean; // is the user logged in or not
tokenCheckComplete: boolean; // have we checked for a persisted user token
user: Object; // current user | null
error?: Object; // if an error occurred | null
}
and then in the different states, AuthActions.LOGIN
case AuthActions.LOGIN: {
return Object.assign({}, state, {inProgress: true, isLoggedIn: false, error: null})
}
and then, AuthActions.LOGIN_SUCCESS
case AuthActions.LOGIN_SUCCESS: {
return Object.assign({}, state, {inProgress: false, user: action.payload, isLoggedIn: true})
}
here is how we handle it in the LoginPage
var dispose = this.store.select('authReducer').subscribe(
(currentState: AuthenticationState) => {
console.log("auth store changed - ", currentState);
if (currentState.user) {
dispose.unsubscribe();
this.nav.setRoot(HomePage, {});
}
// this is where the magic happens...
this.handleProgressDialog(currentState);
this.error = currentState.error
},
error => {
console.log(error)
}
);
}
how we handle loading
/**
*
* #param _currentState
*/
handleProgressDialog(_currentState) {
if (_currentState.inProgress && this.loading === null) {
this.loading = this.loadingCtrl.create({
content: "Logging In User..."
});
this.loading.present()
}
if (!_currentState.inProgress && this.loading !== null) {
this.loading && this.loading.dismiss();
this.loading = null;
}
}
I use Ionic 2 with ngrx too and so far as I know, LoadingController and AlertController don't provide any observable or promise. So I think the best you can do is what you're doing now by subscribing its state and do some condition based on its state.
OR you can get rid LoadingController replace it with ion-spinner:
<ion-spinner [disabled]="isLoading$ | async"></ion-spinner>
And replace AlertController with some label :
<span>{{errorMessage$ | async}}</span>

Categories

Resources