Sfoglia il codice sorgente

refactor(page-tree): replace useState rebuildTrigger with Jotai atom

- Create treeRebuildTriggerAtom for centralized tree rebuild triggering
- Replace multiple useCallback wrappers with stable triggerTreeRebuild from useSetAtom
- Remove unnecessary useState and useCallback imports
- Convert localGeneration from useState to useRef (no re-render needed)

Benefits:
- Centralized state can be shared across multiple tree components
- Better testability with Jotai Provider
- Cleaner code with fewer useCallback wrappers
- useSetAtom returns a stable function reference
Yuki Takei 4 mesi fa
parent
commit
c02a9590d7

+ 17 - 21
apps/app/src/features/page-tree/client/components/SimplifiedItemsTree.tsx

@@ -1,5 +1,5 @@
 import type { FC } from 'react';
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useEffect, useMemo, useRef } from 'react';
 import {
   asyncDataLoaderFeature,
   hotkeysCoreFeature,
@@ -22,6 +22,7 @@ import {
   usePageTreeInformationGeneration,
   usePageTreeRevalidationEffect,
 } from '../states/page-tree-update';
+import { useTreeRebuildTrigger, useTriggerTreeRebuild } from '../states/tree-rebuild';
 
 // Stable features array to avoid recreating on every render
 const TREE_FEATURES = [
@@ -65,7 +66,9 @@ export const SimplifiedItemsTree: FC<Props> = (props: Props) => {
     scrollerElem,
   } = props;
 
-  const [, setRebuildTrigger] = useState(0);
+  // Subscribe to rebuild trigger to re-render when tree structure changes
+  useTreeRebuildTrigger();
+  const triggerTreeRebuild = useTriggerTreeRebuild();
 
   const { data: rootPageResult } = useSWRxRootPage({ suspense: true });
   const rootPage = rootPageResult?.rootPage;
@@ -75,15 +78,13 @@ export const SimplifiedItemsTree: FC<Props> = (props: Props) => {
   const dataLoader = useDataLoader(rootPageId, allPagesCount);
 
   // Tree item handlers (rename, create, etc.) with stable callbacks for headless-tree
-  const handleAfterRename = useCallback(() => {
-    setRebuildTrigger((prev) => prev + 1);
-  }, []);
+  // Note: triggerTreeRebuild is stable (from useSetAtom), so no need for useCallback wrapper
   const {
     getItemName,
     isItemFolder,
     handleRename,
     creatingParentId,
-  } = useTreeItemHandlers(handleAfterRename);
+  } = useTreeItemHandlers(triggerTreeRebuild);
 
   // Stable initial state
   const initialState = useMemo(() => ({ expandedItems: [ROOT_PAGE_VIRTUAL_ID] }), []);
@@ -100,13 +101,13 @@ export const SimplifiedItemsTree: FC<Props> = (props: Props) => {
   });
 
   // Track local generation number
-  const [localGeneration, setLocalGeneration] = useState(1);
+  const localGenerationRef = useRef(1);
   const globalGeneration = usePageTreeInformationGeneration();
 
   // Refetch data when global generation is updated
-  usePageTreeRevalidationEffect(tree, localGeneration, {
+  usePageTreeRevalidationEffect(tree, localGenerationRef.current, {
     // Update local generation number after revalidation
-    onRevalidated: () => setLocalGeneration(globalGeneration),
+    onRevalidated: () => { localGenerationRef.current = globalGeneration; },
   });
 
   // Expand and rebuild tree when creatingParentId changes
@@ -129,8 +130,8 @@ export const SimplifiedItemsTree: FC<Props> = (props: Props) => {
     parentItem?.invalidateChildrenIds(true);
 
     // Trigger re-render
-    setRebuildTrigger((prev) => prev + 1);
-  }, [creatingParentId, tree]);
+    triggerTreeRebuild();
+  }, [creatingParentId, tree, triggerTreeRebuild]);
 
   const items = tree.getItems();
 
@@ -140,18 +141,16 @@ export const SimplifiedItemsTree: FC<Props> = (props: Props) => {
     if (items.length !== prevItemsCountRef.current) {
       prevItemsCountRef.current = items.length;
       // Trigger re-render when items count changes (e.g., after async load completes)
-      setRebuildTrigger((prev) => prev + 1);
+      triggerTreeRebuild();
     }
-  }, [items.length]);
+  }, [items.length, triggerTreeRebuild]);
 
   // Auto-expand items that are ancestors of targetPath
-  const handleAutoExpanded = useCallback(() => {
-    setRebuildTrigger((prev) => prev + 1);
-  }, []);
+  // Note: triggerTreeRebuild is stable, no need for useCallback wrapper
   useAutoExpandAncestors({
     items,
     targetPath,
-    onExpanded: handleAutoExpanded,
+    onExpanded: triggerTreeRebuild,
   });
 
   const virtualizer = useVirtualizer({
@@ -200,10 +199,7 @@ export const SimplifiedItemsTree: FC<Props> = (props: Props) => {
               isWipPageShown={isWipPageShown}
               isEnableActions={isEnableActions}
               isReadOnlyUser={isReadOnlyUser}
-              onToggle={() => {
-                // Trigger re-render to show/hide children
-                setRebuildTrigger((prev) => prev + 1);
-              }}
+              onToggle={triggerTreeRebuild}
             />
           </div>
         );

+ 31 - 0
apps/app/src/features/page-tree/client/states/tree-rebuild.ts

@@ -0,0 +1,31 @@
+import { atom, useAtomValue, useSetAtom } from 'jotai';
+
+/**
+ * Atom to track when the tree needs to be rebuilt.
+ * Incrementing this value will trigger a re-render in components that subscribe to it.
+ *
+ * This is useful for triggering tree rebuilds after operations that change the tree structure,
+ * such as:
+ * - Creating a new page (placeholder node added)
+ * - Renaming a page
+ * - Expanding/collapsing items with async data loading
+ */
+const treeRebuildTriggerAtom = atom(0);
+
+/**
+ * Hook to get the current rebuild trigger value.
+ * Components using this hook will re-render when the trigger changes.
+ */
+export const useTreeRebuildTrigger = (): number => {
+  return useAtomValue(treeRebuildTriggerAtom);
+};
+
+/**
+ * Hook to get a function that triggers a tree rebuild.
+ * The returned function is stable and can be passed to callbacks without causing re-renders.
+ */
+export const useTriggerTreeRebuild = (): (() => void) => {
+  const setTrigger = useSetAtom(treeRebuildTriggerAtom);
+  // Note: useSetAtom returns a stable function reference
+  return () => setTrigger((prev) => prev + 1);
+};