diff --git a/app/soapbox/entity-store/actions.ts b/app/soapbox/entity-store/actions.ts index 5a05100c8..30ae75535 100644 --- a/app/soapbox/entity-store/actions.ts +++ b/app/soapbox/entity-store/actions.ts @@ -6,6 +6,7 @@ const ENTITIES_DISMISS = 'ENTITIES_DISMISS' as const; const ENTITIES_FETCH_REQUEST = 'ENTITIES_FETCH_REQUEST' as const; const ENTITIES_FETCH_SUCCESS = 'ENTITIES_FETCH_SUCCESS' as const; const ENTITIES_FETCH_FAIL = 'ENTITIES_FETCH_FAIL' as const; +const ENTITIES_INVALIDATE_LIST = 'ENTITIES_INVALIDATE_LIST' as const; /** Action to import entities into the cache. */ function importEntities(entities: Entity[], entityType: string, listKey?: string) { @@ -66,6 +67,14 @@ function entitiesFetchFail(entityType: string, listKey: string | undefined, erro }; } +function invalidateEntityList(entityType: string, listKey: string) { + return { + type: ENTITIES_INVALIDATE_LIST, + entityType, + listKey, + }; +} + /** Any action pertaining to entities. */ type EntityAction = ReturnType @@ -73,7 +82,8 @@ type EntityAction = | ReturnType | ReturnType | ReturnType - | ReturnType; + | ReturnType + | ReturnType; export { ENTITIES_IMPORT, @@ -82,12 +92,14 @@ export { ENTITIES_FETCH_REQUEST, ENTITIES_FETCH_SUCCESS, ENTITIES_FETCH_FAIL, + ENTITIES_INVALIDATE_LIST, importEntities, deleteEntities, dismissEntities, entitiesFetchRequest, entitiesFetchSuccess, entitiesFetchFail, + invalidateEntityList, EntityAction, }; diff --git a/app/soapbox/entity-store/hooks/useEntities.ts b/app/soapbox/entity-store/hooks/useEntities.ts index 0f345d675..9ba7ad4f3 100644 --- a/app/soapbox/entity-store/hooks/useEntities.ts +++ b/app/soapbox/entity-store/hooks/useEntities.ts @@ -5,7 +5,7 @@ import { getNextLink, getPrevLink } from 'soapbox/api'; import { useApi, useAppDispatch, useAppSelector, useGetState } from 'soapbox/hooks'; import { filteredArray } from 'soapbox/schemas/utils'; -import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess } from '../actions'; +import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess, invalidateEntityList } from '../actions'; import { parseEntitiesPath } from './utils'; @@ -48,6 +48,7 @@ function useEntities( const isFetched = useListState(path, 'fetched'); const isError = !!useListState(path, 'error'); const totalCount = useListState(path, 'totalCount'); + const isInvalid = useListState(path, 'invalid'); const next = useListState(path, 'next'); const prev = useListState(path, 'prev'); @@ -72,6 +73,7 @@ function useEntities( fetched: true, error: null, lastFetchedAt: new Date(), + invalid: false, })); } catch (error) { dispatch(entitiesFetchFail(entityType, listKey, error)); @@ -96,10 +98,19 @@ function useEntities( } }; + const invalidate = () => { + dispatch(invalidateEntityList(entityType, listKey)); + }; + const staleTime = opts.staleTime ?? 60000; useEffect(() => { - if (isEnabled && !isFetching && (!lastFetchedAt || lastFetchedAt.getTime() + staleTime <= Date.now())) { + if (!isEnabled) return; + if (isFetching) return; + const isUnset = !lastFetchedAt; + const isStale = lastFetchedAt ? Date.now() >= lastFetchedAt.getTime() + staleTime : false; + + if (isInvalid || isUnset || isStale) { fetchEntities(); } }, [endpoint, isEnabled]); @@ -116,6 +127,7 @@ function useEntities( isFetched, isFetching, isLoading: isFetching && entities.length === 0, + invalidate, }; } diff --git a/app/soapbox/entity-store/reducer.ts b/app/soapbox/entity-store/reducer.ts index 4f9c1e4d2..7654257cc 100644 --- a/app/soapbox/entity-store/reducer.ts +++ b/app/soapbox/entity-store/reducer.ts @@ -8,6 +8,7 @@ import { ENTITIES_FETCH_SUCCESS, ENTITIES_FETCH_FAIL, EntityAction, + ENTITIES_INVALIDATE_LIST, } from './actions'; import { createCache, createList, updateStore, updateList } from './utils'; @@ -114,6 +115,14 @@ const setFetching = ( }); }; +const invalidateEntityList = (state: State, entityType: string, listKey: string) => { + return produce(state, draft => { + const cache = draft[entityType] ?? createCache(); + const list = cache.lists[listKey] ?? createList(); + list.state.invalid = true; + }); +}; + /** Stores various entity data and lists in a one reducer. */ function reducer(state: Readonly = {}, action: EntityAction): State { switch (action.type) { @@ -129,6 +138,8 @@ function reducer(state: Readonly = {}, action: EntityAction): State { return setFetching(state, action.entityType, action.listKey, true); case ENTITIES_FETCH_FAIL: return setFetching(state, action.entityType, action.listKey, false, action.error); + case ENTITIES_INVALIDATE_LIST: + return invalidateEntityList(state, action.entityType, action.listKey); default: return state; } diff --git a/app/soapbox/entity-store/types.ts b/app/soapbox/entity-store/types.ts index 09e6c0174..67f37180d 100644 --- a/app/soapbox/entity-store/types.ts +++ b/app/soapbox/entity-store/types.ts @@ -33,6 +33,8 @@ interface EntityListState { fetching: boolean /** Date of the last API fetch for this list. */ lastFetchedAt: Date | undefined + /** Whether the entities should be refetched on the next component mount. */ + invalid: boolean } /** Cache data pertaining to a paritcular entity type.. */ diff --git a/app/soapbox/entity-store/utils.ts b/app/soapbox/entity-store/utils.ts index 0040ae674..cd023cc9c 100644 --- a/app/soapbox/entity-store/utils.ts +++ b/app/soapbox/entity-store/utils.ts @@ -43,6 +43,7 @@ const createListState = (): EntityListState => ({ fetched: false, fetching: false, lastFetchedAt: undefined, + invalid: false, }); export {