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 { schema?: EntitySchema enabled?: boolean } function useBatchedEntities( expandedPath: ExpandedEntitiesPath, ids: string[], entityFn: EntityFn, opts: UseBatchedEntitiesOpts = {}, ) { const getState = useGetState(); const dispatch = useAppDispatch(); const { entityType, listKey, path } = parseEntitiesPath(expandedPath); const schema = opts.schema || z.custom(); 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(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( state: RootState, path: EntitiesPath, entityIds: string[], ): Record { const cache = selectCache(state, path); return entityIds.reduce>((result, id) => { const entity = cache?.store[id]; if (entity) { result[id] = entity as TEntity; } return result; }, {}); } export { useBatchedEntities };