soapbox/app/soapbox/entity-store/hooks/useBatchedEntities.ts

105 wiersze
3.1 KiB
TypeScript

import { useEffect } from 'react';
import { z } from 'zod';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch';
import { useAppSelector } from 'soapbox/hooks/useAppSelector';
import { useGetState } from 'soapbox/hooks/useGetState';
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 };