mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
fix: Improve performance when editing titles in large open document trees (#11858)
This commit is contained in:
@@ -265,27 +265,30 @@ function InnerDocumentLink(
|
||||
};
|
||||
});
|
||||
|
||||
const nodeChildren = React.useMemo(() => {
|
||||
const insertDraftDocument =
|
||||
activeDocument?.isDraft &&
|
||||
activeDocument?.isActive &&
|
||||
activeDocument?.parentDocumentId === node.id;
|
||||
const insertDraftChild = !!(
|
||||
activeDocument?.isDraft &&
|
||||
activeDocument?.isActive &&
|
||||
activeDocument?.parentDocumentId === node.id
|
||||
);
|
||||
|
||||
return collection && insertDraftDocument
|
||||
? sortNavigationNodes(
|
||||
[activeDocument?.asNavigationNode, ...node.children],
|
||||
collection.sort,
|
||||
false
|
||||
)
|
||||
: node.children;
|
||||
}, [
|
||||
activeDocument?.isActive,
|
||||
activeDocument?.isDraft,
|
||||
activeDocument?.parentDocumentId,
|
||||
activeDocument?.asNavigationNode,
|
||||
collection,
|
||||
node,
|
||||
]);
|
||||
// Only subscribe to asNavigationNode when this node is the parent of an
|
||||
// active draft. This avoids every DocumentLink observer re-rendering on
|
||||
// every title keystroke.
|
||||
const draftNavNode = insertDraftChild
|
||||
? activeDocument?.asNavigationNode
|
||||
: undefined;
|
||||
|
||||
const nodeChildren = React.useMemo(
|
||||
() =>
|
||||
collection && draftNavNode
|
||||
? sortNavigationNodes(
|
||||
[draftNavNode, ...node.children],
|
||||
collection.sort,
|
||||
false
|
||||
)
|
||||
: node.children,
|
||||
[draftNavNode, collection, node]
|
||||
);
|
||||
|
||||
const doc = documents.get(node.id);
|
||||
const title = doc?.title || node.title || t("Untitled");
|
||||
|
||||
@@ -7,38 +7,32 @@ export default function useCollectionDocuments(
|
||||
collection: Collection | undefined,
|
||||
activeDocument: Document | undefined
|
||||
) {
|
||||
const insertDraftDocument = useMemo(
|
||||
() =>
|
||||
activeDocument &&
|
||||
activeDocument.isActive &&
|
||||
activeDocument.isDraft &&
|
||||
activeDocument.collectionId === collection?.id &&
|
||||
!activeDocument.parentDocumentId,
|
||||
[
|
||||
activeDocument?.isActive,
|
||||
activeDocument?.isDraft,
|
||||
activeDocument?.collectionId,
|
||||
activeDocument?.parentDocumentId,
|
||||
collection?.id,
|
||||
]
|
||||
const insertDraftDocument = !!(
|
||||
activeDocument &&
|
||||
activeDocument.isActive &&
|
||||
activeDocument.isDraft &&
|
||||
activeDocument.collectionId === collection?.id &&
|
||||
!activeDocument.parentDocumentId
|
||||
);
|
||||
|
||||
// Only subscribe to asNavigationNode when we actually need to insert a draft
|
||||
// into the sorted list. This avoids every CollectionLinkChildren observer
|
||||
// re-rendering on every title keystroke.
|
||||
const draftNavNode = insertDraftDocument
|
||||
? activeDocument?.asNavigationNode
|
||||
: undefined;
|
||||
|
||||
return useMemo(() => {
|
||||
if (!collection?.sortedDocuments) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return insertDraftDocument && activeDocument
|
||||
return draftNavNode
|
||||
? sortNavigationNodes(
|
||||
[activeDocument.asNavigationNode, ...collection.sortedDocuments],
|
||||
[draftNavNode, ...collection.sortedDocuments],
|
||||
collection.sort,
|
||||
false
|
||||
)
|
||||
: collection.sortedDocuments;
|
||||
}, [
|
||||
insertDraftDocument,
|
||||
activeDocument?.asNavigationNode,
|
||||
collection?.sortedDocuments,
|
||||
collection?.sort,
|
||||
]);
|
||||
}, [draftNavNode, collection?.sortedDocuments, collection?.sort]);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { NavigationNode } from "../types";
|
||||
import shallowEqual from "./shallowEqual";
|
||||
import naturalSort from "./naturalSort";
|
||||
|
||||
type Sort = {
|
||||
@@ -21,12 +22,23 @@ export const sortNavigationNodes = (
|
||||
direction: sort.direction,
|
||||
});
|
||||
|
||||
return orderedDocs.map((node) => ({
|
||||
...node,
|
||||
children: sortChildren
|
||||
? sortNavigationNodes(node.children, sort, sortChildren)
|
||||
: node.children,
|
||||
}));
|
||||
if (!sortChildren) {
|
||||
return orderedDocs;
|
||||
}
|
||||
|
||||
return orderedDocs.map((node) => {
|
||||
const sortedChildren = sortNavigationNodes(
|
||||
node.children,
|
||||
sort,
|
||||
sortChildren
|
||||
);
|
||||
// Preserve the original node reference if children order didn't change.
|
||||
// This allows React.memo to skip re-renders of unchanged tree nodes.
|
||||
if (shallowEqual(sortedChildren, node.children)) {
|
||||
return node;
|
||||
}
|
||||
return { ...node, children: sortedChildren };
|
||||
});
|
||||
};
|
||||
|
||||
export const colorPalette = [
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Check if two arrays have the same elements in the same order by reference.
|
||||
* Uses strict equality (===) rather than deep comparison, so object identity
|
||||
* is preserved — important for React.memo optimizations.
|
||||
*
|
||||
* @param a first array.
|
||||
* @param b second array.
|
||||
* @returns true if the arrays are shallowly equal.
|
||||
*/
|
||||
export default function shallowEqual<T>(a: T[], b: T[]): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user