Loading state

Wait for asynchronous initial files before rendering UI or enabling actions.

Declarative gating

Render a skeleton while initial files are loading, then render your preview once hydration is complete.

example.tsx
import {
UplofileRoot,
UplofileTrigger,
UplofilePreview,
} from "@/components/ui/uplofile";
import { mockUpload } from "@/lib/utils.ts";
export default function LoadingStateDeclarativeDemo() {
return (
<UplofileRoot
upload={mockUpload}
initial={loadInitial()}
accept="image/*"
multiple
>
<UplofileTrigger
asChild
render={({ isLoading, open }) => (
<button
onClick={open}
disabled={isLoading}
className="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 shadow"
>
{isLoading ? "Loading initial files…" : "Select Images"}
</button>
)}
/>
<div className="mt-6">
<UplofilePreview
render={({ isLoading, items }) => {
if (isLoading) {
return (
<div className="grid grid-cols-3 gap-3">
{Array.from({ length: 3 }).map((_, i) => (
<div
key={i}
className="aspect-square rounded-lg bg-muted animate-pulse"
/>
))}
</div>
);
}
return (
<div className="grid grid-cols-3 gap-3">
{items.map((item) => (
<img
key={item.uid}
src={item.url || item.previewUrl}
alt={item.name}
className="aspect-square w-full h-full object-cover rounded-lg border"
/>
))}
</div>
);
}}
/>
</div>
</UplofileRoot>
);
}
// Simulate async initial files hydration from server
const loadInitial = () =>
new Promise<Array<{ uid: string; name: string; url: string }>>((resolve) => {
setTimeout(
() =>
resolve([
{
uid: "srv-1",
name: "server-image.jpg",
url: "https://picsum.photos/id/237/600/400",
},
]),
3500,
);
});

Imperative subscription

Subscribe to loading changes using an imperative ref, and toggle your own UI state when hydration completes.

Hydrating initial files…
example.tsx
import { useEffect, useRef, useState } from "react";
import type { UplofileRootRef } from "uplofile";
import {
UplofileRoot,
UplofilePreview,
UplofileTrigger,
} from "@/components/ui/uplofile";
import { mockUpload } from "@/lib/utils.ts";
import { twMerge } from "tailwind-merge";
export default function LoadingStateImperativeDemo() {
const rootRef = useRef<UplofileRootRef>(null);
const [ready, setReady] = useState(false);
useEffect(() => {
// Subscribe imperatively to hydration status
if (!rootRef.current) return;
rootRef.current.onLoadingChange = (loading) => {
setReady(!loading);
};
}, []);
return (
<div className="space-y-4">
<div className="flex items-center gap-2 text-sm">
<div
className={`h-2 w-2 rounded-full ${ready ? "bg-emerald-500" : "bg-amber-500 animate-pulse"}`}
/>
<span>{ready ? "Hydrated • ready" : "Hydrating initial files…"}</span>
</div>
<UplofileRoot
ref={rootRef}
upload={mockUpload}
initial={loadInitial()}
accept="image/*"
>
<UplofileTrigger
render={({ open }) => (
<button
onClick={open}
className="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 shadow"
>
Add more
</button>
)}
/>
<div className={"grid mt-6 gap-3 items-center grid-cols-3"}>
<UplofilePreview className="!block" />
{Array.from({ length: 3 }).map((_, i) => (
<div
key={i}
className={twMerge(
"aspect-square rounded-lg bg-muted animate-pulse",
ready && "hidden",
)}
/>
))}
</div>
</UplofileRoot>
</div>
);
}
const loadInitial = () =>
new Promise<Array<{ uid: string; name: string; url: string }>>((resolve) => {
setTimeout(
() =>
resolve([
{
uid: "srv-2",
name: "hydrated-from-server.jpg",
url: "https://picsum.photos/id/1025/600/400",
},
]),
6000,
);
});

Form integration

Prevent premature form submission by disabling the submit button until initial files finish hydrating.

Hydrating initial attachments…

example.tsx
import {
UplofileRoot,
UplofileHiddenInput,
UplofilePreview,
UplofileTrigger,
} from "@/components/ui/uplofile";
import { mockUpload } from "@/lib/utils.ts";
import { IoSendOutline, IoReloadOutline } from "react-icons/io5";
export default function LoadingStateFormDemo() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
alert("Submitted! Check console for payload.");
console.log(Object.fromEntries(fd));
};
return (
<form className="space-y-4" onSubmit={handleSubmit}>
<UplofileRoot
upload={mockUpload}
initial={loadInitial()}
name="attachments"
>
<UplofileHiddenInput />
<UplofileTrigger
render={({ isLoading, open }) => (
<button
type="button"
onClick={open}
disabled={isLoading}
className="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 shadow"
>
{isLoading ? (
<span className="inline-flex items-center gap-2">
<IoReloadOutline className="h-4 w-4 animate-spin" />{" "}
Preparing…
</span>
) : (
"Add attachments"
)}
</button>
)}
/>
<UplofilePreview
render={({ items, isLoading }) => (
<div className="mt-4 space-y-2">
{isLoading && (
<p className="text-xs text-muted-foreground">
Hydrating initial attachments…
</p>
)}
{items.map((it) => (
<div key={it.uid} className="text-sm text-muted-foreground">
{it.name}
</div>
))}
</div>
)}
/>
<div className="pt-2 border-t mt-4">
<UplofileTrigger
render={({ isLoading }) => (
<button
type="submit"
disabled={isLoading}
className="w-full inline-flex items-center justify-center gap-2 rounded-lg text-sm font-bold ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-gray-900 text-white hover:bg-gray-800 h-11 px-8 shadow-lg"
>
<IoSendOutline className="h-4 w-4" />
{isLoading ? "Please wait…" : "Submit"}
</button>
)}
/>
</div>
</UplofileRoot>
</form>
);
}
const loadInitial = () =>
new Promise<Array<{ uid: string; name: string; url: string }>>((resolve) => {
setTimeout(
() =>
resolve([
{
uid: "srv-3",
name: "doc-from-server.pdf",
url: "https://example.com/doc-from-server.pdf",
},
]),
6000,
);
});