ngrx-loading-state
Version:
NgRx Loading State consistently manages loading actions such as API fetches.
861 lines (846 loc) • 36.4 kB
JavaScript
import { combineLatest, map, catchError, of, pipe, groupBy, mergeMap, withLatestFrom, filter } from 'rxjs';
import { createAction, props, on, createSelector } from '@ngrx/store';
import { isArray, isEqual, isPlainObject, pick } from 'lodash';
import { ofType, createEffect } from '@ngrx/effects';
// This file is in the "lr-lodash" directory so that we can use the barrel file
var lodash_functions = /*#__PURE__*/Object.freeze({
__proto__: null,
isArray: isArray,
isEqual: isEqual,
isPlainObject: isPlainObject,
pick: pick
});
/**
* Using this constant for the maxAge parameter will issue an API load if there is no
* on going API call, irrespective of the current age of the previous load action.
*/
const MAX_AGE_LATEST = 0;
/**
* These are used by the global error reducer to keep track of whether error events have
* already been handled
*/
var FailureHandlerState;
(function (FailureHandlerState) {
FailureHandlerState["INIT"] = "INIT";
FailureHandlerState["GLOBAL"] = "GLOBAL";
FailureHandlerState["LOCAL"] = "LOCAL";
})(FailureHandlerState || (FailureHandlerState = {}));
/**
* See if a new API fetch should be issued.
*
* @param currentState The current loading state.
* @param action The the LoadAction dispatched by the user.
* @returns True if a new API fetch should be issued.
*/
function shouldIssueFetch(currentState, action) {
if (currentState == null) {
return true;
}
const maxAge = action === null || action === void 0 ? void 0 : action.maxAge;
// If action not given, then we err on the side of caution and always do a reload, even if
// an API fetch is already happening.
if (maxAge == null) {
return true;
}
else {
// Check if data not loaded or if too old.
const reload = !currentState.successTimestamp || Date.now() - currentState.successTimestamp >= maxAge;
// Do not issue duplicate loads if a fetch is already in progress
return reload && !currentState.loading;
}
}
/**
* Return the type of error handler that should be used.
*
* @param currentState the current state
* @param action the load action dispatched by the user
* @param issueFetch whether the load action should issue a new API call.
* @returns
*/
function getFailureHandlerState(currentState, action, issueFetch) {
if (currentState == null) {
// If currentState does not exist, then errorHandlerState can be assumed to be initialised to ErrorHandlerState.INIT
if (action.localError) {
return FailureHandlerState.LOCAL;
}
else {
return FailureHandlerState.GLOBAL;
}
}
// If loading or issuing API fetch, then there is guaranteed to be a success/failure
// action that the global error handler might need to handle.
if (currentState.loading || issueFetch) {
// If any load action sets the localError to true, then it disables the global error hander
// until the success/failure action is handled.
if (action.localError) {
return FailureHandlerState.LOCAL;
}
else if (currentState.failureHandlerState == FailureHandlerState.INIT) {
// If it's in the INIT state, then we use the default global handler because the
// loading action has not requests for localError handler.
return FailureHandlerState.GLOBAL;
}
// else just fall through and return the existing errorHandler unchanged.
}
return currentState.failureHandlerState;
}
/**
* This function make it easier to define the type of prop<T>. Internal use only.
*
* T extends object meets the condition of props function
*
* ref: https://stackoverflow.com/questions/65888508/how-to-use-generic-type-in-ngrx-createaction-props
*
* @param type String type of the action
* @returns An action creator function
*/
function actionFactory(type) {
// Restricting config type to match createAction requirements
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return createAction(type, props());
}
/**
* Make a clone of any object that implements the LoadingState interface, but copy over only
* those fields that are in the LoadingState interface.
*
* @param src Any object that implements the LoadingState interface
* @returns A copy of src with only fields from LoadingState
*/
function cloneLoadingState(src) {
// Since the "src" could contain extra fields, we want to make sure we exclude other fields in the
// comparison. Using Required<> to make sure we don't miss any fields.
// Would be better if we can iterate all the fields in the LoadingStateBase interface. But unless we change
// LoadingStateBase to a class, it doesn't seem likely. Below is verbose, but at least it should capture all
// the fields.
if (src == null) {
return src;
}
else {
// Using Required<> to ensure we don't miss any fields from LoadingStateBase.
const ret = pick(src, [
'isLoadingState',
'loading',
'success',
'issueFetch',
'failureHandlerState',
'successTimestamp',
'error'
]);
return ret;
}
}
/**
* Combine the currentState and the user dispatched LoadAction into newState.
*
* @param action User dispatched LoadAction
* @param currentState Current store state
* @returns A new state if the state should change, null otherwise.
*/
function getNewLoadState(action, currentState) {
const issueFetch = shouldIssueFetch(currentState, action);
const failureHandlerState = getFailureHandlerState(currentState, action, issueFetch);
const newState = issueFetch
? {
isLoadingState: true,
loading: true,
success: false,
issueFetch,
failureHandlerState,
successTimestamp: currentState === null || currentState === void 0 ? void 0 : currentState.successTimestamp,
error: undefined
}
: {
isLoadingState: true,
// Deliberately avoiding the use of the spread operator, i.e. no ...currentState
// because we want to be 100% explicit about the states we are setting. Using ...currentState
// makes it difficult to read. Being explicit means we need to specify all fields
// from LoadingState. If we ever add more states to LoadingState the typing will catch any
// missing states. There's also just the loading(), success(), failure() functions
// so not too cumbersome to be explicit.
loading: currentState.loading,
success: currentState.success,
issueFetch,
failureHandlerState,
successTimestamp: currentState.successTimestamp,
error: currentState.error
};
// Note that even if issueFetch is false, the errorHandlerState could still change. So we should
// compare every field from old to new state.
return isEqual(currentState, newState) ? null : newState;
}
/**
* @returns Always returns a new success state.
*/
function getNewSuccessState() {
const ret = {
isLoadingState: true,
loading: false,
success: true,
issueFetch: false,
// Each load action will set this again, so here we just set it back to default.
failureHandlerState: FailureHandlerState.INIT,
// Since this time field changes, all SuccessAction will cause a state update.
successTimestamp: Date.now(),
error: undefined
};
return ret;
}
/**
* Returns a new FailureState.
*
* @param action User dispatched FailureAction
* @param currentState Current loading state
* @returns A new loading state.
*/
function getNewFailureState(action, currentState) {
return {
isLoadingState: true,
loading: false,
success: false,
issueFetch: false,
// Leading this as is for the global failure handler to check.
failureHandlerState: currentState.failureHandlerState,
successTimestamp: currentState.successTimestamp,
error: action.error
};
}
function combineLoadingStates(loadingStates) {
var _a;
return {
loading: loadingStates.some((state) => !!(state === null || state === void 0 ? void 0 : state.loading)),
success: loadingStates.every((state) => !!(state === null || state === void 0 ? void 0 : state.success)),
error: (_a = loadingStates.find((state) => !!(state === null || state === void 0 ? void 0 : state.error))) === null || _a === void 0 ? void 0 : _a.error
};
}
function getInitialState() {
return {
loadingStates: {},
idLoadingStates: {}
};
}
function combineLatestLoadingStates(loadingStates$) {
return combineLatest(loadingStates$).pipe(map((loadingStates) => {
return combineLoadingStates(loadingStates);
}));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function instanceOfLoadingState(state) {
return state === null || state === void 0 ? void 0 : state.isLoadingState;
}
/**
* Recursively process all fields of the states. When it encounters a LoadingStateBase object, it
* will check if there are failures and needs global failure handling. If global handling is needed,
* it shows a snackbar message, and changes the failureHandler to FailureHandlerState.INIT as a way to
* mark the failure as having been processed.
*
*/
function processState(options) {
const { state, failureAction, failureHandler } = options;
if (instanceOfLoadingState(state)) {
if (state.error && state.failureHandlerState == FailureHandlerState.GLOBAL) {
// Passing back new reference from this reducer is not unnecessary since for each reducer pass
// all unhandled global failures are handled and failureHandlerState set to FailureHandlerState.INIT to
// mark it as having been processed.
// No effects can fire in between the change from GLOBAL to INIT. And since
// the loadingState must have changed in response to a failure, it's always a new reference
// compared to the old state.
state.failureHandlerState = FailureHandlerState.INIT;
failureHandler(failureAction, state);
}
}
else {
if (isArray(state)) {
// Recursively handle all sub fields. This includes LoadingStateTypes.ID_LOADING_STATE
// As per previous comment, we are not creating new state references so editing in-place.
state.forEach((field) => processState(Object.assign(Object.assign({}, options), { state: field })));
}
else if (isPlainObject(state)) {
Object.values(state).forEach((field) => processState(Object.assign(Object.assign({}, options), { state: field })));
}
}
}
function globalFailureReducerFactory(failureHandler) {
return (reducer) => {
return (state, action) => {
state = reducer(state, action);
const failureAction = action;
// We must ensure that only FailureAction has this field. But we can
// easily change this field name to something more unique.
if (failureAction.error) {
processState({ state, failureHandler: failureHandler, failureAction });
}
return state;
};
};
}
/**
* IdLoadingAction is similar to LoadingAction with the difference that it's parameterized on a user provided ID.
*
*/
class IdLoadingActions {
constructor(options) {
// Could have used createActionGroup() but the string literal typing of Source is giving me trouble. For now,
// just separate types.
this.idLoad = options.idLoad;
this.idSuccess = options.idSuccess;
this.idFailure = options.idFailure;
}
// ----------------------------------------------------------------------------
// Typing
// ----------------------------------------------------------------------------
instanceOfIdLoad(action) {
return action.type === this.idLoad.type;
}
instanceOfIdSuccess(action) {
return action.type === this.idSuccess.type;
}
instanceOfIdFailure(action) {
return action.type === this.idFailure.type;
}
// ------------------------------------------------------------------------------------------------
// Reducer
// ------------------------------------------------------------------------------------------------
reducer(options) {
const { onLoad, onSuccess, onFailure } = options || {};
return [
on(this.idLoad, (state, action) => {
// Reducer must always create a new copy of the state.
const newState = Object.assign(Object.assign({}, state), { idLoadingStates: this.setState(getNewLoadState, action, state.idLoadingStates) });
// The updated loadingStates is passed to the user code for maximum
// flexibility in case the user wishes to change the loadingStates.
return (onLoad ? onLoad(newState, action) : newState);
}),
on(this.idSuccess, (state, action) => {
const newState = Object.assign(Object.assign({}, state), { idLoadingStates: this.setState(getNewSuccessState, action, state.idLoadingStates) });
return (onSuccess ? onSuccess(newState, action) : newState);
}),
on(this.idFailure, (state, action) => {
const newState = Object.assign(Object.assign({}, state), { idLoadingStates: this.setState(getNewFailureState, action, state.idLoadingStates) });
return (onFailure ? onFailure(newState, action) : newState);
})
];
}
catchError(id) {
return catchError((error) => {
return of(
// AZ: Casting to "any" is less than ideal. But just can't figure out the complex typing here.
this.idFailure({
id,
error
}));
});
}
// ----------------------------------------------------------------------------
// Selectors
// ----------------------------------------------------------------------------
/**
* Returns a map of selectors for loading, success, error, and the entire state.
* The advantage of doing it in a bundle is that we can share the result of createStateSelector(),
* if we separated into individual functions, each function might need to call createStateSelector()
* to create a new instance of the selector. We can't cache any created selectors because will cause
* memory leak since the cached references are always help in this class and hence does not get released.
* @param selectLoadingStates Selector that returns the loadingStats of the feature slice. You can use createLoadingStatesSelector()
* to create it.
* @returns A collection of selectors
* state: the LoadingState
* loading: True if loading
* success: True if last load was successful
* error: LrError2 object if previous loading failed.
*
*/
createIdSelectors(selectIdLoadingStates) {
const selectIdLoadingStateMap = createSelector(selectIdLoadingStates, (idLoadingStates) => idLoadingStates[this.key]);
const state = (id) => {
return createSelector(selectIdLoadingStateMap, (idLoadingStateMap) => this.getIdLoadingState(idLoadingStateMap, id));
};
const loading = (id) => {
return createSelector(state(id), (idLoadingState) => idLoadingState.loading);
};
const success = (id) => {
return createSelector(state(id), (idLoadingState) => idLoadingState.success);
};
const error = (id) => {
return createSelector(state(id), (idLoadingState) => idLoadingState.error);
};
const combinedState = createSelector(selectIdLoadingStateMap, (idLoadingStateMap) => {
return combineLoadingStates(idLoadingStateMap ? Object.values(idLoadingStateMap) : []);
});
return {
state,
loading,
success,
error,
combinedState
};
}
// ----------------------------------------------------------------------------
// Effects
// ----------------------------------------------------------------------------
idLoadHandler(fetch) {
return pipe(ofType(this.idLoad), (source) => {
// Below is inspired by: https://github.com/nrwl/nx/blob/master/packages/angular/src/runtime/nx/data-persistence.ts#L75
const groupedFetches = source.pipe(groupBy((action) => {
return action.id; // This will be used as the "group.key"
}));
return groupedFetches.pipe(mergeMap((group) => {
return fetch(group, group.key);
}));
});
}
createEffect(actions$, fetch) {
return createEffect(() => {
return actions$.pipe(this.idLoadHandler(fetch));
});
}
// ----------------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------------
get key() {
return this.idLoad.type;
}
getIdLoadingState(idLoadingStateMap, id) {
if (id == null || id === '') {
throw new Error('id parameter is null or empty string, this is almost always a logic bug.');
}
// We should not be modifying the state without going via the reducer, hence
// returning the immutable "init" object.
return idLoadingStateMap === null || idLoadingStateMap === void 0 ? void 0 : idLoadingStateMap[id];
}
setState(getNewState, action, idLoadingStates) {
var _a;
const currentState = cloneLoadingState((_a = idLoadingStates[this.key]) === null || _a === void 0 ? void 0 : _a[action.id]);
const newState = getNewState(action, currentState);
if (newState) {
// Return new reference only when the state has changed.
return Object.assign(Object.assign({}, idLoadingStates), { [this.key]: Object.assign(Object.assign({}, idLoadingStates[this.key]), { [action.id]: Object.assign(Object.assign({}, newState), { isIdLoadingState: true, id: action.id }) }) });
}
else {
// No change in state, so no change in parent state.
return idLoadingStates;
}
}
}
// These classes are basically serving the same purpose as props<T> in createAction() where it
function idLoad() {
return new IdLoad();
}
function idSuccess() {
return new IdSuccess();
}
function idFailure() {
return new IdFailure();
}
/**
* Creates a set of IdLoadAction, IdSuccessAction, and IdFailureAction. Selectors and reducers are always bundled into
* the same structure.
*
* The difference between IdLoadAction and LoadAction is that IdLoadAction is parameterized by an id field. So you can
* display loading states for multiple items that are all loading in parallel. Typical use case is you have a list of items,
* and you can issue load actions for each one, parameterized by an id of your own choosing, and observe the loading state
* of each item, again parameterized by the id.
*
* @param type The "type" of the action.
* @param _idLoad See usage example
* @param _idSuccess See usage example
* @param _idFailure See usage example
* @returns An instance of LoadingActions class that bundles together actions, selectors and reducers.
* @example
* export const fetchItem = createIdLoadingActions(
* 'Fetch Item',
* // An id field is already included in each of LoadAction, SuccessAction, FailureAction
* load<{}>(),
* success<{ item: object }>(),
* failure<{}>()
* );
*
* // Dispatch load action
* const id = "123";
* this.store.dispatch(fetchItem.idLoad({ id }));
*
* // Using ngrx's createFeatureSelector to select the feature slice from global store.
* const selectState = createFeatureSelector<SimpleState>(SIMPLE_FEATURE_KEY);
* const selectLoadingStates = createLoadingStatesSelector(selectState);
*
* const fetchItemSelectors = fetchItem.createIdSelectors(selectLoadingStates);
*
* // Observe the loading state.
* this.store.select(fetchItemSelectors.state(id));
*
*/
function createIdLoadingActions(actionTypePrefix, _idLoad, _idSuccess, _idFailure) {
return new IdLoadingActions({
idLoad: actionFactory(`${actionTypePrefix}`),
idSuccess: actionFactory(`${actionTypePrefix} Success`),
idFailure: actionFactory(`${actionTypePrefix} Failure`)
});
}
/**
*
* @param featureSelector Selector that selects the current feature slice of the store.
* @returns Selector that selects the loadingStates field from the store
* @example
* // Using ngrx's createFeatureSelector to select the feature slice from global store.
* const selectState = createFeatureSelector<SimpleState>(SIMPLE_FEATURE_KEY);
* const selectLoadingStates = createLoadingStatesSelector(selectState);
*
* You can then use selectLoadingStates to compose other selectors. eg.
*
* export const fetchItem = createIdLoadingActions(
* 'Fetch Item',
* load<{}>(),
* success<{ item: object }>(),
* failure<{}>()
* );
*
* export const fetchItemSelectors = fetchItem.createSelectors(selectLoadingStates);
*
*/
function createIdLoadingStatesSelector(featureSelector) {
return createSelector(featureSelector, (state) => {
return state.idLoadingStates;
});
}
// ------------------------------------------------------------------------
// Internal
// ------------------------------------------------------------------------
// types are positional only, so not easy to read.
class IdLoad {
constructor() {
// These variables with constant string typing prevents Load and Success instances from being
// assignable to each other.
this.type = 'ID_LOAD';
}
}
class IdSuccess {
constructor() {
this.type = 'ID_SUCCESS';
}
}
class IdFailure {
constructor() {
this.type = 'ID_FAILURE';
}
}
/**
* This class bundles up a set of load, success, and failure actions. It contains helpers to create
* reducers for these actions and helpers for creating selectors.
*
* Do not use this class directly, use the createLoadingActions() helper function.
*
*/
class LoadingActions {
constructor(options) {
// Could have used createActionGroup() but the string literal typing of Source is giving me trouble. For now,
// just separate types.
this.load = options.load;
this.success = options.success;
this.failure = options.failure;
}
// ----------------------------------------------------------------------------
// Typing
// ----------------------------------------------------------------------------
/**
* Type guard to test if action is of type LoadingActions.load.
*
* @param action Any ngrx action
* @returns True if action if of type LoadingActions.load
*/
instanceOfLoad(action) {
return action.type === this.load.type;
}
/**
* Type guard to test if action is of type LoadingActions.success.
*
* @param action Any ngrx action
* @returns True if action if of type LoadingActions.success
*/
instanceOfSuccess(action) {
return action.type === this.success.type;
}
/**
* Type guard to test if action is of type LoadingActions.failure.
*
* @param action Any ngrx action
* @returns True if action if of type LoadingActions.failure
*/
instanceOfFailure(action) {
return action.type === this.failure.type;
}
// ------------------------------------------------------------------------------------------------
// Reducer
// ------------------------------------------------------------------------------------------------
/**
* Creates reducers for the load, success, failure actions.
*
* @param options.onLoad Call back when action is LoadAction. Return new copy of state if state needs to change.
* @param options.onSuccess Call back when action is SuccessAction. Return new copy of state if state needs to change.
* @param options.onFailure Call back when action is FailureAction. Return new copy of state if state needs to change.
* @returns A tuple of "on()" instances that handles load, success, failure actions in this order.
* @example
* export const reducer = createReducer(
* initialState,
* // Note: (1)
* ...fetchItem.reducer<ItemState>({
* onSuccess: (state, { item }): ItemState => {
* return { ...state, item };
* },
* // You can also customise what happens for LoadAction and FailureAction through onLoad and onFailure.
* // But most of the time, there's nothing to do for those. They are automatically handled by the fetchItem.reducer
* }),
* );
*
* (1) We need this explicit return type because "return type widening" means that the
* type constraints on the onSuccess function is only narrowing, so if you provide more
* fields in the return object that is not in VaultState, there is no error. Because
* the return value can be narrowed to the onSuccess function's constraints. So we have
* to explicitly specify the return type here. This is a well known issue that has been
* raise since at least 2016. So does not look like it will be fixed. Also, it's a recommended
* pattern in ngrx reducers to have this explicit typing.
*
* Ref https://github.com/microsoft/TypeScript/issues/241#issuecomment-327269994
*
* (2): Given that there are explicity return types on the onLoad, onSuccess, onFailure functions
* it should be possible to infer the type of the state here. But can't figure out how.
*
*/
reducer(options) {
const { onLoad, onSuccess, onFailure } = options || {};
return [
on(this.load, (state, action) => {
// Reducer must always create a new copy of the state.
const newState = Object.assign(Object.assign({}, state), { loadingStates: this.setState(getNewLoadState, action, state.loadingStates) });
// The updated loadingStates is passed to the user code for maximum
// flexibility in case the user wishes to change the loadingStates.
return (onLoad ? onLoad(newState, action) : newState);
}),
on(this.success, (state, action) => {
const newState = Object.assign(Object.assign({}, state), { loadingStates: this.setState(getNewSuccessState, action, state.loadingStates) });
return (onSuccess ? onSuccess(newState, action) : newState);
}),
on(this.failure, (state, action) => {
const newState = Object.assign(Object.assign({}, state), { loadingStates: this.setState(getNewFailureState, action, state.loadingStates) });
return (onFailure ? onFailure(newState, action) : newState);
})
];
}
/**
* Catches errors in effect.
*
* @returns rxjs operator that emits a FailureAction.
* @example
*
* fetchCount$ = createEffect(() => {
* return this.actions$.pipe(
* ofType(fetchCount.load),
* filterLoading(this.store.select(fetchCountSelectors.state)),
* switchMap((action) => {
* return apiCAll().pipe(
* map(item => fetchCount.success(item)),
* fetchCount.catchError()
* );
* })
* );
* });
*
*/
catchError() {
return catchError((error) => {
return of(
// AZ: Casting to "any" is less than ideal. But just can't figure out the complex typing here.
this.failure({
error
}));
});
}
// ----------------------------------------------------------------------------
// Selectors
// ----------------------------------------------------------------------------
/**
* Returns a map of selectors for loading, success, error, and the entire loading state. Similar to the ngrx entities adaptor.
*
* Design: The advantage of doing it in a bundle is that we can share the result of createStateSelector().
* If we had separate individual functions, each function might need to call createStateSelector()
* to create a new instance of the selector. We can't cache any created selectors because that will cause
* a memory leak since the cached references are always held in this class and hence does not get released.
*
* @param selectLoadingStates Selector that returns the loadingStats of the feature slice. You can use createLoadingStatesSelector()
* to create it.
* @returns A collection of selectors
* state: the LoadingState
* loading: True if loading
* success: True if last load was successful
* error: any errors from the last API call.
* @example
* // The feature slice selector. Standard ngrx stuff.
* const selectState = createFeatureSelector<SimpleState>(SIMPLE_FEATURE_KEY);
*
* // Selector that selects the loadingStates field from the global store. The createLoadingStatesSelector()
* // is provided as a part of this lib as well.
* const selectLoadingStates = createLoadingStatesSelector(selectState);
*
* // Create the selectors related to the fetchItem loading state.
* export const fetchItemSelectors = fetchItem.createSelectors(selectLoadingStates);
*
* // You can then observe the loading states:
* this.store.select(fetchItemSelectors.state); // The entire LoadingState
* this.store.select(fetchItemSelectors.success); // The boolean success flag
* this.store.select(fetchItemSelectors.loading); // The boolean loading flag
*
*/
createSelectors(selectLoadingStates) {
const state = createSelector(selectLoadingStates, (loadingStates) => {
return this.getLoadingState(loadingStates);
});
const loading = createSelector(state, (loadingState) => loadingState.loading);
const success = createSelector(state, (loadingState) => loadingState.success);
const error = createSelector(state, (loadingState) => loadingState.error);
return {
state,
loading,
success,
error
};
}
// ----------------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------------
get key() {
return this.load.type;
}
getLoadingState(loadingStates) {
// We should not be modifying the state without going via the reducer, hence
// returning the immutable "init" object.
return loadingStates[this.key];
}
setState(getNewState, action, loadingStates) {
// We work with LoadingStateBase here to be generic. The idLoadingActions also
// use these functions.
const currentState = cloneLoadingState(this.getLoadingState(loadingStates));
const newState = getNewState(action, currentState);
if (newState) {
// Return new reference only when the state has changed.
return Object.assign(Object.assign({}, loadingStates), { [this.key]: Object.assign({}, newState) });
}
else {
// No change in state, so no change in parent state.
return loadingStates;
}
}
}
function load() {
return new Load();
}
function success() {
return new Success();
}
function failure() {
return new Failure();
}
/**
* Creates a set of load, success, failure actions. Selectors and reducers are always bundled into
* the same structure.
*
* @param type The "type" of the action.
* @param _load See usage example
* @param _success See usage example
* @param _failure See usage example
* @returns An instance of LoadingActions class that bundles together actions, selectors and reducers.
* @example
* export const fetchItem = createLoadingActions(
* 'Fetch Item',
* load<{ itemId: number }>(), // Action type is: 'Fetch Item'
* success<{ item: object }>(), // Action type is: 'Fetch Item Success'
* failure<{}>() // Action type is: 'Fetch Item Failure'
* );
*/
function createLoadingActions(type, _load, _success, _failure) {
return new LoadingActions({
load: actionFactory(`${type}`),
success: actionFactory(`${type} Success`),
failure: actionFactory(`${type} Failure`)
});
}
/**
*
* @param featureSelector Selector that selects the current feature slice of the store.
* @returns Selector that selects the loadingStates field from the store
* @example
* // Using ngrx's createFeatureSelector to select the feature slice from global store.
* const selectState = createFeatureSelector<SimpleState>(SIMPLE_FEATURE_KEY);
* const selectLoadingStates = createLoadingStatesSelector(selectState);
*
* You can then use selectLoadingStates to compose other selectors. eg.
*
* export const fetchItem = createLoadingActions(
* 'Fetch Item',
* load<{ itemId: number }>(),
* success<{ item: object }>(),
* failure<{}>()
* );
*
* export const fetchItemSelectors = fetchItem.createSelectors(selectLoadingStates);
*
*/
function createLoadingStatesSelector(featureSelector) {
return createSelector(featureSelector, (state) => {
return state.loadingStates;
});
}
// ------------------------------------------------------------------------
// Internal
// ------------------------------------------------------------------------
// These classes are basically serving the same purpose as props<T> in createAction() where it
// just holds the type and allows you to name the class to make it easier to read. The alternative
// is to explicitly specify the type when calling createLoadingActions<...>(). But the template
// types are positional only, so not easy to read.
class Load {
constructor() {
// These variables with constant string typing prevents Load and Success instances from being
// assignable to each other.
this.type = 'LOAD';
}
}
class Success {
constructor() {
this.type = 'SUCCESS';
}
}
class Failure {
constructor() {
this.type = 'FAILURE';
}
}
/**
* A ngrx pipeline operator that filters out any actions that does not require the
* issuing of API calls.
*
* Design: Note that whether the action should issue a fetch is done in the reducer, where the
* loadingState.issueFetch parameter is updated. We can NOT combine the current state and the
* action in the effect to decide if we need to issue an API call, because that could lead to race
* conditions. The only guaranteed point of synchronous execution is the reducer.
*
* @param loadingState$ Observable that emit the current loading state from the store.
* @returns Stream that only emits a LoadAction if that that load action should result in an API call.
* @example
* fetchCount$ = createEffect(() => {
* return this.actions$.pipe(
* ofType(fetchCount.load),
* filterLoading(this.store.select(fetchCountSelectors.state)),
* switchMap((action) => {
* ...
* })
* );
* });
*
*/
function filterLoading(loadingState$) {
return pipe(withLatestFrom(loadingState$), filter(([_, loadingState]) => loadingState == null || loadingState.issueFetch), map(([action, _]) => action));
}
/*
* Public API Surface of ngrx-loading-state
*/
/**
* Generated bundle index. Do not edit.
*/
export { MAX_AGE_LATEST, createIdLoadingActions, createIdLoadingStatesSelector, createLoadingActions, createLoadingStatesSelector, failure, filterLoading, getInitialState, globalFailureReducerFactory, idFailure, idLoad, idSuccess, load, success };
//# sourceMappingURL=ngrx-loading-state.mjs.map