Skip to content

Commit

Permalink
feat(app-general): add centralized loading and error management
Browse files Browse the repository at this point in the history
  • Loading branch information
rams23 committed Dec 28, 2020
1 parent b822cb2 commit 1f53258
Show file tree
Hide file tree
Showing 12 changed files with 208 additions and 4 deletions.
3 changes: 2 additions & 1 deletion packages/game-app/src/_shared/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import I18n from 'i18n-js';
import enTranslations from '@assets/i18n/en';
import { reducer, actions, name } from './slice';
import { useTranslate } from './useTranslate';
import translateError from './translateError';

export function initializeI18n() {
const defaultLocale = 'en-EN';
Expand All @@ -12,4 +13,4 @@ export function initializeI18n() {
I18n.translations[defaultLocale] = enTranslations;
}

export { reducer, actions, name, useTranslate };
export { reducer, actions, name, useTranslate, translateError };
22 changes: 22 additions & 0 deletions packages/game-app/src/_shared/i18n/translateError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useTranslate } from './useTranslate';

const defaultError = 'errors.general';

export default function translateError(
t: ReturnType<typeof useTranslate>,
error: { code?: string; errorCode?: string; message?: string },
scope?: string,
) {
const errorCode = error.code || error.errorCode;
let message = t(`${scope || 'errors.code'}.${errorCode}` as any, {
default: '',
});

if (!message) {
message = t(`errors.code.${errorCode}` as any, {
default: error.message,
});
}

return message || t(defaultError as any);
}
12 changes: 10 additions & 2 deletions packages/game-app/src/_shared/i18n/useTranslate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ export function useTranslate() {
}

export function translateFactory(language: string) {
return function translate(key: Path<typeof enTranslations>) {
return I18n.t(key, { locale: language });
return function translate(
key: Path<typeof enTranslations>,
options?: {
default?: string;
},
) {
return I18n.t(key, {
locale: language,
defaultValue: options?.default,
});
};
}
56 changes: 56 additions & 0 deletions packages/game-app/src/_shared/requests-status/createRequestHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Action } from '@reduxjs/toolkit';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { translateError, useTranslate } from '@pipeline/i18n';
import { selectRequestStatus } from './selectors';
import { RequestsKeys } from './requestsKeys';
import { actions as requestsActions } from './index';
import { GeneralHookResponse } from './types';

type HookOptions = {
errorMessagesScope?: string;
};

export function createRequestHook<T extends Array<any>>(
requestKey: keyof RequestsKeys,
triggerAction: (...args: T) => Action,
options?: HookOptions,
): () => GeneralHookResponse<T> {
return function (): GeneralHookResponse<T> {
const t = useTranslate();
const dispatch = useDispatch();
const keySelector = useMemo(() => selectRequestStatus(requestKey), []);

const { loading, success, error } = useSelector(keySelector);

const getErrorMessage = (error: any) => {
return error ? translateError(t, error, options?.errorMessagesScope) : undefined;
};

const [translatedError, setTranslatedError] = useState(getErrorMessage(error));

useEffect(() => {
const errorText = error ? getErrorMessage(error) : undefined;
setTranslatedError(errorText);
}, [error]);

const call = useCallback(
(...args: T) => {
dispatch(triggerAction(...args));
},
[dispatch],
);

const reset = useCallback(() => {
dispatch(requestsActions.resetStatus(requestKey));
}, [dispatch]);

useEffect(() => {
return () => {
dispatch(requestsActions.resetStatus(requestKey));
};
}, []);

return { loading, success, error, translatedError, call, reset };
};
}
10 changes: 10 additions & 0 deletions packages/game-app/src/_shared/requests-status/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import reducer, { actions, name } from './slice';
import { RequestsKeys } from './requestsKeys';
import { addRequestStatusManagement } from './sagaIntegrations';
import { selectRequestStatus } from './selectors';
import { GeneralHookResponse, RequestStatus } from './types';
import { createRequestHook } from './createRequestHook';

export { actions, addRequestStatusManagement, createRequestHook, name, reducer, selectRequestStatus };

