Skip to content

Commit c21e7b9

Browse files
j-patersonclaude
andauthored
feat: show loading skeleton only for grid while space loads (#1729)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d811664 commit c21e7b9

File tree

4 files changed

+141
-75
lines changed

4 files changed

+141
-75
lines changed

src/app/(spaces)/PublicSpace.tsx

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export default function PublicSpace({
5757
registerSpaceContract,
5858
registerProposalSpace,
5959
registerChannelSpace,
60+
isTabLoading,
61+
isTabChecked,
6062
} = useAppStore((state) => ({
6163
clearLocalSpaces: state.clearLocalSpaces,
6264
getCurrentSpaceId: state.currentSpace.getCurrentSpaceId,
@@ -82,6 +84,8 @@ export default function PublicSpace({
8284
registerSpaceContract: state.space.registerSpaceContract,
8385
registerProposalSpace: state.space.registerProposalSpace,
8486
registerChannelSpace: state.space.registerChannelSpace,
87+
isTabLoading: state.space.isTabLoading,
88+
isTabChecked: state.space.isTabChecked,
8589
}));
8690

8791
const router = useRouter();
@@ -174,15 +178,42 @@ export default function PublicSpace({
174178

175179
// Config logic:
176180
// - If we have currentTabName and the tab is loaded in store, use it
177-
// - If we don't have currentSpaceId (viewing someone else's space), use default config
178-
// - Otherwise, return undefined to trigger Suspense while loading
179-
const config = currentTabName && currentConfig?.tabs?.[currentTabName] ? {
180-
...currentConfig.tabs[currentTabName],
181-
isEditable,
182-
} : {
183-
...spacePageData.config,
181+
// - If we're still loading from the database, return undefined to trigger Suspense/skeleton
182+
// - If we've checked and no data exists, use the default config
183+
const config = useMemo(() => {
184+
// If tab data is loaded in the store, use it
185+
if (currentTabName && currentConfig?.tabs?.[currentTabName]) {
186+
return {
187+
...currentConfig.tabs[currentTabName],
188+
isEditable,
189+
};
190+
}
191+
192+
// If we have a spaceId and haven't finished checking the database yet, show loading
193+
if (currentSpaceId && currentTabName) {
194+
const loading = isTabLoading(currentSpaceId, currentTabName);
195+
const checked = isTabChecked(currentSpaceId, currentTabName);
196+
197+
// Still loading - return undefined to show skeleton
198+
if (loading || !checked) {
199+
return undefined;
200+
}
201+
}
202+
203+
// Tab has been checked and no data exists - use default config
204+
return {
205+
...spacePageData.config,
206+
isEditable,
207+
};
208+
}, [
209+
currentTabName,
210+
currentConfig,
211+
currentSpaceId,
212+
isTabLoading,
213+
isTabChecked,
214+
spacePageData.config,
184215
isEditable,
185-
};
216+
]);
186217

187218
// Register the space if it doesn't exist
188219
useEffect(() => {

src/app/(spaces)/Space.tsx

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export type SpaceConfigSaveDetails = Partial<
5555
};
5656

5757
type SpaceArgs = {
58-
config: SpaceConfig;
58+
config: SpaceConfig | undefined;
5959
saveConfig: (config: SpaceConfigSaveDetails) => Promise<void>;
6060
commitConfig: () => Promise<void>;
6161
resetConfig: () => Promise<void>;
@@ -128,14 +128,17 @@ export default function Space({
128128
const isHomebasePath = usePathHelper();
129129
const { isMobile, showMobileContainer, viewportMobile, setMobilePreview } = useViewportManager();
130130
const { saveLocalConfig } = useConfigManager(saveConfig);
131-
131+
132+
// Track if config is loading
133+
const isConfigLoading = isUndefined(config);
134+
132135
// Calculate layout stuff once instead of re-computing
133136
const layoutConfig = getLayoutConfig(config?.layoutDetails);
134-
const layoutFidgetIds = layoutConfig?.layout && config.fidgetInstanceDatums
137+
const layoutFidgetIds = layoutConfig?.layout && config?.fidgetInstanceDatums
135138
? extractFidgetIdsFromLayout(layoutConfig.layout, config.fidgetInstanceDatums)
136139
: [];
137140

138-
141+
139142
// Figure out what should be visible
140143
const shouldShowFeed = !!feed && (!isMobile || (showFeedOnMobile && !isHomebasePath));
141144
const shouldShowMainLayout = !(isMobile && showFeedOnMobile && !isHomebasePath);
@@ -155,8 +158,10 @@ export default function Space({
155158
}), [commitConfig, resetConfig, setEditMode, setMobilePreview]);
156159

157160
useEffect(() => {
158-
setSidebarEditable(config.isEditable);
159-
}, [config.isEditable, setSidebarEditable]);
161+
if (config) {
162+
setSidebarEditable(config.isEditable);
163+
}
164+
}, [config?.isEditable, setSidebarEditable]);
160165

161166
// Cleanup logic to remove orphaned fidgets
162167
const cleanupHasRun = React.useRef(false);
@@ -191,10 +196,10 @@ export default function Space({
191196
}
192197

193198
cleanupHasRun.current = true;
194-
}, [config.layoutDetails, config.fidgetInstanceDatums, profile, feed, saveConfig, commitConfig]);
199+
}, [config?.layoutDetails, config?.fidgetInstanceDatums, profile, feed, saveConfig, commitConfig]);
195200

196-
// Props for our layout components
197-
const baseLayoutProps = {
201+
// Props for our layout components (only used when config is defined)
202+
const baseLayoutProps = config ? {
198203
theme: config.theme,
199204
fidgetInstanceDatums: config.fidgetInstanceDatums,
200205
fidgetTrayContents: config.fidgetTrayContents,
@@ -210,21 +215,21 @@ export default function Space({
210215
fid: config.fid,
211216
inEditMode: !viewportMobile && editMode,
212217
isHomebasePath: isHomebasePath,
213-
};
218+
} : null;
214219

215-
const desktopViewProps = {
220+
const desktopViewProps = baseLayoutProps ? {
216221
...baseLayoutProps,
217222
layoutFidgetKey: getLayoutFidgetForMode(config?.layoutDetails, 'desktop'),
218223
inEditMode: !viewportMobile && editMode,
219-
};
224+
} : null;
220225

221-
const mobileLayoutProps = {
226+
const mobileLayoutProps = baseLayoutProps ? {
222227
...baseLayoutProps,
223228
layoutFidgetIds,
224229
isHomebasePath,
225230
hasFeed: !!feed,
226231
feed,
227-
};
232+
} : null;
228233

229234
const mainContent = (
230235
<div className="flex flex-col h-full">
@@ -266,22 +271,29 @@ export default function Space({
266271
: "grow h-[calc(100vh-64px)]"
267272
}
268273
>
269-
<Suspense
270-
fallback={
271-
<SpaceLoading
272-
hasProfile={!isNil(profile)}
273-
hasFeed={!isNil(feed)}
274-
/>
275-
}
276-
>
277-
{isMobile ? (
278-
<MobileViewSimplified
279-
{...mobileLayoutProps}
280-
/>
281-
) : (
282-
<DesktopView {...desktopViewProps} />
283-
)}
284-
</Suspense>
274+
{isConfigLoading ? (
275+
<SpaceLoading
276+
hasProfile={!isNil(profile)}
277+
hasFeed={!isNil(feed)}
278+
/>
279+
) : (
280+
<Suspense
281+
fallback={
282+
<SpaceLoading
283+
hasProfile={!isNil(profile)}
284+
hasFeed={!isNil(feed)}
285+
/>
286+
}
287+
>
288+
{isMobile && mobileLayoutProps ? (
289+
<MobileViewSimplified
290+
{...mobileLayoutProps}
291+
/>
292+
) : desktopViewProps ? (
293+
<DesktopView {...desktopViewProps} />
294+
) : null}
295+
</Suspense>
296+
)}
285297
</div>
286298
)}
287299
</div>
@@ -290,7 +302,7 @@ export default function Space({
290302

291303
return (
292304
<>
293-
{showMobileContainer && editMode && portalRef.current
305+
{showMobileContainer && editMode && portalRef.current && config
294306
? createPortal(
295307
<aside
296308
id="logo-sidebar"
@@ -321,7 +333,7 @@ export default function Space({
321333
: "user-theme-background flex flex-col"
322334
}`}
323335
style={{
324-
backgroundColor: showMobileContainer ? undefined : config.theme?.properties.background,
336+
backgroundColor: showMobileContainer ? undefined : config?.theme?.properties.background,
325337
...(showMobileContainer && {
326338
backgroundImage: "url('/images/space-background.png')",
327339
backgroundSize: 'cover',
@@ -332,7 +344,7 @@ export default function Space({
332344
}}
333345
>
334346
<div className="w-full h-full transition-all duration-100 ease-out">
335-
{showMobileContainer ? (
347+
{showMobileContainer && config ? (
336348
<MobilePreview
337349
config={config}
338350
editMode={editMode}
@@ -349,7 +361,7 @@ export default function Space({
349361
/>
350362
) : (
351363
<>
352-
<CustomHTMLBackground html={config.theme?.properties.backgroundHTML} />
364+
<CustomHTMLBackground html={config?.theme?.properties.backgroundHTML ?? ""} />
353365
{mainContent}
354366
</>
355367
)}

src/app/(spaces)/SpacePage.tsx

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { ReactNode, useRef, useEffect } from "react";
1+
import React, { ReactNode } from "react";
22
import Space, { SpaceConfig, SpaceConfigSaveDetails } from "./Space";
33
import { useSidebarContext } from "@/common/components/organisms/Sidebar";
44

@@ -24,37 +24,6 @@ export default function SpacePage({
2424
feed,
2525
showFeedOnMobile,
2626
}: SpacePageArgs) {
27-
28-
// Create a stable deferred promise that won't change across renders
29-
const deferredRef = useRef<{
30-
promise: Promise<void>;
31-
resolve: () => void;
32-
} | null>(null);
33-
34-
// Initialize the deferred promise once
35-
if (!deferredRef.current) {
36-
let resolve: () => void;
37-
const promise = new Promise<void>((res) => {
38-
resolve = res;
39-
});
40-
deferredRef.current = {
41-
promise,
42-
resolve: resolve!,
43-
};
44-
}
45-
46-
// Watch for config becoming defined and resolve the deferred
47-
useEffect(() => {
48-
if (config && deferredRef.current) {
49-
deferredRef.current.resolve();
50-
}
51-
}, [config]);
52-
53-
// If config is undefined, throw the stable promise to trigger Suspense fallback
54-
if (!config) {
55-
throw deferredRef.current.promise;
56-
}
57-
5827
const { editMode, setEditMode, setSidebarEditable, portalRef } =
5928
useSidebarContext();
6029

0 commit comments

Comments
 (0)