Skip to content

Commit 04bd333

Browse files
authored
feat: lazy load of the yaml library components (#1556)
## Description Closes #1306 Contributes to Shopify/oasis-frontend#249 Removed the `populateComponentRefs` function call when setting the component library state. Also removed the `storeComponentsFromLibrary` function and its associated logic that was storing individual components in local storage. ## Related Issue and Pull requests ## Type of Change - [ ] Bug fix - [ ] New feature - [x] Improvement - [x] Cleanup/Refactor - [ ] Breaking change - [ ] Documentation update ## Checklist - [ ] I have tested this does not break current pipelines / runs functionality - [ ] I have tested the changes on staging ## Screenshots (if applicable) ## Test Instructions [Screen Recording 2025-12-15 at 4.42.24 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.com/user-attachments/thumbnails/56c6fa34-3963-4dc9-828f-3a761dee6e2e.mov" />](https://app.graphite.com/user-attachments/video/56c6fa34-3963-4dc9-828f-3a761dee6e2e.mov) 1. Open Tangle in "Incokgnito" browser mode. Ensure no lib is available in Application / IndexedDB / components. 2. Create "New Pipeline". 3. Ensure, that Component Library is available almost instantly. 4. Ensure, that Components can be dropped on canvas. 5. Ensure, that you can open "Info" dialog from left panel. In the video I manually removed "hydration" info (name, author, digest) from the "Quick start" folder to ensure, that Left panel can display "not loaded" libraries. ## Additional Comments This change simplifies the component library loading process by removing unnecessary processing steps.
1 parent 2d10327 commit 04bd333

File tree

4 files changed

+114
-56
lines changed

4 files changed

+114
-56
lines changed

src/components/shared/FavoriteComponentToggle.tsx

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
1+
import {
2+
useMutation,
3+
useQueryClient,
4+
useSuspenseQuery,
5+
} from "@tanstack/react-query";
16
import { PackagePlus, Star } from "lucide-react";
2-
import type { MouseEvent, PropsWithChildren } from "react";
7+
import type { ComponentProps, MouseEvent, PropsWithChildren } from "react";
38
import { useCallback, useMemo, useState } from "react";
49

510
import { ConfirmationDialog } from "@/components/shared/Dialogs";
611
import { Button } from "@/components/ui/button";
712
import { Icon } from "@/components/ui/icon";
13+
import { Spinner } from "@/components/ui/spinner";
14+
import { useGuaranteedHydrateComponentReference } from "@/hooks/useHydrateComponentReference";
815
import { cn } from "@/lib/utils";
916
import { useComponentLibrary } from "@/providers/ComponentLibraryProvider";
17+
import { isFavoriteComponent } from "@/providers/ComponentLibraryProvider/componentLibrary";
1018
import { hydrateComponentReference } from "@/services/componentService";
1119
import type { ComponentReference } from "@/utils/componentSpec";
1220
import { getComponentName } from "@/utils/getComponentName";
1321

22+
import { withSuspenseWrapper } from "./SuspenseWrapper";
23+
1424
interface ComponentFavoriteToggleProps {
1525
component: ComponentReference;
1626
hideDelete?: boolean;
1727
}
1828

19-
interface StateButtonProps {
29+
interface StateButtonProps extends ComponentProps<typeof Button> {
2030
active?: boolean;
2131
isDanger?: boolean;
2232
onClick?: () => void;
@@ -27,6 +37,7 @@ const IconStateButton = ({
2737
isDanger = false,
2838
onClick,
2939
children,
40+
...props
3041
}: PropsWithChildren<StateButtonProps>) => {
3142
const handleFavorite = useCallback(
3243
(e: MouseEvent) => {
@@ -51,6 +62,7 @@ const IconStateButton = ({
5162
)}
5263
variant="ghost"
5364
size="icon"
65+
{...props}
5466
>
5567
{children}
5668
</Button>
@@ -84,28 +96,57 @@ const DeleteFromLibraryButton = ({ active, onClick }: StateButtonProps) => {
8496
);
8597
};
8698

99+
const favoriteComponentKey = (component: ComponentReference) => {
100+
return ["component", "is-favorite", component.digest];
101+
};
102+
103+
const FavoriteToggleButton = withSuspenseWrapper(
104+
({ component }: { component: ComponentReference }) => {
105+
const queryClient = useQueryClient();
106+
107+
const { setComponentFavorite } = useComponentLibrary();
108+
const hydratedComponent = useGuaranteedHydrateComponentReference(component);
109+
110+
const { data: isFavorited } = useSuspenseQuery({
111+
queryKey: favoriteComponentKey(hydratedComponent),
112+
queryFn: async () => isFavoriteComponent(hydratedComponent),
113+
});
114+
115+
const { mutate: setFavorite } = useMutation({
116+
mutationFn: async () =>
117+
setComponentFavorite(hydratedComponent, !isFavorited),
118+
onSuccess: () => {
119+
queryClient.invalidateQueries({
120+
queryKey: favoriteComponentKey(hydratedComponent),
121+
});
122+
},
123+
});
124+
125+
return <FavoriteStarButton active={isFavorited} onClick={setFavorite} />;
126+
},
127+
() => <Spinner size={10} />,
128+
() => (
129+
<IconStateButton disabled>
130+
<Icon name="Star" />
131+
</IconStateButton>
132+
),
133+
);
134+
87135
export const ComponentFavoriteToggle = ({
88136
component,
89137
hideDelete = false,
90138
}: ComponentFavoriteToggleProps) => {
91139
const {
92140
addToComponentLibrary,
93141
removeFromComponentLibrary,
94-
checkIfFavorited,
95142
checkIfUserComponent,
96143
checkLibraryContainsComponent,
97-
setComponentFavorite,
98144
} = useComponentLibrary();
99145

100146
const [isOpen, setIsOpen] = useState(false);
101147

102148
const { spec, url } = component;
103149

104-
const isFavorited = useMemo(
105-
() => checkIfFavorited(component),
106-
[component, checkIfFavorited],
107-
);
108-
109150
const isUserComponent = useMemo(
110151
() => checkIfUserComponent(component),
111152
[component, checkIfUserComponent],
@@ -121,10 +162,6 @@ export const ComponentFavoriteToggle = ({
121162
[spec, url],
122163
);
123164

124-
const onFavorite = useCallback(() => {
125-
setComponentFavorite(component, !isFavorited);
126-
}, [isFavorited, setComponentFavorite]);
127-
128165
// Delete User Components
129166
const handleDelete = useCallback(async () => {
130167
removeFromComponentLibrary(component);
@@ -166,7 +203,7 @@ export const ComponentFavoriteToggle = ({
166203
{!isInLibrary && <AddToLibraryButton onClick={openConfirmationDialog} />}
167204

168205
{isInLibrary && !isUserComponent && (
169-
<FavoriteStarButton active={isFavorited} onClick={onFavorite} />
206+
<FavoriteToggleButton component={component} />
170207
)}
171208

172209
{showDeleteButton && (

src/providers/ComponentLibraryProvider/ComponentLibraryProvider.tsx

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
updateComponentInListByText,
3232
updateComponentRefInList,
3333
} from "@/utils/componentStore";
34-
import { USER_COMPONENTS_LIST_NAME } from "@/utils/constants";
34+
import { MINUTES, USER_COMPONENTS_LIST_NAME } from "@/utils/constants";
3535
import { createPromiseFromDomEvent } from "@/utils/dom";
3636
import { getComponentName } from "@/utils/getComponentName";
3737
import {
@@ -48,11 +48,11 @@ import {
4848
} from "../../hooks/useRequiredContext";
4949
import { useComponentSpec } from "../ComponentSpecProvider";
5050
import {
51-
fetchFavoriteComponents,
5251
fetchUsedComponents,
5352
fetchUserComponents,
5453
filterToUniqueByDigest,
5554
flattenFolders,
55+
isFavoriteComponent,
5656
populateComponentRefs,
5757
} from "./componentLibrary";
5858
import { useForcedSearchContext } from "./ForcedSearchProvider";
@@ -70,7 +70,7 @@ type ComponentLibraryContextType = {
7070
componentLibrary: ComponentLibrary | undefined;
7171
userComponentsFolder: ComponentFolder | undefined;
7272
usedComponentsFolder: ComponentFolder;
73-
favoritesFolder: ComponentFolder;
73+
favoritesFolder: ComponentFolder | undefined;
7474
isLoading: boolean;
7575
error: Error | null;
7676
existingComponentLibraries: StoredLibrary[] | undefined;
@@ -93,6 +93,9 @@ type ComponentLibraryContextType = {
9393
component: ComponentReference,
9494
favorited: boolean,
9595
) => void;
96+
/**
97+
* @deprecated
98+
*/
9699
checkIfFavorited: (component: ComponentReference) => boolean;
97100
checkIfUserComponent: (component: ComponentReference) => boolean;
98101
checkLibraryContainsComponent: (component: ComponentReference) => boolean;
@@ -220,21 +223,56 @@ export const ComponentLibraryProvider = ({
220223
);
221224

222225
// Fetch "Starred" components
223-
const favoritesFolder: ComponentFolder = useMemo(
224-
() => fetchFavoriteComponents(componentLibrary),
225-
[componentLibrary],
226+
const { data: favoritesFolderData, refetch: refetchFavorites } = useQuery({
227+
queryKey: ["favorites"],
228+
queryFn: async () => {
229+
const favoritesFolder: ComponentFolder = {
230+
name: "Favorite Components",
231+
components: [],
232+
folders: [],
233+
isUserFolder: false,
234+
};
235+
236+
if (!componentLibrary || !componentLibrary.folders) {
237+
return favoritesFolder;
238+
}
239+
240+
const uniqueLibraryComponents = filterToUniqueByDigest(
241+
flattenFolders(componentLibrary),
242+
);
243+
244+
for (const component of uniqueLibraryComponents) {
245+
if (await isFavoriteComponent(component)) {
246+
favoritesFolder.components?.push(component);
247+
}
248+
}
249+
250+
return favoritesFolder;
251+
},
252+
enabled: Boolean(componentLibrary),
253+
staleTime: 10 * MINUTES,
254+
});
255+
256+
const favoritesFolder = useMemo(
257+
() =>
258+
favoritesFolderData ?? {
259+
name: "Favorite Components",
260+
components: [],
261+
folders: [],
262+
isUserFolder: false,
263+
},
264+
[favoritesFolderData],
226265
);
227266

228267
// Methods
229268
const refreshComponentLibrary = useCallback(async () => {
230269
const { data: updatedLibrary } = await refetchLibrary();
231270

232271
if (updatedLibrary) {
233-
populateComponentRefs(updatedLibrary).then((result) => {
234-
setComponentLibrary(result);
235-
});
272+
setComponentLibrary(updatedLibrary);
273+
await refetchFavorites();
236274
}
237-
}, [refetchLibrary]);
275+
}, [refetchLibrary, refetchFavorites]);
238276

239277
const refreshUserComponents = useCallback(async () => {
240278
const { data: updatedUserComponents } = await refetchUserComponents();
@@ -571,9 +609,7 @@ export const ComponentLibraryProvider = ({
571609
setComponentLibrary(undefined);
572610
return;
573611
}
574-
populateComponentRefs(rawComponentLibrary).then((result) => {
575-
setComponentLibrary(result);
576-
});
612+
setComponentLibrary(rawComponentLibrary);
577613
}, [rawComponentLibrary]);
578614

579615
useEffect(() => {

src/providers/ComponentLibraryProvider/componentLibrary.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
getAllComponentFilesFromList,
1414
} from "@/utils/componentStore";
1515
import { USER_COMPONENTS_LIST_NAME } from "@/utils/constants";
16-
import { getComponentByUrl } from "@/utils/localforage";
16+
import { getComponentById, getComponentByUrl } from "@/utils/localforage";
1717
import { componentSpecToYaml } from "@/utils/yaml";
1818

1919
export const fetchUserComponents = async (): Promise<ComponentFolder> => {
@@ -80,6 +80,19 @@ export const fetchUsedComponents = (graphSpec: GraphSpec): ComponentFolder => {
8080
};
8181
};
8282

83+
export async function isFavoriteComponent(component: ComponentReference) {
84+
if (!component.digest) return false;
85+
86+
const storedComponent = await getComponentById(
87+
`component-${component.digest}`,
88+
);
89+
90+
return storedComponent?.favorited ?? false;
91+
}
92+
93+
/**
94+
* @deprecated
95+
*/
8396
export const fetchFavoriteComponents = (
8497
componentLibrary: ComponentLibrary | undefined,
8598
): ComponentFolder => {

src/services/componentService.ts

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { getAppSettings } from "@/appSettings";
22
import {
3-
type ComponentFolder,
43
type ComponentLibrary,
54
isValidComponentLibrary,
65
} from "@/types/componentLibrary";
@@ -112,36 +111,9 @@ export const fetchAndStoreComponentLibrary =
112111
updatedAt: Date.now(),
113112
});
114113

115-
// Also store individual components for future reference
116-
await storeComponentsFromLibrary(obj);
117-
118114
return obj;
119115
};
120116

121-
/**
122-
* Store all components from the library in local storage
123-
*/
124-
const storeComponentsFromLibrary = async (
125-
library: ComponentLibrary,
126-
): Promise<void> => {
127-
const processFolder = async (folder: ComponentFolder) => {
128-
// Store each component in the folder
129-
for (const component of folder.components || []) {
130-
await fetchAndStoreComponent(component);
131-
}
132-
133-
// Process subfolders recursively
134-
for (const subfolder of folder.folders || []) {
135-
await processFolder(subfolder);
136-
}
137-
};
138-
139-
// Process all top-level folders
140-
for (const folder of library.folders) {
141-
await processFolder(folder);
142-
}
143-
};
144-
145117
/**
146118
* Fetch and store a single component by URL
147119
*/

0 commit comments

Comments
 (0)