103 lines
3 KiB
TypeScript
103 lines
3 KiB
TypeScript
|
import { useEffect } from 'react';
|
||
|
import { z } from 'zod';
|
||
|
|
||
|
import { useAppDispatch, useAppSelector, useGetState } from 'soapbox/hooks';
|
||
|
import { filteredArray } from 'soapbox/schemas/utils';
|
||
|
|
||
|
import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess } from '../actions';
|
||
|
import { selectCache, selectListState, useListState } from '../selectors';
|
||
|
|
||
|
import { parseEntitiesPath } from './utils';
|
||
|
|
||
|
import type { Entity } from '../types';
|
||
|
import type { EntitiesPath, EntityFn, EntitySchema, ExpandedEntitiesPath } from './types';
|
||
|
import type { RootState } from 'soapbox/store';
|
||
|
|
||
|
interface UseBatchedEntitiesOpts<TEntity extends Entity> {
|
||
|
schema?: EntitySchema<TEntity>
|
||
|
enabled?: boolean
|
||
|
}
|
||
|
|
||
|
function useBatchedEntities<TEntity extends Entity>(
|
||
|
expandedPath: ExpandedEntitiesPath,
|
||
|
ids: string[],
|
||
|
entityFn: EntityFn<string[]>,
|
||
|
opts: UseBatchedEntitiesOpts<TEntity> = {},
|
||
|
) {
|
||
|
const getState = useGetState();
|
||
|
const dispatch = useAppDispatch();
|
||
|
const { entityType, listKey, path } = parseEntitiesPath(expandedPath);
|
||
|
const schema = opts.schema || z.custom<TEntity>();
|
||
|
|
||
|
const isEnabled = opts.enabled ?? true;
|
||
|
const isFetching = useListState(path, 'fetching');
|
||
|
const lastFetchedAt = useListState(path, 'lastFetchedAt');
|
||
|
const isFetched = useListState(path, 'fetched');
|
||
|
const isInvalid = useListState(path, 'invalid');
|
||
|
const error = useListState(path, 'error');
|
||
|
|
||
|
/** Get IDs of entities not yet in the store. */
|
||
|
const filteredIds = useAppSelector((state) => {
|
||
|
const cache = selectCache(state, path);
|
||
|
if (!cache) return ids;
|
||
|
return ids.filter((id) => !cache.store[id]);
|
||
|
});
|
||
|
|
||
|
const entityMap = useAppSelector((state) => selectEntityMap<TEntity>(state, path, ids));
|
||
|
|
||
|
async function fetchEntities() {
|
||
|
const isFetching = selectListState(getState(), path, 'fetching');
|
||
|
if (isFetching) return;
|
||
|
|
||
|
dispatch(entitiesFetchRequest(entityType, listKey));
|
||
|
try {
|
||
|
const response = await entityFn(filteredIds);
|
||
|
const entities = filteredArray(schema).parse(response.data);
|
||
|
dispatch(entitiesFetchSuccess(entities, entityType, listKey, 'end', {
|
||
|
next: undefined,
|
||
|
prev: undefined,
|
||
|
totalCount: undefined,
|
||
|
fetching: false,
|
||
|
fetched: true,
|
||
|
error: null,
|
||
|
lastFetchedAt: new Date(),
|
||
|
invalid: false,
|
||
|
}));
|
||
|
} catch (e) {
|
||
|
dispatch(entitiesFetchFail(entityType, listKey, e));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
useEffect(() => {
|
||
|
if (filteredIds.length && isEnabled) {
|
||
|
fetchEntities();
|
||
|
}
|
||
|
}, [filteredIds.length]);
|
||
|
|
||
|
return {
|
||
|
entityMap,
|
||
|
isFetching,
|
||
|
lastFetchedAt,
|
||
|
isFetched,
|
||
|
isError: !!error,
|
||
|
isInvalid,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function selectEntityMap<TEntity extends Entity>(
|
||
|
state: RootState,
|
||
|
path: EntitiesPath,
|
||
|
entityIds: string[],
|
||
|
): Record<string, TEntity> {
|
||
|
const cache = selectCache(state, path);
|
||
|
|
||
|
return entityIds.reduce<Record<string, TEntity>>((result, id) => {
|
||
|
const entity = cache?.store[id];
|
||
|
if (entity) {
|
||
|
result[id] = entity as TEntity;
|
||
|
}
|
||
|
return result;
|
||
|
}, {});
|
||
|
}
|
||
|
|
||
|
export { useBatchedEntities };
|