-
-
-
{t("labels.libraryLoadingMessage")}
+ if (
+ libraryItemsData.status === "loading" &&
+ !libraryItemsData.isInitialized
+ ) {
+ return (
+
+
+
+
+ {t("labels.libraryLoadingMessage")}
+
-
-
- );
- }
+
+ );
+ }
- const showBtn =
- libraryItemsData.libraryItems.length > 0 || pendingElements.length > 0;
+ const showBtn =
+ libraryItemsData.libraryItems.length > 0 || pendingElements.length > 0;
- return (
-
-
- {showBtn && (
-
+
- )}
-
- );
-};
+ {showBtn && (
+
+ )}
+
+ );
+ },
+);
+
+const getPendingElements = (
+ elements: readonly NonDeletedExcalidrawElement[],
+ selectedElementIds: UIAppState["selectedElementIds"],
+) => ({
+ elements,
+ pending: getSelectedElements(
+ elements,
+ { selectedElementIds },
+ {
+ includeBoundTextElement: true,
+ includeElementsInFrames: true,
+ },
+ ),
+ selectedElementIds,
+});
const usePendingElementsMemo = (
appState: UIAppState,
- elements: readonly NonDeletedExcalidrawElement[],
+ app: AppClassProperties,
) => {
- const create = useCallback(
- (appState: UIAppState, elements: readonly NonDeletedExcalidrawElement[]) =>
- getSelectedElements(elements, appState, {
- includeBoundTextElement: true,
- includeElementsInFrames: true,
- }),
- [],
+ const elements = useExcalidrawElements();
+ const [state, setState] = useState(() =>
+ getPendingElements(elements, appState.selectedElementIds),
+ );
+
+ const selectedElementVersions = useRef(
+ new Map
(),
);
- const val = useRef(create(appState, elements));
- const prevAppState = useRef(appState);
- const prevElements = useRef(elements);
+ useEffect(() => {
+ for (const element of state.pending) {
+ selectedElementVersions.current.set(element.id, element.version);
+ }
+ }, [state.pending]);
- const update = useCallback(() => {
+ useEffect(() => {
if (
- !isShallowEqual(
- appState.selectedElementIds,
- prevAppState.current.selectedElementIds,
- ) ||
- !isShallowEqual(elements, prevElements.current)
+ // Only update once pointer is released.
+ // Reading directly from app.state to make it clear it's not reactive
+ // (hence, there's potential for stale state)
+ app.state.cursorButton === "up" &&
+ app.state.activeTool.type === "selection"
) {
- val.current = create(appState, elements);
- prevAppState.current = appState;
- prevElements.current = elements;
+ setState((prev) => {
+ // if selectedElementIds changed, we don't have to compare versions
+ // ---------------------------------------------------------------------
+ if (
+ !isShallowEqual(prev.selectedElementIds, appState.selectedElementIds)
+ ) {
+ selectedElementVersions.current.clear();
+ return getPendingElements(elements, appState.selectedElementIds);
+ }
+ // otherwise we need to check whether selected elements changed
+ // ---------------------------------------------------------------------
+ const elementsMap = app.scene.getNonDeletedElementsMap();
+ for (const id of Object.keys(appState.selectedElementIds)) {
+ const currVersion = elementsMap.get(id)?.version;
+ if (
+ currVersion &&
+ currVersion !== selectedElementVersions.current.get(id)
+ ) {
+ // we can't update the selectedElementVersions in here
+ // because of double render in StrictMode which would overwrite
+ // the state in the second pass with the old `prev` state.
+ // Thus, we update versions in a separate effect. May create
+ // a race condition since current effect is not fully reactive.
+ return getPendingElements(elements, appState.selectedElementIds);
+ }
+ }
+ // nothing changed
+ // ---------------------------------------------------------------------
+ return prev;
+ });
}
- }, [create, appState, elements]);
-
- return useMemo(
- () => ({
- update,
- value: val.current,
- }),
- [update, val],
- );
+ }, [
+ app,
+ app.state.cursorButton,
+ app.state.activeTool.type,
+ appState.selectedElementIds,
+ elements,
+ ]);
+
+ return state.pending;
};
/**
* This component is meant to be rendered inside inside our
* or host apps Sidebar components.
*/
-export const LibraryMenu = () => {
- const { library, id, onInsertElements } = useApp();
+export const LibraryMenu = memo(() => {
+ const app = useApp();
+ const { onInsertElements } = app;
const appProps = useAppProps();
const appState = useUIAppState();
- const app = useApp();
const setAppState = useExcalidrawSetAppState();
- const elements = useExcalidrawElements();
const [selectedItems, setSelectedItems] = useState([]);
- const memoizedLibrary = useMemo(() => library, [library]);
- // BUG: pendingElements are still causing some unnecessary rerenders because clicking into canvas returns some ids even when no element is selected.
- const pendingElements = usePendingElementsMemo(appState, elements);
+ const memoizedLibrary = useMemo(() => app.library, [app.library]);
+ const pendingElements = usePendingElementsMemo(appState, app);
const onInsertLibraryItems = useCallback(
(libraryItems: LibraryItems) => {
@@ -223,22 +273,18 @@ export const LibraryMenu = () => {
});
}, [setAppState]);
- useEffect(() => {
- return app.onPointerUpEmitter.on(() => pendingElements.update());
- }, [app, pendingElements]);
-
return (
);
-};
+});