use-data-loader.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import { useMemo, useRef } from 'react';
  2. import type { TreeDataLoader } from '@headless-tree/core';
  3. import { apiv3Get } from '~/client/util/apiv3-client';
  4. import type { IPageForTreeItem } from '~/interfaces/page';
  5. import {
  6. CREATING_PAGE_VIRTUAL_ID,
  7. ROOT_PAGE_VIRTUAL_ID,
  8. } from '../../constants/_inner';
  9. import { type ChildrenData, fetchAndCacheChildren } from '../../services';
  10. import {
  11. createPlaceholderPageData,
  12. useCreatingParentId,
  13. useCreatingParentPath,
  14. } from '../../states/_inner';
  15. function constructRootPageForVirtualRoot(
  16. rootPageId: string,
  17. allPagesCount: number,
  18. ): IPageForTreeItem {
  19. return {
  20. _id: rootPageId,
  21. path: '/',
  22. parent: null,
  23. descendantCount: allPagesCount,
  24. grant: 1,
  25. isEmpty: false,
  26. wip: false,
  27. };
  28. }
  29. export const useDataLoader = (
  30. rootPageId: string,
  31. allPagesCount: number,
  32. ): TreeDataLoader<IPageForTreeItem> => {
  33. const creatingParentId = useCreatingParentId();
  34. const creatingParentPath = useCreatingParentPath();
  35. // Use refs to avoid recreating dataLoader callbacks when creating state changes
  36. // The creating state is accessed via refs so that:
  37. // 1. The dataLoader reference stays stable (prevents headless-tree from refetching all data)
  38. // 2. The actual creating state is still read at execution time (when invalidateChildrenIds is called)
  39. const creatingParentIdRef = useRef(creatingParentId);
  40. const creatingParentPathRef = useRef(creatingParentPath);
  41. creatingParentIdRef.current = creatingParentId;
  42. creatingParentPathRef.current = creatingParentPath;
  43. // Memoize the entire dataLoader object to ensure reference stability
  44. // Only recreate when rootPageId or allPagesCount changes (which are truly needed for the API calls)
  45. // Note: Creating state is read from refs inside callbacks to avoid triggering dataLoader recreation
  46. const dataLoader = useMemo<TreeDataLoader<IPageForTreeItem>>(() => {
  47. const getItem = async (itemId: string): Promise<IPageForTreeItem> => {
  48. // Virtual root (should rarely be called since it's provided by getChildrenWithData)
  49. if (itemId === ROOT_PAGE_VIRTUAL_ID) {
  50. return constructRootPageForVirtualRoot(rootPageId, allPagesCount);
  51. }
  52. // Creating placeholder node - return placeholder data
  53. if (itemId === CREATING_PAGE_VIRTUAL_ID) {
  54. // This shouldn't normally be called, but return empty placeholder if it is
  55. return createPlaceholderPageData('', '/');
  56. }
  57. // For all pages (including root), use /page-listing/item endpoint
  58. // Note: This should rarely be called thanks to getChildrenWithData caching
  59. const response = await apiv3Get<{ item: IPageForTreeItem }>(
  60. '/page-listing/item',
  61. { id: itemId },
  62. );
  63. return response.data.item;
  64. };
  65. const getChildrenWithData = async (
  66. itemId: string,
  67. ): Promise<ChildrenData> => {
  68. // Virtual root returns root page as its only child
  69. // Use actual MongoDB _id as tree item ID to avoid duplicate API calls
  70. if (itemId === ROOT_PAGE_VIRTUAL_ID) {
  71. return [
  72. {
  73. id: rootPageId,
  74. data: constructRootPageForVirtualRoot(rootPageId, allPagesCount),
  75. },
  76. ];
  77. }
  78. // Placeholder node has no children
  79. if (itemId === CREATING_PAGE_VIRTUAL_ID) {
  80. return [];
  81. }
  82. const children = await fetchAndCacheChildren(itemId);
  83. // If this parent is in "creating" mode, prepend placeholder node
  84. // Read from refs to get current value without triggering dataLoader recreation
  85. const currentCreatingParentId = creatingParentIdRef.current;
  86. const currentCreatingParentPath = creatingParentPathRef.current;
  87. if (
  88. currentCreatingParentId === itemId &&
  89. currentCreatingParentPath != null
  90. ) {
  91. const placeholderData = createPlaceholderPageData(
  92. itemId,
  93. currentCreatingParentPath,
  94. );
  95. return [
  96. { id: CREATING_PAGE_VIRTUAL_ID, data: placeholderData },
  97. ...children,
  98. ];
  99. }
  100. return children;
  101. };
  102. return { getItem, getChildrenWithData };
  103. }, [allPagesCount, rootPageId]);
  104. return dataLoader;
  105. };