Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added fetching and pagination services #6

Merged
merged 1 commit into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
},
"peerDependencies": {
"lodash": "^4.17.21",
"mobx": "^6.12.0",
"react": ">=16.8.0",
"tslib": "^2.5.0",
"yup": ">=0.32.11"
Expand Down
27 changes: 27 additions & 0 deletions src/services/fetching.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { action, makeObservable, observable } from 'mobx';

class Fetching {
/**
* isFetching on submit
*/
public isFetching = false;

/**
* @constructor
*/
constructor() {
makeObservable(this, {
isFetching: observable,
setFetching: action.bound,
});
}

/**
* Set is loading
*/
public setFetching(isLoading: boolean): void {
this.isFetching = isLoading;
}
}

export default Fetching;
338 changes: 338 additions & 0 deletions src/services/pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
import _ from 'lodash';
import type { IReactionDisposer } from 'mobx';
import { action, makeObservable, observable, reaction } from 'mobx';

export type IRequestReturn<TEntity> =
| { count?: number; data: TEntity[]; page: number }
| string
| undefined;

type TEntitiesFunc<TEntity> = (page?: number) => Promise<IRequestReturn<TEntity>>;

interface IPaginationParams<TEntity> {
getEntities: TEntitiesFunc<TEntity>;
entities?: TEntity[];
initState?: Partial<typeof getInitialState>;
shouldAdd?: boolean;
isLocal?: boolean;
}

export interface IState {
error: string | null;
isFetching: boolean;
isFirstRender: boolean;
isFirstPage: boolean;
isLastPage: boolean;
hasPreviousPage: boolean;
hasNextPage: boolean;
pageSize: number;
page: number;
totalPages: number;
count: number;
firstPageNumber: number;
lastPageNumber: number;
pages: number[];
filter?: string;
orderBy?: string;
}

export const getInitialState = (): IState => ({
error: null,
isFetching: false,
isFirstRender: true,
isFirstPage: true,
isLastPage: false,
hasPreviousPage: false,
hasNextPage: true,
pageSize: 20,
page: 1,
totalPages: 0,
count: 0,
firstPageNumber: 1,
lastPageNumber: 0,
pages: [1],
});

/**
* Pagination
*/
class Pagination<TEntity> {
/**
* List of entities
*/
public entities: TEntity[] = [];

/**
* Pagination state
*/
public state: IState;

/**
* Default error message
*/
public defaultErrorMsg = 'Unknown error';

/**
* Get table entities
*/
public getEntities: IPaginationParams<TEntity>['getEntities'];

/**
* List will be paginated via infinite scroll or with pages buttons
* @private
*/
private shouldAdd: boolean;

/**
* Add protection on push duplicates for strict mode if true
* @private
*/
private isLocal: boolean;

/**
* @constructor
*/
constructor({
getEntities,
initState = {},
shouldAdd = false,
isLocal = false,
}: IPaginationParams<TEntity>) {
this.shouldAdd = shouldAdd;
this.isLocal = isLocal;
this.state = { ...getInitialState(), ...initState };
this.getEntities = this.wrapRequest(getEntities);

makeObservable(this, {
entities: observable,
state: observable,
setEntities: action.bound,
setFetching: action.bound,
setPageSize: action.bound,
setPage: action.bound,
setError: action.bound,
setTotalCount: action.bound,
resetState: action.bound,
resetIsFirstRender: action.bound,
setHasNextPage: action.bound,
setHasPreviousPage: action.bound,
});
}
/**
* Serialize observable data for SSR
*/
public toJSON = (): { entities: TEntity[]; state: IState } => ({
entities: this.entities,
state: this.state,
});

/**
* Serialize pagination service for SSR
*/
public static create = <
TContext extends { pagination: Pagination<unknown>; getEntities: TEntitiesFunc<unknown> },
>(
context: TContext,
) => {
const paginationState = context.pagination;

context.pagination = new Pagination({
getEntities: context.getEntities,
shouldAdd: true,
});

context.pagination.entities = paginationState?.entities;
context.pagination.state = paginationState?.state;

context.getEntities = context.pagination.getEntities;
};

/**
* Add state subscribers
* Get entities when state changed
*/
public addSubscribe = (): IReactionDisposer =>
reaction(
() => ({
page: this.state.page,
pageSize: this.state.pageSize,
}),
() => void this.getEntities(),
);

/**
* Reset state before retry requests
*/
public resetIsFirstRender(): void {
this.state.isFirstRender = true;
}

/**
* Reset table store
*/
public resetState(): void {
this.state = getInitialState();
this.setEntities([]);
}

/**
* Set error message
*/
public setError(message: string | null): void {
this.state.error = message;
}

/**
* Set page size
*/
public setPageSize(count: number): void {
this.state.pageSize = count;
this.setPage(1);
}

/**
* Set current page
*/
public setPage(page: number): void {
this.state.page = page;
}

/**
* Set shouldAdd only on init
*/
public setShouldAdd(shouldAdd: boolean): void {
this.shouldAdd = shouldAdd;
}

/**
* Set list entities
*/
public setEntities(entities: TEntity[], shouldAdd = false): void {
if (shouldAdd) {
// Add protection on push duplicates for strict mode
if (this.isLocal) {
const isDuplicatedResult = this.entities.some((el) => _.isEqual(el, entities?.[0]));

if (isDuplicatedResult) {
return;
}
}

this.entities.push(...entities);
} else {
this.entities = entities;
}
}

/**
* Wrapper for get entities
*/
public wrapRequest(callback: TEntitiesFunc<TEntity>): TEntitiesFunc<TEntity> {
this.getEntities = async (pageVal) => {
this.setError(null);
this.setFetching(true);

const result = await callback(pageVal ?? this.state.page);

this.setFetching(false);

if (result === undefined) {
return;
}

if (typeof result === 'string') {
this.setError(result ?? this.defaultErrorMsg);

return;
}

if (!result.page) {
result.page = 1;
}

const { data, count, page } = result;

this.setPage(page);
this.setTotalCount(count ?? data.length);
this.setEntities(data, page > 1 && this.shouldAdd);
this.setHasNextPage();
this.setHasPreviousPage();

return result;
};

return this.getEntities;
}

/**
* Toggle fetching
*/
public setFetching(isFetching: boolean): void {
this.state.isFetching = isFetching;

if (!this.state.isFetching) {
this.state.isFirstRender = false;
}
}

/**
* Lazy load pagination
*/
public getNextPage = (): Promise<IRequestReturn<TEntity>> | undefined => {
const { page, pageSize, count } = this.state;

if (page * pageSize >= count) {
return;
}

if (this.entities.length >= count) {
return;
}

if (page === 1 && count < pageSize) {
return;
}

return this.getEntities(page + 1);
};

/**
* Get prev page
*/
public getPrevPage = (): Promise<IRequestReturn<TEntity>> | undefined => {
const { page } = this.state;

if (page <= 1) {
return;
}

return this.getEntities(page - 1);
};

/**
* Set count entities
*/
public setTotalCount(count: number): void {
this.state.count = count;
}

/**
* Set has next page state
*/
public setHasNextPage(): void {
const { count, page, pageSize } = this.state;

this.state.hasNextPage = count > page * pageSize;
}

/**
* Set has previous page state
*/
public setHasPreviousPage(): void {
const { page } = this.state;

this.state.hasPreviousPage = page > 1;
}
}

export default Pagination;