From a530ec0202d832c8ed52b2a8a07e9a37cb1fb0e7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 23 Mar 2023 19:22:26 -0500 Subject: [PATCH] EntityStore: switch all hooks to use a callback function --- app/soapbox/entity-store/hooks/types.ts | 8 +++---- .../entity-store/hooks/useCreateEntity.ts | 19 ++++++---------- .../entity-store/hooks/useDeleteEntity.ts | 17 +++++--------- .../entity-store/hooks/useDismissEntity.ts | 22 +++++++++++-------- app/soapbox/entity-store/hooks/useEntities.ts | 21 +++++++++--------- app/soapbox/entity-store/hooks/useEntity.ts | 12 +++++----- .../entity-store/hooks/useEntityActions.ts | 7 ++++-- .../entity-store/hooks/useEntityRequest.ts | 21 ------------------ .../entity-store/hooks/useIncrementEntity.ts | 22 +++++++++++-------- app/soapbox/entity-store/hooks/utils.ts | 15 ++----------- .../api/groups/useGroupMembershipRequests.ts | 18 +++++++-------- app/soapbox/hooks/api/useGroupMembers.ts | 6 ++++- app/soapbox/hooks/api/usePopularGroups.ts | 4 +++- app/soapbox/hooks/api/useSuggestedGroups.ts | 4 +++- app/soapbox/hooks/useGroups.ts | 15 +++++++++---- 15 files changed, 95 insertions(+), 116 deletions(-) delete mode 100644 app/soapbox/entity-store/hooks/useEntityRequest.ts diff --git a/app/soapbox/entity-store/hooks/types.ts b/app/soapbox/entity-store/hooks/types.ts index e9df8c18b..95ba8b016 100644 --- a/app/soapbox/entity-store/hooks/types.ts +++ b/app/soapbox/entity-store/hooks/types.ts @@ -1,5 +1,5 @@ import type { Entity } from '../types'; -import type { AxiosRequestConfig } from 'axios'; +import type { AxiosResponse } from 'axios'; import type z from 'zod'; type EntitySchema = z.ZodType; @@ -33,9 +33,9 @@ interface EntityCallbacks { /** * Passed into hooks to make requests. - * Can be a URL for GET requests, or a request object. + * Must return an Axios response. */ -type EntityRequest = string | URL | AxiosRequestConfig; +type EntityFn = (value: T) => Promise export type { EntitySchema, @@ -43,5 +43,5 @@ export type { EntitiesPath, EntityPath, EntityCallbacks, - EntityRequest, + EntityFn, }; \ No newline at end of file diff --git a/app/soapbox/entity-store/hooks/useCreateEntity.ts b/app/soapbox/entity-store/hooks/useCreateEntity.ts index 658cb180d..ba9dd802b 100644 --- a/app/soapbox/entity-store/hooks/useCreateEntity.ts +++ b/app/soapbox/entity-store/hooks/useCreateEntity.ts @@ -1,36 +1,31 @@ import { z } from 'zod'; -import { useAppDispatch } from 'soapbox/hooks'; +import { useAppDispatch, useLoading } from 'soapbox/hooks'; import { importEntities } from '../actions'; -import { useEntityRequest } from './useEntityRequest'; -import { parseEntitiesPath, toAxiosRequest } from './utils'; +import { parseEntitiesPath } from './utils'; import type { Entity } from '../types'; -import type { EntityCallbacks, EntityRequest, EntitySchema, ExpandedEntitiesPath } from './types'; +import type { EntityCallbacks, EntityFn, EntitySchema, ExpandedEntitiesPath } from './types'; interface UseCreateEntityOpts { schema?: EntitySchema } -function useCreateEntity( +function useCreateEntity( expandedPath: ExpandedEntitiesPath, - entityRequest: EntityRequest, + entityFn: EntityFn, opts: UseCreateEntityOpts = {}, ) { const dispatch = useAppDispatch(); - const { request, isLoading } = useEntityRequest(); + const [isLoading, setPromise] = useLoading(); const { entityType, listKey } = parseEntitiesPath(expandedPath); async function createEntity(data: Data, callbacks: EntityCallbacks = {}): Promise { try { - const result = await request({ - ...toAxiosRequest(entityRequest), - data, - }); - + const result = await setPromise(entityFn(data)); const schema = opts.schema || z.custom(); const entity = schema.parse(result.data); diff --git a/app/soapbox/entity-store/hooks/useDeleteEntity.ts b/app/soapbox/entity-store/hooks/useDeleteEntity.ts index a8c671cc1..767224af6 100644 --- a/app/soapbox/entity-store/hooks/useDeleteEntity.ts +++ b/app/soapbox/entity-store/hooks/useDeleteEntity.ts @@ -1,11 +1,8 @@ -import { useAppDispatch, useGetState } from 'soapbox/hooks'; +import { useAppDispatch, useGetState, useLoading } from 'soapbox/hooks'; import { deleteEntities, importEntities } from '../actions'; -import { useEntityRequest } from './useEntityRequest'; -import { toAxiosRequest } from './utils'; - -import type { EntityCallbacks, EntityRequest } from './types'; +import type { EntityCallbacks, EntityFn } from './types'; /** * Optimistically deletes an entity from the store. @@ -14,11 +11,11 @@ import type { EntityCallbacks, EntityRequest } from './types'; */ function useDeleteEntity( entityType: string, - entityRequest: EntityRequest, + entityFn: EntityFn, ) { const dispatch = useAppDispatch(); const getState = useGetState(); - const { request, isLoading } = useEntityRequest(); + const [isLoading, setPromise] = useLoading(); async function deleteEntity(entityId: string, callbacks: EntityCallbacks = {}): Promise { // Get the entity before deleting, so we can reverse the action if the API request fails. @@ -28,11 +25,7 @@ function useDeleteEntity( dispatch(deleteEntities([entityId], entityType, { preserveLists: true })); try { - // HACK: replace occurrences of `:id` in the URL. Maybe there's a better way? - const axiosReq = toAxiosRequest(entityRequest); - axiosReq.url?.replaceAll(':id', entityId); - - await request(axiosReq); + await setPromise(entityFn(entityId)); // Success - finish deleting entity from the state. dispatch(deleteEntities([entityId], entityType)); diff --git a/app/soapbox/entity-store/hooks/useDismissEntity.ts b/app/soapbox/entity-store/hooks/useDismissEntity.ts index 1ba5f4a60..b09e35951 100644 --- a/app/soapbox/entity-store/hooks/useDismissEntity.ts +++ b/app/soapbox/entity-store/hooks/useDismissEntity.ts @@ -1,27 +1,31 @@ -import { useAppDispatch } from 'soapbox/hooks'; +import { useAppDispatch, useLoading } from 'soapbox/hooks'; import { dismissEntities } from '../actions'; import { parseEntitiesPath } from './utils'; -import type { ExpandedEntitiesPath } from './types'; - -type DismissFn = (entityId: string) => Promise | T; +import type { EntityFn, ExpandedEntitiesPath } from './types'; /** * Removes an entity from a specific list. * To remove an entity globally from all lists, see `useDeleteEntity`. */ -function useDismissEntity(expandedPath: ExpandedEntitiesPath, dismissFn: DismissFn) { - const { entityType, listKey } = parseEntitiesPath(expandedPath); - +function useDismissEntity(expandedPath: ExpandedEntitiesPath, entityFn: EntityFn) { const dispatch = useAppDispatch(); + const [isLoading, setPromise] = useLoading(); + const { entityType, listKey } = parseEntitiesPath(expandedPath); + // TODO: optimistic dismissing - return async function dismissEntity(entityId: string): Promise { - const result = await dismissFn(entityId); + async function dismissEntity(entityId: string) { + const result = await setPromise(entityFn(entityId)); dispatch(dismissEntities([entityId], entityType, listKey)); return result; + } + + return { + dismissEntity, + isLoading, }; } diff --git a/app/soapbox/entity-store/hooks/useEntities.ts b/app/soapbox/entity-store/hooks/useEntities.ts index c20d75f43..f2e84c93e 100644 --- a/app/soapbox/entity-store/hooks/useEntities.ts +++ b/app/soapbox/entity-store/hooks/useEntities.ts @@ -2,17 +2,16 @@ import { useEffect } from 'react'; import z from 'zod'; import { getNextLink, getPrevLink } from 'soapbox/api'; -import { useAppDispatch, useAppSelector, useGetState } from 'soapbox/hooks'; +import { useApi, useAppDispatch, useAppSelector, useGetState } from 'soapbox/hooks'; import { filteredArray } from 'soapbox/schemas/utils'; import { realNumberSchema } from 'soapbox/utils/numbers'; import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess, invalidateEntityList } from '../actions'; -import { useEntityRequest } from './useEntityRequest'; import { parseEntitiesPath } from './utils'; import type { Entity, EntityListState } from '../types'; -import type { EntitiesPath, EntityRequest, EntitySchema, ExpandedEntitiesPath } from './types'; +import type { EntitiesPath, EntityFn, EntitySchema, ExpandedEntitiesPath } from './types'; import type { RootState } from 'soapbox/store'; /** Additional options for the hook. */ @@ -33,11 +32,11 @@ function useEntities( /** Tells us where to find/store the entity in the cache. */ expandedPath: ExpandedEntitiesPath, /** API route to GET, eg `'/api/v1/notifications'`. If undefined, nothing will be fetched. */ - entityRequest: EntityRequest, + entityFn: EntityFn, /** Additional options for the hook. */ opts: UseEntitiesOpts = {}, ) { - const { request } = useEntityRequest(); + const api = useApi(); const dispatch = useAppDispatch(); const getState = useGetState(); @@ -55,14 +54,14 @@ function useEntities( const next = useListState(path, 'next'); const prev = useListState(path, 'prev'); - const fetchPage = async(req: EntityRequest, overwrite = false): Promise => { + const fetchPage = async(req: EntityFn, overwrite = false): Promise => { // Get `isFetching` state from the store again to prevent race conditions. const isFetching = selectListState(getState(), path, 'fetching'); if (isFetching) return; dispatch(entitiesFetchRequest(entityType, listKey)); try { - const response = await request(req); + const response = await req(); const schema = opts.schema || z.custom(); const entities = filteredArray(schema).parse(response.data); const parsedCount = realNumberSchema.safeParse(response.headers['x-total-count']); @@ -83,18 +82,18 @@ function useEntities( }; const fetchEntities = async(): Promise => { - await fetchPage(entityRequest, true); + await fetchPage(entityFn, true); }; const fetchNextPage = async(): Promise => { if (next) { - await fetchPage(next); + await fetchPage(() => api.get(next)); } }; const fetchPreviousPage = async(): Promise => { if (prev) { - await fetchPage(prev); + await fetchPage(() => api.get(prev)); } }; @@ -113,7 +112,7 @@ function useEntities( if (isInvalid || isUnset || isStale) { fetchEntities(); } - }, [entityRequest, isEnabled]); + }, [isEnabled]); return { entities, diff --git a/app/soapbox/entity-store/hooks/useEntity.ts b/app/soapbox/entity-store/hooks/useEntity.ts index 1b091c655..f30c9a18a 100644 --- a/app/soapbox/entity-store/hooks/useEntity.ts +++ b/app/soapbox/entity-store/hooks/useEntity.ts @@ -1,14 +1,12 @@ import { useEffect } from 'react'; import z from 'zod'; -import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useLoading } from 'soapbox/hooks'; import { importEntities } from '../actions'; -import { useEntityRequest } from './useEntityRequest'; - import type { Entity } from '../types'; -import type { EntitySchema, EntityPath, EntityRequest } from './types'; +import type { EntitySchema, EntityPath, EntityFn } from './types'; /** Additional options for the hook. */ interface UseEntityOpts { @@ -20,10 +18,10 @@ interface UseEntityOpts { function useEntity( path: EntityPath, - entityRequest: EntityRequest, + entityFn: EntityFn, opts: UseEntityOpts = {}, ) { - const { request, isLoading: isFetching } = useEntityRequest(); + const [isFetching, setPromise] = useLoading(); const dispatch = useAppDispatch(); const [entityType, entityId] = path; @@ -37,7 +35,7 @@ function useEntity( const fetchEntity = async () => { try { - const response = await request(entityRequest); + const response = await setPromise(entityFn()); const entity = schema.parse(response.data); dispatch(importEntities([entity], entityType)); } catch (e) { diff --git a/app/soapbox/entity-store/hooks/useEntityActions.ts b/app/soapbox/entity-store/hooks/useEntityActions.ts index f3dcd4db1..dab6f7f77 100644 --- a/app/soapbox/entity-store/hooks/useEntityActions.ts +++ b/app/soapbox/entity-store/hooks/useEntityActions.ts @@ -1,3 +1,5 @@ +import { useApi } from 'soapbox/hooks'; + import { useCreateEntity } from './useCreateEntity'; import { useDeleteEntity } from './useDeleteEntity'; import { parseEntitiesPath } from './utils'; @@ -19,13 +21,14 @@ function useEntityActions( endpoints: EntityActionEndpoints, opts: UseEntityActionsOpts = {}, ) { + const api = useApi(); const { entityType, path } = parseEntitiesPath(expandedPath); const { deleteEntity, isLoading: deleteLoading } = - useDeleteEntity(entityType, { method: 'delete', url: endpoints.delete }); + useDeleteEntity(entityType, (entityId) => api.delete(endpoints.delete!.replaceAll(':id', entityId))); const { createEntity, isLoading: createLoading } = - useCreateEntity(path, { method: 'post', url: endpoints.post }, opts); + useCreateEntity(path, (data) => api.post(endpoints.post!, data), opts); return { createEntity, diff --git a/app/soapbox/entity-store/hooks/useEntityRequest.ts b/app/soapbox/entity-store/hooks/useEntityRequest.ts deleted file mode 100644 index 678b8197c..000000000 --- a/app/soapbox/entity-store/hooks/useEntityRequest.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useApi, useLoading } from 'soapbox/hooks'; - -import { EntityRequest } from './types'; -import { toAxiosRequest } from './utils'; - -function useEntityRequest() { - const api = useApi(); - const [isLoading, setPromise] = useLoading(); - - function request(entityRequest: EntityRequest) { - const req = api.request(toAxiosRequest(entityRequest)); - return setPromise(req); - } - - return { - request, - isLoading, - }; -} - -export { useEntityRequest }; \ No newline at end of file diff --git a/app/soapbox/entity-store/hooks/useIncrementEntity.ts b/app/soapbox/entity-store/hooks/useIncrementEntity.ts index c0cbd133d..2b09cc445 100644 --- a/app/soapbox/entity-store/hooks/useIncrementEntity.ts +++ b/app/soapbox/entity-store/hooks/useIncrementEntity.ts @@ -1,32 +1,36 @@ -import { useAppDispatch } from 'soapbox/hooks'; +import { useAppDispatch, useLoading } from 'soapbox/hooks'; import { incrementEntities } from '../actions'; import { parseEntitiesPath } from './utils'; -import type { ExpandedEntitiesPath } from './types'; - -type IncrementFn = (entityId: string) => Promise | T; +import type { EntityFn, ExpandedEntitiesPath } from './types'; /** * Increases (or decreases) the `totalCount` in the entity list by the specified amount. * This only works if the API returns an `X-Total-Count` header and your components read it. */ -function useIncrementEntity( +function useIncrementEntity( expandedPath: ExpandedEntitiesPath, diff: number, - incrementFn: IncrementFn, + entityFn: EntityFn, ) { - const { entityType, listKey } = parseEntitiesPath(expandedPath); const dispatch = useAppDispatch(); + const [isLoading, setPromise] = useLoading(); + const { entityType, listKey } = parseEntitiesPath(expandedPath); - return async function incrementEntity(entityId: string): Promise { + async function incrementEntity(entityId: string): Promise { dispatch(incrementEntities(entityType, listKey, diff)); try { - await incrementFn(entityId); + await setPromise(entityFn(entityId)); } catch (e) { dispatch(incrementEntities(entityType, listKey, diff * -1)); } + } + + return { + incrementEntity, + isLoading, }; } diff --git a/app/soapbox/entity-store/hooks/utils.ts b/app/soapbox/entity-store/hooks/utils.ts index 741a5cf13..8b9269a2e 100644 --- a/app/soapbox/entity-store/hooks/utils.ts +++ b/app/soapbox/entity-store/hooks/utils.ts @@ -1,5 +1,4 @@ -import type { EntitiesPath, EntityRequest, ExpandedEntitiesPath } from './types'; -import type { AxiosRequestConfig } from 'axios'; +import type { EntitiesPath, ExpandedEntitiesPath } from './types'; function parseEntitiesPath(expandedPath: ExpandedEntitiesPath) { const [entityType, ...listKeys] = expandedPath; @@ -13,15 +12,5 @@ function parseEntitiesPath(expandedPath: ExpandedEntitiesPath) { }; } -function toAxiosRequest(req: EntityRequest): AxiosRequestConfig { - if (typeof req === 'string' || req instanceof URL) { - return { - method: 'get', - url: req.toString(), - }; - } - return req; -} - -export { parseEntitiesPath, toAxiosRequest }; \ No newline at end of file +export { parseEntitiesPath }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/groups/useGroupMembershipRequests.ts b/app/soapbox/hooks/api/groups/useGroupMembershipRequests.ts index 560aef329..6fab87209 100644 --- a/app/soapbox/hooks/api/groups/useGroupMembershipRequests.ts +++ b/app/soapbox/hooks/api/groups/useGroupMembershipRequests.ts @@ -11,20 +11,20 @@ function useGroupMembershipRequests(groupId: string) { const { entities, invalidate, ...rest } = useEntities( path, - `/api/v1/groups/${groupId}/membership_requests`, + () => api.get(`/api/v1/groups/${groupId}/membership_requests`), { schema: accountSchema }, ); - const authorize = useIncrementEntity(path, -1, (accountId: string) => { - return api - .post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`) - .then(invalidate); + const { incrementEntity: authorize } = useIncrementEntity(path, -1, async (accountId: string) => { + const response = await api.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`); + invalidate(); + return response; }); - const reject = useIncrementEntity(path, -1, (accountId: string) => { - return api - .post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`) - .then(invalidate); + const { incrementEntity: reject } = useIncrementEntity(path, -1, async (accountId: string) => { + const response = await api.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`); + invalidate(); + return response; }); return { diff --git a/app/soapbox/hooks/api/useGroupMembers.ts b/app/soapbox/hooks/api/useGroupMembers.ts index 8948660d6..669f1c082 100644 --- a/app/soapbox/hooks/api/useGroupMembers.ts +++ b/app/soapbox/hooks/api/useGroupMembers.ts @@ -2,10 +2,14 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useEntities } from 'soapbox/entity-store/hooks'; import { GroupMember, groupMemberSchema } from 'soapbox/schemas'; +import { useApi } from '../useApi'; + function useGroupMembers(groupId: string, role: string) { + const api = useApi(); + const { entities, ...result } = useEntities( [Entities.GROUP_MEMBERSHIPS, groupId, role], - `/api/v1/groups/${groupId}/memberships?role=${role}`, + () => api.get(`/api/v1/groups/${groupId}/memberships?role=${role}`), { schema: groupMemberSchema }, ); diff --git a/app/soapbox/hooks/api/usePopularGroups.ts b/app/soapbox/hooks/api/usePopularGroups.ts index 88ae48c9d..97d375174 100644 --- a/app/soapbox/hooks/api/usePopularGroups.ts +++ b/app/soapbox/hooks/api/usePopularGroups.ts @@ -2,15 +2,17 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useEntities } from 'soapbox/entity-store/hooks'; import { Group, groupSchema } from 'soapbox/schemas'; +import { useApi } from '../useApi'; import { useFeatures } from '../useFeatures'; import { useGroupRelationships } from '../useGroups'; function usePopularGroups() { + const api = useApi(); const features = useFeatures(); const { entities, ...result } = useEntities( [Entities.GROUPS, 'popular'], - '/api/mock/groups', // '/api/v1/truth/trends/groups' + () => api.get('/api/mock/groups'), // '/api/v1/truth/trends/groups' { schema: groupSchema, enabled: features.groupsDiscovery, diff --git a/app/soapbox/hooks/api/useSuggestedGroups.ts b/app/soapbox/hooks/api/useSuggestedGroups.ts index c1b85805c..9d5e20ace 100644 --- a/app/soapbox/hooks/api/useSuggestedGroups.ts +++ b/app/soapbox/hooks/api/useSuggestedGroups.ts @@ -2,15 +2,17 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useEntities } from 'soapbox/entity-store/hooks'; import { Group, groupSchema } from 'soapbox/schemas'; +import { useApi } from '../useApi'; import { useFeatures } from '../useFeatures'; import { useGroupRelationships } from '../useGroups'; function useSuggestedGroups() { + const api = useApi(); const features = useFeatures(); const { entities, ...result } = useEntities( [Entities.GROUPS, 'suggested'], - '/api/mock/groups', // '/api/v1/truth/suggestions/groups' + () => api.get('/api/mock/groups'), // '/api/v1/truth/suggestions/groups' { schema: groupSchema, enabled: features.groupsDiscovery, diff --git a/app/soapbox/hooks/useGroups.ts b/app/soapbox/hooks/useGroups.ts index 832d852a5..9bd0e99ca 100644 --- a/app/soapbox/hooks/useGroups.ts +++ b/app/soapbox/hooks/useGroups.ts @@ -2,17 +2,19 @@ import { z } from 'zod'; import { Entities } from 'soapbox/entity-store/entities'; import { useEntities, useEntity } from 'soapbox/entity-store/hooks'; +import { useApi } from 'soapbox/hooks'; import { groupSchema, Group } from 'soapbox/schemas/group'; import { groupRelationshipSchema, GroupRelationship } from 'soapbox/schemas/group-relationship'; import { useFeatures } from './useFeatures'; function useGroups() { + const api = useApi(); const features = useFeatures(); const { entities, ...result } = useEntities( [Entities.GROUPS], - '/api/v1/groups', + () => api.get('/api/v1/groups'), { enabled: features.groups, schema: groupSchema }, ); const { relationships } = useGroupRelationships(entities.map(entity => entity.id)); @@ -29,9 +31,11 @@ function useGroups() { } function useGroup(groupId: string, refetch = true) { + const api = useApi(); + const { entity: group, ...result } = useEntity( [Entities.GROUPS, groupId], - `/api/v1/groups/${groupId}`, + () => api.get(`/api/v1/groups/${groupId}`), { schema: groupSchema, refetch }, ); const { entity: relationship } = useGroupRelationship(groupId); @@ -43,18 +47,21 @@ function useGroup(groupId: string, refetch = true) { } function useGroupRelationship(groupId: string) { + const api = useApi(); + return useEntity( [Entities.GROUP_RELATIONSHIPS, groupId], - `/api/v1/groups/relationships?id[]=${groupId}`, + () => api.get(`/api/v1/groups/relationships?id[]=${groupId}`), { schema: z.array(groupRelationshipSchema).transform(arr => arr[0]) }, ); } function useGroupRelationships(groupIds: string[]) { + const api = useApi(); const q = groupIds.map(id => `id[]=${id}`).join('&'); const { entities, ...result } = useEntities( [Entities.GROUP_RELATIONSHIPS, ...groupIds], - `/api/v1/groups/relationships?${q}`, + () => api.get(`/api/v1/groups/relationships?${q}`), { schema: groupRelationshipSchema, enabled: groupIds.length > 0 }, );