Skip to content

Commit

Permalink
Type the callAPI function and actions
Browse files Browse the repository at this point in the history
It now correctly returns a Thunk which can be dispatched to get a
promise with a typed response. No more Thunk<any>/Thunk<Promise<any>>
bs.

The way we handle Action and PromiseAction is awful. The PromiseAction
is handled by the middleware, while the Action is returned by the
callAPI, for some reason.

Seems to work fine, but I'm getting a ts error when dispatching the
action in the call api function. Would appreciate some help here.
  • Loading branch information
ivarnakken committed Apr 27, 2023
1 parent 9d5d7bc commit 645de7d
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 40 deletions.
1 change: 1 addition & 0 deletions app/actions/ActionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const Event = {
FOLLOW: generateStatuses('Event.FOLLOW') as AAT,
UNFOLLOW: generateStatuses('Event.UNFOLLOW') as AAT,
IS_USER_FOLLOWING: generateStatuses('Event.IS_USER_FOLLOWING') as AAT,
FETCH_ANALYTICS: generateStatuses('Event.FETCH_ANALYTICS') as AAT,
};

/**
Expand Down
15 changes: 13 additions & 2 deletions app/actions/EventActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,9 +419,20 @@ export function isUserFollowing(eventId: number): Thunk<any> {
},
});
}
export function fetchAnalytics(eventId: ID): Thunk<Promise<Action>> {

export type AnalyticsResponse = {
date: string;
visitors: number;
pageviews: number;
visitDuration: number | null;
bounceRate: number | null;
};

export function fetchAnalytics(
eventId: ID
): Thunk<Promise<Action<AnalyticsResponse[]>>> {
return callAPI({
types: Event.FETCH,
types: Event.FETCH_ANALYTICS,
endpoint: `/events/${String(eventId)}/statistics/`,
method: 'GET',
meta: {
Expand Down
46 changes: 30 additions & 16 deletions app/actions/callAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { normalize } from 'normalizr';
import { logout } from 'app/actions/UserActions';
import { selectIsLoggedIn } from 'app/reducers/auth';
import { selectPaginationNext } from 'app/reducers/selectors';
import type { AsyncActionType, Thunk } from 'app/types';
import type { Action, AsyncActionType, PromiseAction, Thunk } from 'app/types';
import createQueryString from 'app/utils/createQueryString';
import type {
HttpRequestOptions,
Expand Down Expand Up @@ -69,7 +69,7 @@ type CallAPIOptions = {
};
};

export default function callAPI({
export default function callAPI<T>({
types,
method = 'GET',
headers = {},
Expand All @@ -85,8 +85,8 @@ export default function callAPI({
enableOptimistic = false,
requiresAuthentication = true,
timeout,
}: CallAPIOptions): Thunk<Promise<any>> {
return (dispatch, getState) => {
}: CallAPIOptions): Thunk<Promise<Action<T>>> {
return async (dispatch, getState) => {
const requestOptions: HttpRequestOptions = {
method,
body,
Expand Down Expand Up @@ -160,12 +160,9 @@ export default function callAPI({
query: query || {},
schema,
})(state);

const cursor =
pagination &&
pagination.fetchNext &&
paginationForRequest &&
paginationForRequest.pagination &&
paginationForRequest.pagination.next
pagination?.fetchNext && paginationForRequest?.pagination?.next
? paginationForRequest.pagination.next.cursor
: '';

Expand All @@ -180,8 +177,20 @@ export default function callAPI({
urlFor(`${endpoint}${qs}`),
requestOptions
);
return dispatch({

const action: PromiseAction<T> = {
types,
promise: promise
.then((response) => normalizeJsonResponse(response))
.catch((error) => {
const handleErrorAction = handleError(
error,
propagateError,
endpoint,
loggedIn
);
dispatch(handleErrorAction);
}),
payload: optimisticPayload,
meta: {
queryString: qsWithoutPagination,
Expand All @@ -196,11 +205,16 @@ export default function callAPI({
body,
schemaKey,
},
promise: promise
.then((response) => normalizeJsonResponse(response))
.catch((error) =>
dispatch(handleError(error, propagateError, endpoint, loggedIn))
),
});
};

dispatch(action);

// The promiseMiddleware will handle the PromiseAction and return a Promise<Action<T>>
return action.promise.then((payload) => ({
type: types.SUCCESS,
payload,
meta: action.meta,
success: true,
}));
};
}
24 changes: 16 additions & 8 deletions app/routes/events/components/EventAttendeeStatistics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import {
XAxis,
YAxis,
} from 'recharts';
import { fetchAnalytics } from 'app/actions/EventActions';
import {
type AnalyticsResponse,
fetchAnalytics,
} from 'app/actions/EventActions';
import Card from 'app/components/Card';
import ChartLabel from 'app/components/Chart/ChartLabel';
import DistributionPieChart from 'app/components/Chart/PieChart';
Expand All @@ -22,6 +25,7 @@ import type { Dateish } from 'app/models';
import { useAppDispatch } from 'app/store/hooks';
import type { ID } from 'app/store/models';
import type { DetailedRegistration } from 'app/store/models/Registration';
import type { Action } from 'app/types';
import styles from './EventAttendeeStatistics.css';

interface RegistrationDateDataPoint {
Expand Down Expand Up @@ -230,11 +234,11 @@ const initialMetricValue = {
visitDuration: { title: 'Besøkstid', value: 0 },
};

const calculateMetrics = (data) => {
const calculateMetrics = (data: AnalyticsResponse[]) => {
return data.reduce((acc, item) => {
acc.visitors.value += item.visitors;
acc.pageviews.value += item.pageviews;
acc.visitDuration.value += item.visitDuration;
acc.visitDuration.value += Number(item.visitDuration);

return acc;
}, initialMetricValue);
Expand All @@ -246,16 +250,20 @@ const Analytics = ({ eventId }: { eventId: ID }) => {
pageviews: { title: string; value: number };
visitDuration: { title: string; value: number };
}>(initialMetricValue);
const [data, setData] = useState<{ date: string; visitors: number }[]>([]);
const [data, setData] = useState<AnalyticsResponse[]>([]);

const dispatch = useAppDispatch();

useEffect(() => {
eventId &&
dispatch(fetchAnalytics(eventId)).then((res) => {
setData(res.payload);
setMetrics(calculateMetrics(res.payload));
});
dispatch(fetchAnalytics(eventId)).then(
(res: Action<AnalyticsResponse[]>) => {
if (!res.payload) return;

setData(res.payload);
setMetrics(calculateMetrics(res.payload));
}
);
}, [eventId, dispatch]);

return (
Expand Down
8 changes: 7 additions & 1 deletion app/store/createStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import promiseMiddleware from 'app/store/middleware/promiseMiddleware';
import createSentryMiddleware from 'app/store/middleware/sentryMiddleware';
import type { GetCookie } from 'app/types';
import type { History } from 'history';
import type { AnyAction } from 'redux';
import type { ThunkDispatch } from 'redux-thunk';

export const history: History = __CLIENT__
? createBrowserHistory()
Expand Down Expand Up @@ -72,4 +74,8 @@ const createStore = (
export default createStore;

export type Store = ReturnType<typeof createStore>;
export type AppDispatch = Store['dispatch'];
export type AppDispatch = ThunkDispatch<
RootState,
{ getCookie: GetCookie },
AnyAction
>;
2 changes: 1 addition & 1 deletion app/store/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import type { RootState } from 'app/store/createRootReducer';
import type { AppDispatch } from 'app/store/createStore';
import type { TypedUseSelectorHook } from 'react-redux';

export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
13 changes: 5 additions & 8 deletions app/store/middleware/promiseMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { RootState } from 'app/store/createRootReducer';
import type { AsyncActionType, AsyncActionTypeArray } from 'app/types';
import type {
AsyncActionType,
AsyncActionTypeArray,
PromiseAction,
} from 'app/types';
import type { Middleware } from '@reduxjs/toolkit';

function extractTypes(
Expand All @@ -12,13 +16,6 @@ function extractTypes(
return [types.BEGIN, types.SUCCESS, types.FAILURE];
}

export interface PromiseAction<T> {
types: AsyncActionType;
promise: Promise<T>;
meta?: any;
payload?: any;
}

export default function promiseMiddleware(): Middleware<
<T>(action: PromiseAction<T>) => Promise<T>,
RootState
Expand Down
8 changes: 4 additions & 4 deletions app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,18 @@ export type Token = DecodedToken & {
encodedToken: EncodedToken;
};

export type Action = {
export type Action<T = any> = {
type: string;
payload?: any;
payload?: T;
meta?: any;
error?: boolean;
success?: boolean; // 65 WAT M8 https://github.com/acdlite/flux-standard-action
success?: boolean;
};
export type PromiseAction<T> = {
types: AsyncActionType;
promise: Promise<T>;
meta?: any;
payload?: any;
payload?: T;
};
export type GetState = () => RootState;
export type GetCookie = (arg0: string) => EncodedToken | null | undefined;
Expand Down

0 comments on commit 645de7d

Please sign in to comment.