Ionic 2 with ngrx, AlertController, LoadController issue - javascript

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>

Related

Unable to access store with pinia

I have a login page hooked up to firebase and I'm trying to use pinia to update the state to login the user after registering. I'm having a problem that whenever it tries to access the state I get "Uncaught (in promise) TypeError: this.userStore is undefined"
Pinia Store:
import { defineStore } from 'pinia'
export default defineStore('user', {
state: () => ({
userLoggedIn: false,
}),
})
RegisterForm:
<script>
import { auth, usersCollection } from "#/includes/firebase";
import { mapWritableState } from 'pinia';
import useUserStore from "#/stores/user";
export default {
name: "registerForm",
computed: {
...mapWritableState(useUserStore, ['userLoggedIn'])
},
mounted() {
console.log(this.userStore.userLoggedIn);
},
console logged the login state for testing, but shows undefined. Not sure what the issue is. I have another store setup the same way and it works just fine.
same for my methods to actually trigger the state from false to true. Having the console log "this.userLoggedIn" works, but does not work in the methods.
methods: {
async register(values) {
this.reg_show_alert = true;
this.reg_in_submission = true;
this.reg_alert_variant = "bg-blue-500";
this.reg_alert_msg = "Please wait! You account is bring created.";
let userCred = null;
try {
userCred = await auth.createUserWithEmailAndPassword(
values.email,
values.password
);
} catch (error) {
this.reg_in_submission = false;
this.reg_alert_variant = "bg-red-500";
this.reg_alert_msg =
"An unexpected error occured. Please try again later.";
return;
}
try {
await usersCollection.add({
name: values.name,
email: values.email,
age: values.age,
country: values.country,
});
} catch (error) {
this.reg_in_submission = false;
this.reg_alert_variant = "bg-red-500";
this.reg_alert_msg =
"An unexpected error occured. Please try again later.";
return;
}
this.userStore.userLoggedIn = true;
this.reg_alert_variant = "bg-green-500";
this.reg_alert_msg = "You account has been created";
console.log(userCred);
},
},
In your store file
export const useUserStore = defineStore("user", {
state: () => ({ userLoggedIn: false })
});
In your, register form
import { mapWritableState } from "pinia";
import { useUserStore } from "#/store";
computed: {
...mapWritableState(useUserStore, [
"userLoggedIn",
]),
},
mounted() {
console.log(this.userLoggedIn);
//You can directly modify your state value.
this.userLoggedIn = true;
console.log(this.userLoggedIn);
},

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

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

Write unit test for reducer but - TypeError: state.commentList is not iterable

this is the commnetsReducer.js file
import { ADD_COMMENT } from "./actionType";
// let initialState = {
// commentList : []
// };
const commnetsReducer = (state = { commentList: [] }, action) => {
switch (action.type) {
case ADD_COMMENT:
return { ...state, commentList: [...state.commentList, action.payload] };
default:
return state;
}
};
export default commnetsReducer;
**this is the unit test for above reducer commnetsReducer.test.js **
import commnetsReducer from "../reducer";
import { ADD_COMMENT } from "../actionType";
// const uuid = require("uuid");
describe("comment reducer ", () => {
it("should returns initial state", () => {
expect(commnetsReducer(undefined, {})).toEqual({
commentList: []
});
});
it("handle action of type SAVE_COMMENT ", () => {
expect(
commnetsReducer([], { type: ADD_COMMENT, payload: "new comment" })
).toEqual({commentList :['new comment']});
});
});
**this is the error I got in console **
enter image description here
commnetsReducer([], { type: ADD_COMMENT, payload: "new comment" })
You've passed in an array as the state. Arrays have no .commentList property, so when your reducer tries to spread state.commentList you get that error from trying to spread undefined.
Instead, pass in a state with the right shape, such as:
commnetsReducer({
commentList: []
}, {
type: ADD_COMMENT,
payload: "new comment"
});

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
}

Angular and firebase - Cannot Map of undefined on first load

It works after the first load, I need to know how to provide a promise to prevent the data mapping without first letting it load. On first load of the site it displays the error below. I think it's cause by not allowing the colleciton of data from the database before trying to map it?
Console error
DashboardComponent_Host.html:1 ERROR TypeError: Cannot read property 'map' of undefined at DashboardComponent.webpackJsonp.../../../../../src/app/dashboard/dashboard.component.ts.DashboardComponent.populateDashboard (dashboard.component.ts:70) at
For context the line 70 is this.goalsLength e.g. the first line that calls the db.
The TS file
ngOnInit() {
this.populateDashboard();
}
populateDashboard() {
// just needs a filter on active = if active its not completed. CHANGE IN DB FROM ACTIVE TO COMPLETED
this.goalsLength = this.progressService.getActiveGoals().map(goals => {
return goals.length;
});
this.visionsLength = this.progressService.getCompletedVisions().map(visions => {
return visions.length;
});
this.opportunitiesLength = this.progressService.getCompletedOpportunities().map(opportunities => {
return opportunities.length;
});
this.actionPlansLength = this.progressService.getCompletedActionPlans().map(actionPlans => {
return actionPlans.length;
});
Service
userId: string;
completedVisions: FirebaseListObservable<VisionItem[]> = null;
activeGoals: FirebaseListObservable<Goal[]> = null;
opportunities: FirebaseListObservable<Goal[]> = null;
actionPlans: FirebaseListObservable<Goal[]> = null;
constructor(private db: AngularFireDatabase,
private afAuth: AngularFireAuth) {
// setting userId returned from auth state to the userId on the service, now we can query
// currently logged in user, using the id. IMPORTANT
this.afAuth.authState.subscribe(user => {
if (user) {
this.userId = user.uid
}
});
}
// Used to get the dashboard values.
getCompletedVisions(): FirebaseListObservable<VisionItem[]> {
if (!this.userId) { return; } // if undefined return null.
this.completedVisions = this.db.list(`visions/${this.userId}`, {
query: {
orderByChild: 'completed',
equalTo: true
}
});
return this.completedVisions;
}
getCompletedOpportunities(): FirebaseListObservable<Goal[]> {
if (!this.userId) { return }; // if undefined return null.
this.opportunities = this.db.list(`goals/${this.userId}`, {
query: {
orderByChild: 'opportunitiesCompleted',
equalTo: true
}
});
return this.opportunities;
}
getCompletedActionPlans(): FirebaseListObservable<Goal[]> {
if (!this.userId) { return }; // if undefined return null.
this.actionPlans = this.db.list(`goals/${this.userId}`, {
query: {
orderByChild: 'allActionPlanFieldsCompleted',
equalTo: true
}
});
return this.actionPlans;
}
// Dashboard related queries.
getActiveGoals(): FirebaseListObservable<Goal[]> {
if (!this.userId) { return }; // if undefined return null.
this.activeGoals = this.db.list(`goals/${this.userId}`, {
query: {
orderByChild: 'completed',
equalTo: true
}
});
return this.activeGoals;
}
Just as the error messages state, you are willing to return FirebaseListObservable from all service functions and subscribing them from component, but service functions will return null when this.userId doesn't exist or unsetted.
The reason this happened is because you are setting this.userId in an asynchronous way(this.afAuth.authState.subscribe).
Just be sure all branches return Observable, for example:
if (!this.userId) { return Observable.of([]); } // provide an empty array if userId is not yet ready

Categories

Resources