bigbuffet-rw/app/soapbox/entity-store/hooks/useEntities.ts

132 lines
3.9 KiB
TypeScript
Raw Normal View History

import { useEffect } from 'react';
2022-12-04 15:53:56 -08:00
import { getNextLink, getPrevLink } from 'soapbox/api';
2022-12-04 14:58:13 -08:00
import { useApi, useAppDispatch, useAppSelector } from 'soapbox/hooks';
2022-12-04 15:53:56 -08:00
import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess } from '../actions';
2022-12-04 14:58:13 -08:00
import type { Entity } from '../types';
import type { RootState } from 'soapbox/store';
2022-12-04 14:58:13 -08:00
/** Tells us where to find/store the entity in the cache. */
type EntityPath = [
/** Name of the entity type for use in the global cache, eg `'Notification'`. */
entityType: string,
/** Name of a particular index of this entity type. You can use empty-string (`''`) if you don't need separate lists. */
listKey: string,
]
/** Additional options for the hook. */
interface UseEntitiesOpts<TEntity> {
/** A parser function that returns the desired type, or undefined if validation fails. */
parser?: (entity: unknown) => TEntity | undefined
/**
* Time (milliseconds) until this query becomes stale and should be refetched.
* It is 1 minute by default, and can be set to `Infinity` to opt-out of automatic fetching.
*/
staleTime?: number
}
/** A hook for fetching and displaying API entities. */
function useEntities<TEntity extends Entity>(
/** Tells us where to find/store the entity in the cache. */
path: EntityPath,
/** API route to GET, eg `'/api/v1/notifications'`. If undefined, nothing will be fetched. */
endpoint: string | undefined,
/** Additional options for the hook. */
opts: UseEntitiesOpts<TEntity> = {},
) {
2022-12-04 14:58:13 -08:00
const api = useApi();
const dispatch = useAppDispatch();
const [entityType, listKey] = path;
const defaultParser = (entity: unknown) => entity as TEntity;
const parseEntity = opts.parser || defaultParser;
const cache = useAppSelector(state => state.entities[entityType]);
const list = cache?.lists[listKey];
2022-12-04 14:58:13 -08:00
const entityIds = list?.ids;
const entities: readonly TEntity[] = entityIds ? (
Array.from(entityIds).reduce<TEntity[]>((result, id) => {
const entity = parseEntity(cache?.store[id] as unknown);
2022-12-04 14:58:13 -08:00
if (entity) {
result.push(entity);
}
return result;
}, [])
) : [];
const isFetching = Boolean(list?.state.fetching);
const isLoading = isFetching && entities.length === 0;
const hasNextPage = Boolean(list?.state.next);
const hasPreviousPage = Boolean(list?.state.prev);
2022-12-04 15:53:56 -08:00
const fetchPage = async(url: string): Promise<void> => {
// Get `isFetching` state from the store again to prevent race conditions.
const isFetching = dispatch((_, getState: () => RootState) => Boolean(getState().entities[entityType]?.lists[listKey]?.state.fetching));
if (isFetching) return;
2022-12-04 15:53:56 -08:00
dispatch(entitiesFetchRequest(entityType, listKey));
try {
const response = await api.get(url);
dispatch(entitiesFetchSuccess(response.data, entityType, listKey, {
next: getNextLink(response),
prev: getPrevLink(response),
fetching: false,
error: null,
lastFetchedAt: new Date(),
2022-12-04 15:53:56 -08:00
}));
} catch (error) {
dispatch(entitiesFetchFail(entityType, listKey, error));
}
};
const fetchEntities = async(): Promise<void> => {
if (endpoint) {
await fetchPage(endpoint);
}
2022-12-04 14:58:13 -08:00
};
2022-12-04 15:53:56 -08:00
const fetchNextPage = async(): Promise<void> => {
2022-12-04 14:58:13 -08:00
const next = list?.state.next;
if (next) {
2022-12-04 15:53:56 -08:00
await fetchPage(next);
2022-12-04 14:58:13 -08:00
}
};
2022-12-04 15:53:56 -08:00
const fetchPreviousPage = async(): Promise<void> => {
2022-12-04 14:58:13 -08:00
const prev = list?.state.prev;
if (prev) {
2022-12-04 15:53:56 -08:00
await fetchPage(prev);
2022-12-04 14:58:13 -08:00
}
};
const staleTime = opts.staleTime ?? 60000;
const lastFetchedAt = list?.state.lastFetchedAt;
useEffect(() => {
if (!isFetching && (!lastFetchedAt || lastFetchedAt.getTime() + staleTime <= Date.now())) {
fetchEntities();
}
}, [endpoint]);
2022-12-04 14:58:13 -08:00
return {
entities,
fetchEntities,
isFetching,
isLoading,
hasNextPage,
hasPreviousPage,
fetchNextPage,
fetchPreviousPage,
};
}
export {
useEntities,
};