export type { GeneralHookResponse, RequestStatus, RequestsKeys };
7 changes: 7 additions & 0 deletions packages/game-app/src/_shared/requests-status/requestsKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @packageDocumentation
*/

export interface RequestsKeys {
signup: null;
}
19 changes: 19 additions & 0 deletions packages/game-app/src/_shared/requests-status/sagaIntegrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { RequestsKeys } from './requestsKeys';
import { call, put } from 'redux-saga/effects';
import { actions as loadingActions } from './index';
import { SagaIterator } from 'redux-saga';

export function addRequestStatusManagement<T = any, Fn extends (...args: any[]) => SagaIterator<T> = () => any>(
gen: Fn,
requestKey: keyof RequestsKeys,
) {
return function* composed(...args: Parameters<Fn>) {
try {
yield put(loadingActions.startRequest(requestKey));
yield call(gen, ...args);
yield put(loadingActions.requestSuccess(requestKey));
} catch (e) {
yield put(loadingActions.requestError({ key: requestKey, error: e }));
}
};
}
13 changes: 13 additions & 0 deletions packages/game-app/src/_shared/requests-status/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createSelector } from 'reselect';
import { State as RequestsState, name } from './slice';
import { RequestsKeys } from './requestsKeys';

type State = {} & { [name]: RequestsState };

const getRequestsStatusState = (state: State) => state[name];

export const selectRequestStatus = (key: keyof RequestsKeys) =>
createSelector(
getRequestsStatusState,
requests => requests[key] || { loading: false, error: undefined, success: false },
);
48 changes: 48 additions & 0 deletions packages/game-app/src/_shared/requests-status/slice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { RequestsKeys } from './requestsKeys';
import { RequestStatus } from './types';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

export type State = {
[key in keyof RequestsKeys]?: RequestStatus;
};

const requestManagementSlice = createSlice({
name: 'requestsStatus',
initialState: {} as State,
reducers: {
resetStatus(state, action: PayloadAction<keyof RequestsKeys>) {
state[action.payload] = {
loading: false,
success: false,
error: undefined,
};
},
startRequest(state, action: PayloadAction<keyof RequestsKeys>) {
state[action.payload] = {
loading: true,
success: false,
error: undefined,
};
},
requestError(state, action: PayloadAction<{ key: keyof RequestsKeys; error: { message: string; code: string } }>) {
state[action.payload.key] = {
loading: false,
success: false,
error: action.payload.error,
};
},
requestSuccess(state, action: PayloadAction<keyof RequestsKeys>) {
state[action.payload] = {
loading: false,
success: true,
error: undefined,
};
},
},
});

export default requestManagementSlice.reducer;

export const actions = requestManagementSlice.actions;

export const name = requestManagementSlice.name;
17 changes: 17 additions & 0 deletions packages/game-app/src/_shared/requests-status/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface RequestStatus {
loading: boolean;
error?: {
message: string;
code: string;
};
success: boolean;
}

export type GeneralHookResponse<T extends Array<any>> = {
loading: boolean;
success: boolean;
error?: { message: string; code: string };
translatedError?: string;
call: (...args: T) => void;
reset: () => void;
};
2 changes: 2 additions & 0 deletions packages/game-app/src/_shared/routing/routingPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ export enum RoutingPath {
Login = '/login',
Signup = '/signup',
Dashboard = '/dashboard',
EmailVerificationRequired = '/email-verification-required',
VerifyEmail = '/verify-email',
}
3 changes: 2 additions & 1 deletion packages/game-app/tsconfig.paths.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"@pipeline/app-config": ["src/_shared/config"],
"@pipeline/i18n": ["src/_shared/i18n"],
"@pipeline/routing": ["src/_shared/routing"],
"@pipeline/auth": ["src/_shared/auth"]
"@pipeline/auth": ["src/_shared/auth"],
"@pipeline/requests-status": ["src/_shared/requests-status"]
}
}
}

0 comments on commit 1f53258

Please sign in to comment.