Skip to content

Instantly share code, notes, and snippets.

@fResult
Last active February 18, 2024 10:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fResult/162a0c72a415654c6dc5542fda3770e4 to your computer and use it in GitHub Desktop.
Save fResult/162a0c72a415654c6dc5542fda3770e4 to your computer and use it in GitHub Desktop.
Higher Order Functions in mapping data fetching Strapi API integrate with React Query
import { compose, isEmpty, map } from 'lodash/fp';
import { useQuery } from '@tanstack/react-query';
// USAGE
// This makes us read "mapStrapiResponse toStaticContent AFTER fetchSomeStaticPageContent"
const fetchAndMapContent: () => Promise<SomeStaticContentAttrs> = compose(
mapStrapiResponse(toStaticContent),
fetchSomeStaticPageContent,
);
const pageContent: SomeStaticContentAttrs = await fetchAndMapContent();
// OR
const { data: pageContent, loading: pageContentLoading } = useQuery({
queryKey: ['some-page-content'],
queryFunction: compose(mapStrapiResponse(toStaticContent), fetchSomeStaticPageContent),
});
// This makes us read "mapStrapiListResponse toAuthor AFTER fetchAuthors"
const fetchAndMapAuthors: () => Promise<Array<AuthorAttrs>> = compose(
mapStrapiListResponse(toAuthor),
fetchAuthors,
);
const authors: Array<AuthorAttrs> = await fetchAndMapAuthors();
// OR
const { data: authors, loading: authorsLoading } = useQuery({
queryKey: ['authors'],
queryFunction: compose(mapStrapiListResponse(toAuthor), fetchAuthors),
});
// This makes us read "mapStrapiResponse toAuthor AFTER fetchAuthorById of authorId"
const fetchAndMapAuthor: () => Promise<AuthorAttrs> = compose(
mapStrapiResponse(toAuthor),
fetchAuthorById(new AutherID(authorId)),
);
const author: AuthorAttrs = await fetchAndMapAuthor();
// OR
const { data: author, loading: authorLoading } = useQuery({
queryKey: ['authors' { id: authorId }],
queryFunction: compose(mapStrapiListResponse(toAuthor), fetchAuthorById(new AutherID(authorId))),
});
class AuthorID {
constructor(private readonly _value: number) {}
get value(): number {
return this._value;
}
};
// IMPLEMENTATION
function toStaticContent(response: StrapiData<SomeStaticContentAttrsResponse>): SomeStaticContentAttrsResponse {
if (isEmpty(response)) return {} as SomeStaticContentAttrsResponse;
const { authors, ...attrs } = response.attributes;
return {
id: response.id,
...attrs,
authors: map(toAuthor)(authors.data),
};
}
function toAuthor(response: StrapiData<AuthorAttrsResponse>): AuthorAttrs {
if (isEmpty(response)) return {} as AuthorAttrs;
return {
id: response.id,
...response.attributes,
};
}
type SomeStaticContentAttrsResponse = BaseStrapiAttrs & {
title: string;
subTitle: string;
shortContent: string;
/** HTML String */
fullContent: string;
authors: StrapiResponse<AuthorAttrsResponse>;
}
enum Position { Developer, Designer, ProductOwner }
type AuthorAttrsResponse = BaseStrapiAttrs & {
name: string;
position: Position;
aboutMe: string;
}
type SomeStaticContentAttrs = BaseStrapiAttrs & {
id: number;
title: string;
subTitle: string;
shortContent: string;
/** HTML String */
fullContent: string;
authors: Array<AuthorAttrs>;
}
type AuthorAttrs = BaseStrapiAttrs & {
id: number;
name: string;
position: Position;
aboutMe: string;
}
// IMPLEMENTATION AS HIGHER ORDER FUNCTIONS
type Mapper<T, R> = (x: T) => R;
function mapStrapiResponse<T extends StrapiResponse<BaseStrapiAttrs>, R extends BaseStrapiAttrs>(
toContent: Mapper<T['data'], R>,
): (fResponse: Promise<T>) => Promise<R> {
return async function forFutureResponse(fResponse) {
const response = await fResponse;
if (isEmpty(response)) return {} as R;
return toContent(response.data);
};
}
function mapStrapiListResponse<T extends StrapiResponse<Array<BaseStrapiAttrs>>, R extends BaseStrapiAttrs>(
toContent: Mapper<T['data'][number], R>,
): (fResponse: Promise<T>) => Promise<Array<R>> {
return async function forFutureResponse(fResponse) {
const response = await fResponse;
if (isEmpty(response)) return {} as Array<R>;
return map(toContent)(response.data);
};
}
function mapPaginateStrapiResponse<T extends StrapiResponse<Array<BaseStrapiAttrs>>, R extends BaseStrapiAttrs>(
toContent: Mapper<T['data'][number], R>,
): (fResponse: Promise<T>) => Promise<{ data: Array<R>; meta: StrapiMeta }> {
return async function forFutureResponse(fResponse) {
const response = await fResponse;
type ListData = StrapiResponse<Array<R>>['data'];
type Meta = StrapiResponse<Array<R>>['meta'];
if (isEmpty(response)) return { data: [] as Array<R>, meta: {} as Meta };
const { data = [] as ListData, meta = {} as Meta } = response;
return {
data: map(toContent)(data),
meta,
};
};
}
type BaseStrapiAttrs = {
locale: StrapiLocale;
localizations: StrapiLocalization;
/** Date String */
createdAt: string;
/** Date String */
updatedAt: string;
/** Date String */
publishedAt: string;
};
type StrapiData<Attrs> = {
id: number;
attributes: Attrs;
};
type StrapiResponse<Attrs> = Attrs extends Array<infer Element>
? {
data: Array<StrapiData<Element>>;
meta: StrapiMeta;
}
: Attrs extends BaseStrapiAttrs
? {
data: StrapiData<Attrs>;
// TODO : for now we don't know the meta content will be sent from strapi, if it's single type.
meta: StrapiMeta;
}
: Attrs;
type StrapiMeta = {
pagination: {
page: number;
pageSize: number;
pageCount: number;
total: number;
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment