perf: memoize rendering of library (#6622)

Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Arnost Pleskot 2023-05-31 15:37:13 +02:00 committed by GitHub
parent 82d8d02697
commit 253c5c7866
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 377 additions and 306 deletions

View File

@ -1,4 +1,4 @@
import React, { useCallback } from "react"; import React, { useState, useCallback, useMemo, useRef } from "react";
import Library, { import Library, {
distributeLibraryItemsOnSquareGrid, distributeLibraryItemsOnSquareGrid,
libraryItemsAtom, libraryItemsAtom,
@ -27,6 +27,8 @@ import { useUIAppState } from "../context/ui-appState";
import "./LibraryMenu.scss"; import "./LibraryMenu.scss";
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons"; import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
import { isShallowEqual } from "../utils";
import { NonDeletedExcalidrawElement } from "../element/types";
export const isLibraryMenuOpenAtom = atom(false); export const isLibraryMenuOpenAtom = atom(false);
@ -42,7 +44,9 @@ export const LibraryMenuContent = ({
libraryReturnUrl, libraryReturnUrl,
library, library,
id, id,
appState, theme,
selectedItems,
onSelectItems,
}: { }: {
pendingElements: LibraryItem["elements"]; pendingElements: LibraryItem["elements"];
onInsertLibraryItems: (libraryItems: LibraryItems) => void; onInsertLibraryItems: (libraryItems: LibraryItems) => void;
@ -51,22 +55,29 @@ export const LibraryMenuContent = ({
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
library: Library; library: Library;
id: string; id: string;
appState: UIAppState; theme: UIAppState["theme"];
selectedItems: LibraryItem["id"][];
onSelectItems: (id: LibraryItem["id"][]) => void;
}) => { }) => {
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope); const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
const addToLibrary = useCallback( const _onAddToLibrary = useCallback(
async (elements: LibraryItem["elements"], libraryItems: LibraryItems) => { (elements: LibraryItem["elements"]) => {
const addToLibrary = async (
processedElements: LibraryItem["elements"],
libraryItems: LibraryItems,
) => {
trackEvent("element", "addToLibrary", "ui"); trackEvent("element", "addToLibrary", "ui");
if (elements.some((element) => element.type === "image")) { if (processedElements.some((element) => element.type === "image")) {
return setAppState({ return setAppState({
errorMessage: "Support for adding images to the library coming soon!", errorMessage:
"Support for adding images to the library coming soon!",
}); });
} }
const nextItems: LibraryItems = [ const nextItems: LibraryItems = [
{ {
status: "unpublished", status: "unpublished",
elements, elements: processedElements,
id: randomId(), id: randomId(),
created: Date.now(), created: Date.now(),
}, },
@ -76,8 +87,15 @@ export const LibraryMenuContent = ({
library.setLibrary(nextItems).catch(() => { library.setLibrary(nextItems).catch(() => {
setAppState({ errorMessage: t("alerts.errorAddingToLibrary") }); setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
}); });
};
addToLibrary(elements, libraryItemsData.libraryItems);
}, },
[onAddToLibrary, library, setAppState], [onAddToLibrary, library, setAppState, libraryItemsData.libraryItems],
);
const libraryItems = useMemo(
() => libraryItemsData.libraryItems,
[libraryItemsData],
); );
if ( if (
@ -103,15 +121,15 @@ export const LibraryMenuContent = ({
<LibraryMenuWrapper> <LibraryMenuWrapper>
<LibraryMenuItems <LibraryMenuItems
isLoading={libraryItemsData.status === "loading"} isLoading={libraryItemsData.status === "loading"}
libraryItems={libraryItemsData.libraryItems} libraryItems={libraryItems}
onAddToLibrary={(elements) => onAddToLibrary={_onAddToLibrary}
addToLibrary(elements, libraryItemsData.libraryItems)
}
onInsertLibraryItems={onInsertLibraryItems} onInsertLibraryItems={onInsertLibraryItems}
pendingElements={pendingElements} pendingElements={pendingElements}
id={id} id={id}
libraryReturnUrl={libraryReturnUrl} libraryReturnUrl={libraryReturnUrl}
theme={appState.theme} theme={theme}
onSelectItems={onSelectItems}
selectedItems={selectedItems}
/> />
{showBtn && ( {showBtn && (
<LibraryMenuControlButtons <LibraryMenuControlButtons
@ -119,13 +137,36 @@ export const LibraryMenuContent = ({
style={{ padding: "16px 12px 0 12px" }} style={{ padding: "16px 12px 0 12px" }}
id={id} id={id}
libraryReturnUrl={libraryReturnUrl} libraryReturnUrl={libraryReturnUrl}
theme={appState.theme} theme={theme}
/> />
)} )}
</LibraryMenuWrapper> </LibraryMenuWrapper>
); );
}; };
const usePendingElementsMemo = (
appState: UIAppState,
elements: readonly NonDeletedExcalidrawElement[],
) => {
const create = () => getSelectedElements(elements, appState, true);
const val = useRef(create());
const prevAppState = useRef<UIAppState>(appState);
const prevElements = useRef(elements);
if (
!isShallowEqual(
appState.selectedElementIds,
prevAppState.current.selectedElementIds,
) ||
!isShallowEqual(elements, prevElements.current)
) {
val.current = create();
prevAppState.current = appState;
prevElements.current = elements;
}
return val.current;
};
/** /**
* This component is meant to be rendered inside <Sidebar.Tab/> inside our * This component is meant to be rendered inside <Sidebar.Tab/> inside our
* <DefaultSidebar/> or host apps Sidebar components. * <DefaultSidebar/> or host apps Sidebar components.
@ -136,9 +177,19 @@ export const LibraryMenu = () => {
const appState = useUIAppState(); const appState = useUIAppState();
const setAppState = useExcalidrawSetAppState(); const setAppState = useExcalidrawSetAppState();
const elements = useExcalidrawElements(); const elements = useExcalidrawElements();
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
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 onAddToLibrary = useCallback(() => { const onInsertLibraryItems = useCallback(
// deselect canvas elements (libraryItems: LibraryItems) => {
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
},
[onInsertElements],
);
const deselectItems = useCallback(() => {
setAppState({ setAppState({
selectedElementIds: {}, selectedElementIds: {},
selectedGroupIds: {}, selectedGroupIds: {},
@ -147,16 +198,16 @@ export const LibraryMenu = () => {
return ( return (
<LibraryMenuContent <LibraryMenuContent
pendingElements={getSelectedElements(elements, appState, true)} pendingElements={pendingElements}
onInsertLibraryItems={(libraryItems) => { onInsertLibraryItems={onInsertLibraryItems}
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems)); onAddToLibrary={deselectItems}
}}
onAddToLibrary={onAddToLibrary}
setAppState={setAppState} setAppState={setAppState}
libraryReturnUrl={appProps.libraryReturnUrl} libraryReturnUrl={appProps.libraryReturnUrl}
library={library} library={memoizedLibrary}
id={id} id={id}
appState={appState} theme={appState.theme}
selectedItems={selectedItems}
onSelectItems={setSelectedItems}
/> />
); );
}; };

View File

@ -73,6 +73,12 @@
} }
} }
&__grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-gap: 1rem;
}
.separator { .separator {
width: 100%; width: 100%;
display: flex; display: flex;

View File

@ -1,4 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { serializeLibraryAsJSON } from "../data/json"; import { serializeLibraryAsJSON } from "../data/json";
import { t } from "../i18n"; import { t } from "../i18n";
import { import {
@ -14,12 +20,22 @@ import Spinner from "./Spinner";
import { duplicateElements } from "../element/newElement"; import { duplicateElements } from "../element/newElement";
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons"; import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
import { LibraryDropdownMenu } from "./LibraryMenuHeaderContent"; import { LibraryDropdownMenu } from "./LibraryMenuHeaderContent";
import LibraryMenuSection from "./LibraryMenuSection"; import {
LibraryMenuSection,
LibraryMenuSectionGrid,
} from "./LibraryMenuSection";
import { useScrollPosition } from "../hooks/useScrollPosition"; import { useScrollPosition } from "../hooks/useScrollPosition";
import { useLibraryCache } from "../hooks/useLibraryItemSvg"; import { useLibraryCache } from "../hooks/useLibraryItemSvg";
import "./LibraryMenuItems.scss"; import "./LibraryMenuItems.scss";
// using an odd number of items per batch so the rendering creates an irregular
// pattern which looks more organic
const ITEMS_RENDERED_PER_BATCH = 17;
// when render outputs cached we can render many more items per batch to
// speed it up
const CACHED_ITEMS_RENDERED_PER_BATCH = 64;
export default function LibraryMenuItems({ export default function LibraryMenuItems({
isLoading, isLoading,
libraryItems, libraryItems,
@ -29,6 +45,8 @@ export default function LibraryMenuItems({
theme, theme,
id, id,
libraryReturnUrl, libraryReturnUrl,
onSelectItems,
selectedItems,
}: { }: {
isLoading: boolean; isLoading: boolean;
libraryItems: LibraryItems; libraryItems: LibraryItems;
@ -38,8 +56,9 @@ export default function LibraryMenuItems({
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
theme: UIAppState["theme"]; theme: UIAppState["theme"];
id: string; id: string;
selectedItems: LibraryItem["id"][];
onSelectItems: (id: LibraryItem["id"][]) => void;
}) { }) {
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
const libraryContainerRef = useRef<HTMLDivElement>(null); const libraryContainerRef = useRef<HTMLDivElement>(null);
const scrollPosition = useScrollPosition<HTMLDivElement>(libraryContainerRef); const scrollPosition = useScrollPosition<HTMLDivElement>(libraryContainerRef);
@ -49,13 +68,16 @@ export default function LibraryMenuItems({
libraryContainerRef.current?.scrollTo(0, scrollPosition); libraryContainerRef.current?.scrollTo(0, scrollPosition);
} }
}, []); // eslint-disable-line react-hooks/exhaustive-deps }, []); // eslint-disable-line react-hooks/exhaustive-deps
const { svgCache } = useLibraryCache();
const unpublishedItems = libraryItems.filter( const { svgCache } = useLibraryCache();
(item) => item.status !== "published", const unpublishedItems = useMemo(
() => libraryItems.filter((item) => item.status !== "published"),
[libraryItems],
); );
const publishedItems = libraryItems.filter(
(item) => item.status === "published", const publishedItems = useMemo(
() => libraryItems.filter((item) => item.status === "published"),
[libraryItems],
); );
const showBtn = !libraryItems.length && !pendingElements.length; const showBtn = !libraryItems.length && !pendingElements.length;
@ -69,10 +91,8 @@ export default function LibraryMenuItems({
LibraryItem["id"] | null LibraryItem["id"] | null
>(null); >(null);
const onItemSelectToggle = ( const onItemSelectToggle = useCallback(
id: LibraryItem["id"], (id: LibraryItem["id"], event: React.MouseEvent) => {
event: React.MouseEvent,
) => {
const shouldSelect = !selectedItems.includes(id); const shouldSelect = !selectedItems.includes(id);
const orderedItems = [...unpublishedItems, ...publishedItems]; const orderedItems = [...unpublishedItems, ...publishedItems];
@ -85,7 +105,7 @@ export default function LibraryMenuItems({
const rangeEnd = orderedItems.findIndex((item) => item.id === id); const rangeEnd = orderedItems.findIndex((item) => item.id === id);
if (rangeStart === -1 || rangeEnd === -1) { if (rangeStart === -1 || rangeEnd === -1) {
setSelectedItems([...selectedItems, id]); onSelectItems([...selectedItems, id]);
return; return;
} }
@ -103,16 +123,24 @@ export default function LibraryMenuItems({
[], [],
); );
setSelectedItems(nextSelectedIds); onSelectItems(nextSelectedIds);
} else { } else {
setSelectedItems([...selectedItems, id]); onSelectItems([...selectedItems, id]);
} }
setLastSelectedItem(id); setLastSelectedItem(id);
} else { } else {
setLastSelectedItem(null); setLastSelectedItem(null);
setSelectedItems(selectedItems.filter((_id) => _id !== id)); onSelectItems(selectedItems.filter((_id) => _id !== id));
} }
}; },
[
lastSelectedItem,
onSelectItems,
publishedItems,
selectedItems,
unpublishedItems,
],
);
const getInsertedElements = useCallback( const getInsertedElements = useCallback(
(id: string) => { (id: string) => {
@ -136,37 +164,45 @@ export default function LibraryMenuItems({
[libraryItems, selectedItems], [libraryItems, selectedItems],
); );
const onItemDrag = (id: LibraryItem["id"], event: React.DragEvent) => { const onItemDrag = useCallback(
(id: LibraryItem["id"], event: React.DragEvent) => {
event.dataTransfer.setData( event.dataTransfer.setData(
MIME_TYPES.excalidrawlib, MIME_TYPES.excalidrawlib,
serializeLibraryAsJSON(getInsertedElements(id)), serializeLibraryAsJSON(getInsertedElements(id)),
); );
}; },
[getInsertedElements],
);
const isItemSelected = (id: LibraryItem["id"] | null) => { const isItemSelected = useCallback(
(id: LibraryItem["id"] | null) => {
if (!id) { if (!id) {
return false; return false;
} }
return selectedItems.includes(id); return selectedItems.includes(id);
}; },
[selectedItems],
);
const onAddToLibraryClick = useCallback(() => {
onAddToLibrary(pendingElements);
}, [pendingElements, onAddToLibrary]);
const onItemClick = useCallback( const onItemClick = useCallback(
(id: LibraryItem["id"] | null) => { (id: LibraryItem["id"] | null) => {
if (!id) { if (id) {
onAddToLibrary(pendingElements);
} else {
onInsertLibraryItems(getInsertedElements(id)); onInsertLibraryItems(getInsertedElements(id));
} }
}, },
[ [getInsertedElements, onInsertLibraryItems],
getInsertedElements,
onAddToLibrary,
onInsertLibraryItems,
pendingElements,
],
); );
const itemsRenderedPerBatch =
svgCache.size >= libraryItems.length
? CACHED_ITEMS_RENDERED_PER_BATCH
: ITEMS_RENDERED_PER_BATCH;
return ( return (
<div <div
className="library-menu-items-container" className="library-menu-items-container"
@ -181,7 +217,7 @@ export default function LibraryMenuItems({
{!isLibraryEmpty && ( {!isLibraryEmpty && (
<LibraryDropdownMenu <LibraryDropdownMenu
selectedItems={selectedItems} selectedItems={selectedItems}
onSelectItems={setSelectedItems} onSelectItems={onSelectItems}
className="library-menu-dropdown-container--in-heading" className="library-menu-dropdown-container--in-heading"
/> />
)} )}
@ -225,20 +261,28 @@ export default function LibraryMenuItems({
</div> </div>
</div> </div>
) : ( ) : (
<LibraryMenuSectionGrid>
{pendingElements.length > 0 && (
<LibraryMenuSection <LibraryMenuSection
items={[ itemsRenderedPerBatch={itemsRenderedPerBatch}
// append pending library item items={[{ id: null, elements: pendingElements }]}
...(pendingElements.length onItemSelectToggle={onItemSelectToggle}
? [{ id: null, elements: pendingElements }] onItemDrag={onItemDrag}
: []), onClick={onAddToLibraryClick}
...unpublishedItems, isItemSelected={isItemSelected}
]} svgCache={svgCache}
/>
)}
<LibraryMenuSection
itemsRenderedPerBatch={itemsRenderedPerBatch}
items={unpublishedItems}
onItemSelectToggle={onItemSelectToggle} onItemSelectToggle={onItemSelectToggle}
onItemDrag={onItemDrag} onItemDrag={onItemDrag}
onClick={onItemClick} onClick={onItemClick}
isItemSelected={isItemSelected} isItemSelected={isItemSelected}
svgCache={svgCache} svgCache={svgCache}
/> />
</LibraryMenuSectionGrid>
)} )}
</> </>
@ -251,7 +295,9 @@ export default function LibraryMenuItems({
</div> </div>
)} )}
{publishedItems.length > 0 ? ( {publishedItems.length > 0 ? (
<LibraryMenuSectionGrid>
<LibraryMenuSection <LibraryMenuSection
itemsRenderedPerBatch={itemsRenderedPerBatch}
items={publishedItems} items={publishedItems}
onItemSelectToggle={onItemSelectToggle} onItemSelectToggle={onItemSelectToggle}
onItemDrag={onItemDrag} onItemDrag={onItemDrag}
@ -259,6 +305,7 @@ export default function LibraryMenuItems({
isItemSelected={isItemSelected} isItemSelected={isItemSelected}
svgCache={svgCache} svgCache={svgCache}
/> />
</LibraryMenuSectionGrid>
) : unpublishedItems.length > 0 ? ( ) : unpublishedItems.length > 0 ? (
<div <div
style={{ style={{
@ -285,7 +332,7 @@ export default function LibraryMenuItems({
> >
<LibraryDropdownMenu <LibraryDropdownMenu
selectedItems={selectedItems} selectedItems={selectedItems}
onSelectItems={setSelectedItems} onSelectItems={onSelectItems}
/> />
</LibraryMenuControlButtons> </LibraryMenuControlButtons>
)} )}

View File

@ -1,16 +1,10 @@
import React, { useEffect, useMemo, useState } from "react"; import React, { memo, ReactNode, useEffect, useState } from "react";
import { LibraryUnit } from "./LibraryUnit"; import { EmptyLibraryUnit, LibraryUnit } from "./LibraryUnit";
import { LibraryItem } from "../types"; import { LibraryItem } from "../types";
import Stack from "./Stack";
import clsx from "clsx";
import { ExcalidrawElement, NonDeleted } from "../element/types"; import { ExcalidrawElement, NonDeleted } from "../element/types";
import { SvgCache } from "../hooks/useLibraryItemSvg"; import { SvgCache } from "../hooks/useLibraryItemSvg";
import { useTransition } from "../hooks/useTransition"; import { useTransition } from "../hooks/useTransition";
const ITEMS_PER_ROW = 4;
const ROWS_RENDERED_PER_BATCH = 6;
const CACHED_ROWS_RENDERED_PER_BATCH = 16;
type LibraryOrPendingItem = ( type LibraryOrPendingItem = (
| LibraryItem | LibraryItem
| /* pending library item */ { | /* pending library item */ {
@ -26,91 +20,58 @@ interface Props {
onItemDrag: (id: LibraryItem["id"], event: React.DragEvent) => void; onItemDrag: (id: LibraryItem["id"], event: React.DragEvent) => void;
isItemSelected: (id: LibraryItem["id"] | null) => boolean; isItemSelected: (id: LibraryItem["id"] | null) => boolean;
svgCache: SvgCache; svgCache: SvgCache;
itemsRenderedPerBatch: number;
} }
function LibraryRow({ export const LibraryMenuSectionGrid = ({
children,
}: {
children: ReactNode;
}) => {
return <div className="library-menu-items-container__grid">{children}</div>;
};
export const LibraryMenuSection = memo(
({
items, items,
onItemSelectToggle, onItemSelectToggle,
onItemDrag, onItemDrag,
isItemSelected, isItemSelected,
onClick, onClick,
svgCache, svgCache,
}: Props) { itemsRenderedPerBatch,
}: Props) => {
const [, startTransition] = useTransition();
const [index, setIndex] = useState(0);
useEffect(() => {
if (index < items.length) {
startTransition(() => {
setIndex(index + itemsRenderedPerBatch);
});
}
}, [index, items.length, startTransition, itemsRenderedPerBatch]);
return ( return (
<Stack.Row className="library-menu-items-container__row"> <>
{items.map((item) => ( {items.map((item, i) => {
<Stack.Col key={item.id}> return i < index ? (
<LibraryUnit <LibraryUnit
elements={item?.elements} elements={item?.elements}
isPending={!item?.id && !!item?.elements} isPending={!item?.id && !!item?.elements}
onClick={onClick} onClick={onClick}
id={item?.id || null} svgCache={svgCache}
id={item?.id}
selected={isItemSelected(item.id)} selected={isItemSelected(item.id)}
onToggle={onItemSelectToggle} onToggle={onItemSelectToggle}
onDrag={onItemDrag} onDrag={onItemDrag}
svgCache={svgCache} key={item?.id ?? i}
/>
</Stack.Col>
))}
</Stack.Row>
);
}
const EmptyLibraryRow = () => (
<Stack.Row className="library-menu-items-container__row" gap={1}>
{Array.from({ length: ITEMS_PER_ROW }).map((_, index) => (
<Stack.Col key={index}>
<div className={clsx("library-unit", "library-unit--skeleton")} />
</Stack.Col>
))}
</Stack.Row>
);
function LibraryMenuSection({
items,
onItemSelectToggle,
onItemDrag,
isItemSelected,
onClick,
svgCache,
}: Props) {
const rows = Math.ceil(items.length / ITEMS_PER_ROW);
const [, startTransition] = useTransition();
const [index, setIndex] = useState(0);
const rowsRenderedPerBatch = useMemo(() => {
return svgCache.size === 0
? ROWS_RENDERED_PER_BATCH
: CACHED_ROWS_RENDERED_PER_BATCH;
}, [svgCache]);
useEffect(() => {
if (index < rows) {
startTransition(() => {
setIndex(index + rowsRenderedPerBatch);
});
}
}, [index, rows, startTransition, rowsRenderedPerBatch]);
return (
<>
{Array.from({ length: rows }).map((_, i) =>
i < index ? (
<LibraryRow
key={i}
items={items.slice(i * ITEMS_PER_ROW, (i + 1) * ITEMS_PER_ROW)}
onItemSelectToggle={onItemSelectToggle}
onItemDrag={onItemDrag}
onClick={onClick}
isItemSelected={isItemSelected}
svgCache={svgCache}
/> />
) : ( ) : (
<EmptyLibraryRow key={i} /> <EmptyLibraryUnit key={i} />
), );
)} })}
</> </>
); );
} },
);
export default LibraryMenuSection;

View File

@ -30,7 +30,7 @@
var(--color-gray-10) var(--color-gray-10)
); );
background-size: 200% 200%; background-size: 200% 200%;
animation: library-unit__skeleton-opacity-animation 0.3s linear; animation: library-unit__skeleton-opacity-animation 0.2s linear;
} }
} }

View File

@ -1,5 +1,5 @@
import clsx from "clsx"; import clsx from "clsx";
import { useEffect, useRef, useState } from "react"; import { memo, useEffect, useRef, useState } from "react";
import { useDevice } from "../components/App"; import { useDevice } from "../components/App";
import { LibraryItem } from "../types"; import { LibraryItem } from "../types";
import "./LibraryUnit.scss"; import "./LibraryUnit.scss";
@ -7,7 +7,8 @@ import { CheckboxItem } from "./CheckboxItem";
import { PlusIcon } from "./icons"; import { PlusIcon } from "./icons";
import { SvgCache, useLibraryItemSvg } from "../hooks/useLibraryItemSvg"; import { SvgCache, useLibraryItemSvg } from "../hooks/useLibraryItemSvg";
export const LibraryUnit = ({ export const LibraryUnit = memo(
({
id, id,
elements, elements,
isPending, isPending,
@ -37,14 +38,13 @@ export const LibraryUnit = ({
} }
if (svg) { if (svg) {
svg.querySelector(".style-fonts")?.remove();
node.innerHTML = svg.outerHTML; node.innerHTML = svg.outerHTML;
} }
return () => { return () => {
node.innerHTML = ""; node.innerHTML = "";
}; };
}, [elements, svg]); }, [svg]);
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const isMobile = useDevice().isMobile; const isMobile = useDevice().isMobile;
@ -99,4 +99,9 @@ export const LibraryUnit = ({
)} )}
</div> </div>
); );
}; },
);
export const EmptyLibraryUnit = () => (
<div className="library-unit library-unit--skeleton" />
);

View File

@ -39,6 +39,7 @@ export const useLibraryItemSvg = (
// When there is no svg in cache export it and save to cache // When there is no svg in cache export it and save to cache
(async () => { (async () => {
const exportedSvg = await exportLibraryItemToSvg(elements); const exportedSvg = await exportLibraryItemToSvg(elements);
exportedSvg.querySelector(".style-fonts")?.remove();
if (exportedSvg) { if (exportedSvg) {
svgCache.set(id, exportedSvg); svgCache.set(id, exportedSvg);