fix(web): delay loading skeleton to avoid flash on fast loads

Render the memo list skeleton only after loading exceeds a short delay
(SKELETON_LOADING_DELAY_MS). Fast/self-hosted loads now finish before the
threshold and never flash the skeleton, while slow loads still show it.

Close #6047
pull/6051/head
boojack 2 weeks ago
parent e3e4ae1051
commit 9e84f61029

@ -7,7 +7,8 @@ import { Button } from "@/components/ui/button";
import { userServiceClient } from "@/connect";
import { useMemoFilterContext } from "@/contexts/MemoFilterContext";
import { useNewMemo } from "@/contexts/NewMemoContext";
import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
import { DEFAULT_LIST_MEMOS_PAGE_SIZE, SKELETON_LOADING_DELAY_MS } from "@/helpers/consts";
import { useDelayedFlag } from "@/hooks/useDelayedFlag";
import { useInfiniteMemos } from "@/hooks/useMemoQueries";
import { hoistMemoToFront } from "@/hooks/useMemoSorting";
import { userKeys } from "@/hooks/useUserQueries";
@ -101,6 +102,9 @@ const PagedMemoList = (props: Props) => {
{ enabled: props.enabled ?? true },
);
// Only show the skeleton once loading exceeds the delay, so fast loads don't flash it.
const showSkeleton = useDelayedFlag(isLoading, SKELETON_LOADING_DELAY_MS);
// Flatten pages into a single array of memos
const memos = useMemo(() => data?.pages.flatMap((page) => page.memos) || [], [data]);
@ -158,9 +162,11 @@ const PagedMemoList = (props: Props) => {
const children = (
<MentionResolutionProvider contents={sortedMemoList.map((memo) => memo.content)}>
<div className="flex flex-col justify-start w-full max-w-2xl mx-auto">
{/* Show skeleton loader during initial load */}
{/* During initial load, show the skeleton only after the delay; render nothing before then to avoid a flash. */}
{isLoading ? (
<Skeleton showCreator={props.showCreator} count={4} />
showSkeleton ? (
<Skeleton showCreator={props.showCreator} count={4} />
) : null
) : (
<>
{showMemoEditor ? (

@ -3,3 +3,7 @@ export const TAB_SPACE_WIDTH = 2;
// DEFAULT_LIST_MEMOS_PAGE_SIZE is the default page size for list memos request.
export const DEFAULT_LIST_MEMOS_PAGE_SIZE = 16;
// SKELETON_LOADING_DELAY_MS is how long a load must take before the loading skeleton appears.
// Loads that finish faster than this never render the skeleton, avoiding a flash on fast/self-hosted networks.
export const SKELETON_LOADING_DELAY_MS = 250;

@ -2,6 +2,7 @@ export * from "./useAsyncEffect";
export * from "./useCurrentUser";
export * from "./useDateFilterNavigation";
export * from "./useDebouncedEffect";
export * from "./useDelayedFlag";
export * from "./useFilteredMemoStats";
export * from "./useLoading";
export * from "./useLocalStorage";

@ -0,0 +1,23 @@
import { useEffect, useState } from "react";
/**
* Returns a flag that turns true only after `active` has stayed true for `delay` ms,
* and turns false immediately once `active` becomes false.
*
* Useful for delaying loading indicators (e.g. skeletons) so they don't flash on fast operations.
*/
export const useDelayedFlag = (active: boolean, delay: number): boolean => {
const [delayed, setDelayed] = useState(false);
useEffect(() => {
if (!active) {
setDelayed(false);
return;
}
const timeout = window.setTimeout(() => setDelayed(true), delay);
return () => window.clearTimeout(timeout);
}, [active, delay]);
return delayed;
};
Loading…
Cancel
Save