27
loading...
This website collects cookies to deliver better user experience
actions.ts
export const userAddRequest = createAction(
'[User] User Add Request',
props<{username: string}>()
);
export const userAddSuccess= createAction(
'[User] User Add Success',
props<{username: string, id: number}>()
)
export const userAddFailure = createAction(
'[User] User Add Failure',
props<{message: string}>()
)
effect.ts
userAddRequest$ = createEffect(() =>
this.actions$.pipe(
ofType(userActions.userAddRequest ),
exhaustMap(({username}) =>
this.userService.add({username}).pipe(
map(response => userActions.userAddSuccess(response)),
catchError((error: any) => of(userActions.userAddFailure(error))))
)
)
);
userAddSuccess$ = createEffect(
() =>
this.actions$.pipe(
ofType(userActions.userAddSuccess),
tap(() => {
alert('User Add Succeeded');
})
),
{ dispatch: false }
);
userAddFailure$ = createEffect(
() =>
this.actions$.pipe(
ofType(userActions.userAddFailure),
tap(() => {
alert('User Add Failed');
})
),
{ dispatch: false }
);
reducer.ts
export interface State {
users: User[];
isLoading: boolean;
isLoadingSuccess: boolean;
isLoadingFailure: boolean;
}
const reducer = createReducer(
initialState,
on(userActions.userAddRequest, (state) => ({isLoading: true})),
on(userActions.userAddSuccess, (state, {user}) => ({users: state.users.concat(user) , isLoading: false, isLoadingSuccess: true})),
on(userActions.userAddFailure, (state, {user}) => ({user, isLoading: false, isLoadingFailure: true})),
);
selector.ts
export const getLoaded = (state: State) => state.isLoadingSuccess;
export const getLoading = (state: State) => state.isLoading;
getUsers
deleteUser
createUser
loader
store in our application that we use for the single purpose of tracking actions and their state. It allows us to track the current state of any dispatched Ngrx action that we wish to follow.{isLoading: true, isSucceded: true}
:export enum ActionState {
LOADING = 'LOADING',
SUCCEEDED = 'SUCCEEDED',
FAILED = 'FAILED',
}
set
. Each Action in a set
would share a key with others, and we would use it to update the state of that operation in our loaders
store.createAction with method
. It will allow us to add some metadata
alongside the Action definition. This metadata
can be anything.export const userAddSetkey = '[User] User Add Key';
export const userAddRequest = createAction(
'[User] User Add Request',
function prepare(payload: {username: string, password: string}) {
return withLoader({ [userAddSetkey]: ActionState.LOADING }, payload);
}
);
export const userAddSuccess = createAction(
'[User] User Add Success',
function prepare() {
return withLoader({ [userAddSetkey]: ActionState.SUCCEEDED }, null);
}
);
export const userAddFailure = createAction(
'[User] User Add Failure',
function prepare(payload: { message: string }) {
return withLoader({ [userAddSetkey]: ActionState.FAILED }, payload);
}
);
prepare
because it is more similar to the redux prepare
. This function adds additional information to the payload and the actions when they are initialized.withLoader
around our payload. This method will add a property key that will be the same for each Action in our application that implements the tracking. That property will be helpful to know if the dispatched Action contained a state tracker or not.export const LOADER_KEY = '@ngrx-custom-loader';
export type WithLoader<T> = T & {
[LOADER_KEY]: { [k: string]: ActionState };
};
export function withLoader<T>(loader: Partial<{ [k: string]: ActionState }>, payload?: T) {
return Object.assign(payload || {}, { [LOADER_KEY]: loader }) as WithLoader<T>;
}
withLoader
, we will access a new property alongside type
and payload
that will store the action
key and the state
. We define this new Action structure as WithLoader<T>
.{
@ngrx-custom-loader: {'[User] User Add Key': 'LOADING'}
type: "[User] User Add Request"
payload: {username: 'jhon'}
}
loader
store that will save the state
of all actions implementing withLoader
.state.ts
export interface State {
actionState: Record<string, ActionState>;
}
export interface LoadersPartialState {
readonly [LOADERS_FEATURE_KEY]: State;
}
export const initialState: State = {
actionState: {},
};
{}
and will grow every time an action is dispatched to look something like this.{
'[Login] Login Key': 'SUCCEEDED',
'[User] User Add Request': 'LOADING',
...
}
LOADER_KEY
assigned above. If yes, we will store this action state; else, it will do nothing.reducer.ts
export function reducer(
state: State | undefined = initialState,
action: Action | WithLoader<Action>
) {
if (Object.prototype.hasOwnProperty.call(action, LOADER_KEY)) {
const loader = (action as WithLoader<Action>)[LOADER_KEY];
return {
...state,
actionState: {
...state.actionState,
...loader,
},
};
}
return state;
}
selector.ts
export const getIsLoading = (actions: string[] = []) =>
createSelector(getLoadersState, (state) => {
if (actions.length === 1) {
return state.actionState[actions[0]] === ActionState.LOADING;
}
return actions.some((action) => {
return state.actionState[action] === ActionState.LOADING;
});
});
// We added an additional state INIT used when the operation has never been called.
export const getLoadingState = (action: string) =>
createSelector(
getLoadersState,
(state) => state.actionState?.[action] || ActionState.INIT;
);
// The user are getting loaded
this.store.dispatch(loadUsersList());
this.usersLoading$ = this.store.pipe(
select(getIsLoading([userListLoadSetKey]))
);
// A user is being delete
// for this case you also need to store what user it getting deleted to show the feedback on the correct row.
InDeletionUserId = userId;
this.store.dispatch(deleteUser({ id: userId }));
this.userDeleting$ = this.store.pipe(
select(getIsLoading([userDeleteSetKey]))
);
// A user is being created
this.store.dispatch(createUser({ id: accountId }));
this.userCreating$ = this.store.pipe(
select(getIsLoading([userAddSetKey]))
);
// Any of the above is loading
this.isUserStoreLoading$ = this.store.pipe(
select(
getIsLoading([userListLoadSetKey, userDeleteSetKey, userAddSetKey])
)
);
getLoadingState
, you can also track when an operation is finished; helpful in those rare cases where you would like to execute a side effect to Actions outside of an NGRx effect. For example, reset a form when a user is created :onSubmit() {
this.form.controls.username.disable();
this.store.dispatch(userAddRequest({ ...this.form.getRawValue() }));
this.store
.pipe(select(getLoadingState([userAddSetKey])))
.pipe(
takeWhile(
(state) =>
![ActionState.SUCCEEDED, ActionState.FAILED].includes(state),
true
),
filter((state) => state === ActionState.SUCCEEDED),
tap(() => this.form.controls.username.enable())
)
.subscribe();
}