UNPKG

@jameshclrk/rxdb-hooks

Version:

React hooks for integrating with RxDB

395 lines (347 loc) 8.83 kB
import { useEffect, useCallback, useReducer, Reducer } from 'react'; import { RxQuery, RxDocument } from 'rxdb'; import { DeepReadonly } from 'rxdb/dist/types/types'; import { Override } from './type.helpers'; import { isRxQuery } from './helpers'; export type ResultMap<T> = Map<string, RxDocument<T, any>>; export type AnyQueryResult<T> = DeepReadonly<T>[] | RxDocument<T>[]; export type RxQueryResult<T> = { /** * Resulting documents. */ result: AnyQueryResult<T>; /** * Indicates that fetching is in progress. */ isFetching: boolean; /** * Indicates that all available results have been already fetched. * Relevant in "infinite scroll" pagination mode */ isExhausted: boolean; /** * Total number of pages, based on total number of results and page size. * Relevant in "traditional" pagination mode */ pageCount: number; /** * The number of the current page. * Relevant in "infinite scroll" pagination mode */ currentPage: number; /** * Allows consumer to request a specific page of results. * Relevant in "traditional" pagination mode */ fetchPage: (page: number) => void; /** * Allows consumer to incrementally request more results. * Relevant in "infinite scroll" pagination mode */ fetchMore: () => void; /** * Allows consumer to reset results. * Relevant in "infinite scroll" pagination mode */ resetList: () => void; }; export type RxQueryResultJSON<T> = Override< RxQueryResult<T>, { result: DeepReadonly<T>[]; } >; export type RxQueryResultDoc<T> = Override< RxQueryResult<T>, { result: RxDocument<T>[]; } >; /** * Traditional: * Results are split into pages, starts by rendering the first page and total * pageCount is returned, allowing for requesting results of any specific page. * Requires `pageSize` to be defined * * Infinite: * First page of results is rendered, allowing for gradually requesting more. * Requires `pageSize` to be defined */ export type PaginationMode = 'Traditional' | 'Infinite'; export interface UseRxQueryOptions { /** * Controls page size, in both "infinite scroll" & "traditional" pagination */ pageSize?: number; /** * Determines pagination mode */ pagination?: PaginationMode; /** * Converts resulting RxDocuments to plain objects */ json?: boolean; } export type ObservableReturningQuery<T> = | RxQuery<T, Map<string, RxDocument<T, any>>> | RxQuery<T, RxDocument<T> | null> | RxQuery<T, RxDocument<T>[]>; export type AnyRxQuery<T> = ObservableReturningQuery<T>; interface RxState<T> { result: AnyQueryResult<T>; isFetching: boolean; isExhausted: boolean; page: number | undefined; pageCount: number; } enum ActionType { Reset, FetchMore, FetchPage, FetchSuccess, CountPages, QueryChanged, } interface ResetAction { type: ActionType.Reset; } interface FetchMoreAction { type: ActionType.FetchMore; } interface FetchPageAction { type: ActionType.FetchPage; page: number; } interface CountPagesAction { type: ActionType.CountPages; pageCount: number; } interface FetchSuccessAction<T> { type: ActionType.FetchSuccess; docs: AnyQueryResult<T>; pagination: PaginationMode; pageSize: number; } interface QueryChangedAction { type: ActionType.QueryChanged; } type AnyAction<T> = | ResetAction | FetchMoreAction | FetchPageAction | CountPagesAction | FetchSuccessAction<T> | QueryChangedAction; const reducer = <T>(state: RxState<T>, action: AnyAction<T>): RxState<T> => { switch (action.type) { case ActionType.Reset: return { ...state, result: [], isFetching: true, page: 1, }; case ActionType.FetchMore: return { ...state, isFetching: true, page: state.page + 1, }; case ActionType.FetchPage: return { ...state, isFetching: true, page: action.page, }; case ActionType.CountPages: return { ...state, pageCount: action.pageCount, }; case ActionType.FetchSuccess: return { ...state, result: action.docs, isFetching: false, isExhausted: !action.pageSize ? true : action.pagination === 'Infinite' ? action.docs.length < state.page * action.pageSize : false, }; case ActionType.QueryChanged: return { ...state, isFetching: true, }; /* istanbul ignore next */ default: return state; } }; const getResultArray = <T>( result: | RxDocument<T>[] | RxDocument<T> | Map<string, RxDocument<T, any>> | null, json?: boolean ): AnyQueryResult<T> => { if (!result) { return []; } if (result instanceof Map) { return Array.from(result, ([, doc]) => (json ? doc.toJSON() : doc)); } const resultArray = Array.isArray(result) ? result : [result]; return json ? resultArray.map(doc => doc.toJSON()) : resultArray; }; const getResultLength = <T>( result: RxDocument<T>[] | RxDocument<T> ): number => { const resultArray = Array.isArray(result) ? result : [result]; return resultArray.length; }; function useRxQuery<T>(query?: AnyRxQuery<T>): RxQueryResultDoc<T>; function useRxQuery<T>( query: AnyRxQuery<T>, options?: Override<UseRxQueryOptions, { json?: false }> ): RxQueryResultDoc<T>; function useRxQuery<T>( query: AnyRxQuery<T>, options?: Override<UseRxQueryOptions, { json: true }> ): RxQueryResultJSON<T>; /** * Subscribes to specified query and provides results, also providing: * - state indicators for fetching and list depletion * - a fetchMore callback function for pagination support * - a resetList callback function for conveniently reseting list data */ function useRxQuery<T>( query: AnyRxQuery<T>, options: UseRxQueryOptions = {} ): RxQueryResult<T> { const { pageSize, pagination = 'Infinite', json } = options; const initialState: RxState<T> = { result: [], page: pageSize ? 1 : undefined, isFetching: true, isExhausted: false, pageCount: 0, }; const [state, dispatch] = useReducer<Reducer<RxState<T>, AnyAction<T>>>( reducer, initialState ); const fetchPage = useCallback( (page: number) => { if (!pageSize || pagination !== 'Traditional') { return; } if (page < 1 || page > state.pageCount) { return; } dispatch({ type: ActionType.FetchPage, page }); }, [pageSize, pagination, state.pageCount] ); const fetchMore = useCallback(() => { if (!pageSize || pagination !== 'Infinite') { return; } if (state.isFetching || state.isExhausted) { return; } dispatch({ type: ActionType.FetchMore }); }, [pageSize, pagination, state.isFetching, state.isExhausted]); const resetList = useCallback(() => { if (!pageSize) { return; } if (state.page === 1) { return; } dispatch({ type: ActionType.Reset }); }, [pageSize, state.page]); const performQuery = useCallback( (query: ObservableReturningQuery<T>) => { // avoid re-assigning reference to original query let _query = query; if (pageSize && pagination === 'Traditional') { _query = _query .skip((state.page - 1) * pageSize) .limit(pageSize); } if (pageSize && pagination === 'Infinite') { _query = _query.limit(state.page * pageSize); } dispatch({ type: ActionType.QueryChanged, }); // TODO: find more elegant way to resolve type error // (TS doesn't consider _query.$.subscribe to be callable) const sub = (_query.$.subscribe as any)( (result: RxDocument<T> | RxDocument<T>[]) => { const docs = getResultArray(result, json); dispatch({ type: ActionType.FetchSuccess, docs, pagination, pageSize, }); } ); return () => { sub.unsubscribe(); }; }, [json, pageSize, pagination, state.page] ); useEffect(() => { if (!query) { return; } if (isRxQuery(query)) { return performQuery(query); } }, [query, performQuery]); useEffect(() => { if ( !query || !pageSize || pagination !== 'Traditional' || 'then' in query ) { return; } if (isRxQuery(query)) { // Unconvential counting of documents/pages due to missing RxQuery.count(): // https://github.com/pubkey/rxdb/blob/master/orga/BACKLOG.md#rxquerycount // TODO: find more elegant way to resolve type error // (TS doesn't consider _query.$.subscribe to be callable) const countQuerySub = (query.$.subscribe as any)( (result: RxDocument<T> | RxDocument<T>[]) => { const resultLength = getResultLength(result); dispatch({ type: ActionType.CountPages, pageCount: Math.ceil(resultLength / pageSize), }); } ); return () => { countQuerySub.unsubscribe(); }; } }, [query, pageSize, pagination]); return { result: state.result, isFetching: state.isFetching, isExhausted: state.isExhausted, pageCount: state.pageCount, currentPage: state.page, fetchPage, fetchMore, resetList, }; } export default useRxQuery;