UNPKG

@replyke/core

Version:

Replyke: Build interactive apps with social features like comments, votes, feeds, user lists, notifications, and more.

300 lines 13.3 kB
import { useCallback, useMemo, useRef } from "react"; import { useReplykeDispatch, useReplykeSelector } from "../../store/hooks"; import { initializeList, updateFiltersAndSortConfig, setEntityListLoading, setEntityListEntities, incrementPage, selectEntityList, selectEntityListEntities, selectEntityListLoading, selectEntityListHasMore, selectEntityListFilters, selectEntityListSort, selectEntityListConfig, } from "../../store/slices/entityListsSlice"; import useInfusedData from "./useInfusedData"; import useEntityListActions from "./useEntityListActions"; import { handleError } from "../../utils/handleError"; function useEntityList({ listId, infuseData, }) { const dispatch = useReplykeDispatch(); // Get state from Redux (parameterized selectors) const entityList = useReplykeSelector((state) => selectEntityList(state, listId)); const entities = useReplykeSelector((state) => selectEntityListEntities(state, listId)); const loading = useReplykeSelector((state) => selectEntityListLoading(state, listId)); const hasMore = useReplykeSelector((state) => selectEntityListHasMore(state, listId)); const filters = useReplykeSelector((state) => selectEntityListFilters(state, listId)); const sort = useReplykeSelector((state) => selectEntityListSort(state, listId)); const config = useReplykeSelector((state) => selectEntityListConfig(state, listId)); // Get entity actions hook const entityActions = useEntityListActions(); // Infused data const infusedEntities = useInfusedData({ entities, infuseData }); // Debounce timer for filter changes const debounceTimer = useRef(null); // Fetch entities function (always triggers a fetch) const handleFetchEntities = useCallback((newFilters, newSort, newConfig, options) => { // Apply config defaults if not provided const configWithDefaults = { sourceId: null, limit: 10, include: null, ...newConfig, }; // Ensure Redux state is initialized and update filters/sort/config dispatch(initializeList({ listId })); dispatch(updateFiltersAndSortConfig({ listId, filters: newFilters, sort: newSort, config: configWithDefaults, options })); // Clear entities immediately if requested if (options?.clearImmediately) { dispatch(setEntityListEntities({ listId, entities: [], append: false })); } // Define the fetch logic const performFetch = async () => { // Use the applied config (configWithDefaults is the source of truth for this fetch) const currentConfig = { sourceId: configWithDefaults.sourceId, spaceId: configWithDefaults.spaceId, limit: configWithDefaults.limit, include: configWithDefaults.include, }; // Build final filters by taking current state and applying new filters // Use entityList if available, otherwise get current state from Redux after our updates const currentState = entityList || { sortBy: "hot", sortByReaction: "upvote", sortDir: null, sortType: "auto", timeFrame: null, userId: null, followedOnly: false, keywordsFilters: null, titleFilters: null, contentFilters: null, attachmentsFilters: null, locationFilters: null, metadataFilters: null, }; const finalFilters = { ...currentState }; // Apply resetFilters flag - reset only filter properties if (options?.resetFilters) { finalFilters.timeFrame = null; finalFilters.userId = null; finalFilters.followedOnly = false; finalFilters.keywordsFilters = null; finalFilters.titleFilters = null; finalFilters.contentFilters = null; finalFilters.attachmentsFilters = null; finalFilters.locationFilters = null; finalFilters.metadataFilters = null; } // Apply resetSort flag - reset only sort properties if (options?.resetSort) { finalFilters.sortBy = "hot"; finalFilters.sortByReaction = "upvote"; finalFilters.sortDir = null; finalFilters.sortType = "auto"; } // Apply new filters Object.keys(newFilters).forEach((key) => { if (newFilters[key] !== undefined) { finalFilters[key] = newFilters[key]; } }); // Apply new sort if (newSort) { if (newSort.sortBy !== undefined) finalFilters.sortBy = newSort.sortBy; if (newSort.sortByReaction !== undefined) finalFilters.sortByReaction = newSort.sortByReaction; if (newSort.sortDir !== undefined) finalFilters.sortDir = newSort.sortDir; if (newSort.sortType !== undefined) finalFilters.sortType = newSort.sortType; } if (!finalFilters.sortBy) return; // sortBy is required dispatch(setEntityListLoading({ listId, loading: true })); try { await entityActions.fetchEntities(listId, { page: 1, // User-controlled filters from Redux state + new filters sortBy: finalFilters.sortBy, sortByReaction: finalFilters.sortByReaction, sortDir: finalFilters.sortDir, sortType: finalFilters.sortType, timeFrame: finalFilters.timeFrame, userId: finalFilters.userId, followedOnly: finalFilters.followedOnly, locationFilters: finalFilters.locationFilters, keywordsFilters: finalFilters.keywordsFilters, metadataFilters: finalFilters.metadataFilters, titleFilters: finalFilters.titleFilters, contentFilters: finalFilters.contentFilters, attachmentsFilters: finalFilters.attachmentsFilters, // Configuration parameters from current config limit: currentConfig.limit, sourceId: currentConfig.sourceId, spaceId: currentConfig.spaceId, include: currentConfig.include, }); } catch (err) { console.error(`[EntityListRedux] Failed to fetch entities for listId: ${listId}`, err); } }; // Execute immediately if requested, otherwise debounce // For initial loads (empty filters and no sort), make it immediate by default const shouldBeImmediate = options?.fetchImmediately || (Object.keys(newFilters).length === 0 && !newSort); if (shouldBeImmediate) { performFetch(); } else { // Clear existing debounce timer if (debounceTimer.current) { clearTimeout(debounceTimer.current); } // Debounce the actual fetch debounceTimer.current = setTimeout(() => { performFetch(); }, 800); // 800ms debounce delay } }, [dispatch, listId, entityList, config, entityActions.fetchEntities]); // Load more function const loadMore = useCallback(async () => { if (!entityList || loading || !hasMore) return; // Check if fetchEntities has been called before (safeguard) if (!config) { console.error(`[EntityListRedux] loadMore called before fetchEntities for listId: ${listId}. ` + `fetchEntities must be called first to initialize configuration.`); return; } const nextPage = entityList.page + 1; dispatch(incrementPage(listId)); // Directly fetch the next page try { await entityActions.fetchEntities(listId, { page: nextPage, // User-controlled filters from Redux state userId: entityList.userId, followedOnly: entityList.followedOnly, sortBy: entityList.sortBy, sortByReaction: entityList.sortByReaction, sortDir: entityList.sortDir, sortType: entityList.sortType, timeFrame: entityList.timeFrame, locationFilters: entityList.locationFilters, keywordsFilters: entityList.keywordsFilters, metadataFilters: entityList.metadataFilters, titleFilters: entityList.titleFilters, contentFilters: entityList.contentFilters, attachmentsFilters: entityList.attachmentsFilters, // Configuration parameters from state (single source of truth) limit: config.limit, sourceId: config.sourceId, spaceId: config.spaceId, include: config.include, }); } catch (err) { console.error(`[EntityListRedux] Failed to load more entities for listId: ${listId}`, err); } }, [ dispatch, listId, config, entityList, loading, hasMore, entityActions.fetchEntities, ]); // Create entity function const createEntity = useCallback(async ({ insertPosition, ...restOfProps }) => { try { const newEntity = await entityActions.createEntity(listId, { ...restOfProps, sourceId: config?.sourceId || undefined, spaceId: config?.spaceId || undefined, insertPosition, }); return newEntity; } catch (err) { // Error handling is now done in entityActions.createEntity handleError(err, "Failed to create entity"); } }, [entityActions.createEntity, dispatch, listId, config]); // Delete entity function const deleteEntity = useCallback(async ({ entityId }) => { try { await entityActions.deleteEntity(listId, { entityId }); } catch (err) { // Error handling is now done in entityActions.deleteEntity handleError(err, "Failed to delete entity"); } }, [entityActions.deleteEntity, dispatch, listId]); // Load more entities when page changes - REMOVED // This useEffect was causing duplicate API calls and race conditions // Load more is now handled directly in the loadMore function // fetchEntities now handles fetching directly when called // No automatic filter change detection needed // Legacy setEntities function for compatibility // const handleSetEntities = useCallback( // (updater: React.SetStateAction<Entity[]>) => { // if (typeof updater === "function") { // const newEntities = updater(entities); // dispatch( // setEntityListEntities({ // listId, // entities: newEntities, // append: false, // }) // ); // } else { // dispatch( // setEntityListEntities({ listId, entities: updater, append: false }) // ); // } // }, // [dispatch, listId, entities] // ); // No automatic initialization - state is only created when fetchEntities is called return useMemo(() => ({ entities, // setEntities: handleSetEntities, infusedEntities, loading, hasMore, // Individual sort properties (flat access for convenience) sortBy: sort?.sortBy || null, sortByReaction: sort?.sortByReaction || null, sortDir: sort?.sortDir || null, sortType: sort?.sortType || null, // Filter properties timeFrame: filters?.timeFrame || null, sourceId: config?.sourceId || null, userId: filters?.userId || null, followedOnly: filters?.followedOnly || false, keywordsFilters: filters?.keywordsFilters || null, titleFilters: filters?.titleFilters || null, contentFilters: filters?.contentFilters || null, attachmentsFilters: filters?.attachmentsFilters || null, locationFilters: filters?.locationFilters || null, metadataFilters: filters?.metadataFilters || null, fetchEntities: handleFetchEntities, loadMore, createEntity, deleteEntity, }), [ entities, infusedEntities, loading, hasMore, sort, filters, config, handleFetchEntities, loadMore, createEntity, deleteEntity, ]); } export default useEntityList; //# sourceMappingURL=useEntityList.js.map