Uplofile is open sourceStar on GitHub

Loading State & Initial Hydration

Use isLoading to wait for asynchronousinitial files to hydrate before rendering UI or enabling actions. You can also subscribe imperatively via ref.current.onLoadingChange.

Declarative gating with isLoading

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

1import {
2 UplofileRoot,
3 UplofileTrigger,
4 UplofilePreview,
5} from "@/components/ui/uplofile";
6import { mockUpload } from "@/lib/utils.ts";
7
8export default function LoadingStateDeclarativeDemo() {
9 return (
10 <UplofileRoot
11 upload={mockUpload}
12 initial={loadInitial()}
13 accept="image/*"
14 multiple
15 >
16 <UplofileTrigger
17 render={({ isLoading, open }) => (
18 <button
19 onClick={open}
20 disabled={isLoading}
21 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"
22 >
23 {isLoading ? "Loading initial files…" : "Select Images"}
24 </button>
25 )}
26 />
27
28 <div className="mt-6">
29 <UplofilePreview
30 render={({ isLoading, items }) => {
31 if (isLoading) {
32 return (
33 <div className="grid grid-cols-3 gap-3">
34 {Array.from({ length: 3 }).map((_, i) => (
35 <div
36 key={i}
37 className="aspect-square rounded-lg bg-muted animate-pulse"
38 />
39 ))}
40 </div>
41 );
42 }
43
44 return (
45 <div className="grid grid-cols-3 gap-3">
46 {items.map((item) => (
47 <img
48 key={item.uid}
49 src={item.url || item.previewUrl}
50 alt={item.name}
51 className="aspect-square w-full h-full object-cover rounded-lg border"
52 />
53 ))}
54 </div>
55 );
56 }}
57 />
58 </div>
59 </UplofileRoot>
60 );
61}
62
63// Simulate async initial files hydration from server
64const loadInitial = () =>
65 new Promise<Array<{ uid: string; name: string; url: string }>>((resolve) => {
66 setTimeout(
67 () =>
68 resolve([
69 {
70 uid: "srv-1",
71 name: "server-image.jpg",
72 url: "https://picsum.photos/id/237/600/400",
73 },
74 ]),
75 3500,
76 );
77 });

Imperative subscription via ref.onLoadingChange

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

Hydrating initial files…
1import { useEffect, useRef, useState } from "react";
2import type { UplofileRootRef } from "uplofile";
3import {
4 UplofileRoot,
5 UplofilePreview,
6 UplofileTrigger,
7} from "@/components/ui/uplofile";
8import { mockUpload } from "@/lib/utils.ts";
9import { twMerge } from "tailwind-merge";
10
11export default function LoadingStateImperativeDemo() {
12 const rootRef = useRef<UplofileRootRef>(null);
13 const [ready, setReady] = useState(false);
14
15 useEffect(() => {
16 // Subscribe imperatively to hydration status
17 if (!rootRef.current) return;
18 rootRef.current.onLoadingChange = (loading) => {
19 setReady(!loading);
20 };
21 }, []);
22
23 return (
24 <div className="space-y-4">
25 <div className="flex items-center gap-2 text-sm">
26 <div
27 className={`h-2 w-2 rounded-full ${ready ? "bg-emerald-500" : "bg-amber-500 animate-pulse"}`}
28 />
29 <span>{ready ? "Hydrated • ready" : "Hydrating initial files…"}</span>
30 </div>
31
32 <UplofileRoot
33 ref={rootRef}
34 upload={mockUpload}
35 initial={loadInitial()}
36 accept="image/*"
37 >
38 <UplofileTrigger
39 render={({ open }) => (
40 <button
41 onClick={open}
42 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"
43 >
44 Add more
45 </button>
46 )}
47 />
48 <div className={"grid mt-6 gap-3 items-center grid-cols-3"}>
49 <UplofilePreview className="!block" />
50 {Array.from({ length: 3 }).map((_, i) => (
51 <div
52 key={i}
53 className={twMerge(
54 "aspect-square rounded-lg bg-muted animate-pulse",
55 ready && "hidden",
56 )}
57 />
58 ))}
59 </div>
60 </UplofileRoot>
61 </div>
62 );
63}
64
65const loadInitial = () =>
66 new Promise<Array<{ uid: string; name: string; url: string }>>((resolve) => {
67 setTimeout(
68 () =>
69 resolve([
70 {
71 uid: "srv-2",
72 name: "hydrated-from-server.jpg",
73 url: "https://picsum.photos/id/1025/600/400",
74 },
75 ]),
76 6000,
77 );
78 });

Form integration: disable submit until ready

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

Hydrating initial attachments…

1import {
2 UplofileRoot,
3 UplofileHiddenInput,
4 UplofilePreview,
5 UplofileTrigger,
6} from "@/components/ui/uplofile";
7import { mockUpload } from "@/lib/utils.ts";
8import { IoSendOutline, IoReloadOutline } from "react-icons/io5";
9
10export default function LoadingStateFormDemo() {
11 const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
12 e.preventDefault();
13 const fd = new FormData(e.currentTarget);
14 alert("Submitted! Check console for payload.");
15 console.log(Object.fromEntries(fd));
16 };
17
18 return (
19 <form className="space-y-4" onSubmit={handleSubmit}>
20 <UplofileRoot
21 upload={mockUpload}
22 initial={loadInitial()}
23 name="attachments"
24 >
25 <UplofileHiddenInput />
26
27 <UplofileTrigger
28 render={({ isLoading, open }) => (
29 <button
30 type="button"
31 onClick={open}
32 disabled={isLoading}
33 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"
34 >
35 {isLoading ? (
36 <span className="inline-flex items-center gap-2">
37 <IoReloadOutline className="h-4 w-4 animate-spin" /> Preparing…
38 </span>
39 ) : (
40 "Add attachments"
41 )}
42 </button>
43 )}
44 />
45
46 <UplofilePreview
47 render={({ items, isLoading }) => (
48 <div className="mt-4 space-y-2">
49 {isLoading && (
50 <p className="text-xs text-muted-foreground">
51 Hydrating initial attachments…
52 </p>
53 )}
54 {items.map((it) => (
55 <div key={it.uid} className="text-sm text-muted-foreground">
56{it.name}
57 </div>
58 ))}
59 </div>
60 )}
61 />
62
63 <div className="pt-2 border-t mt-4">
64 <UplofileTrigger
65 render={({ isLoading }) => (
66 <button
67 type="submit"
68 disabled={isLoading}
69 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"
70 >
71 <IoSendOutline className="h-4 w-4" />
72 {isLoading ? "Please wait…" : "Submit"}
73 </button>
74 )}
75 />
76 </div>
77 </UplofileRoot>
78 </form>
79 );
80}
81
82const loadInitial = () =>
83 new Promise<Array<{ uid: string; name: string; url: string }>>((resolve) => {
84 setTimeout(
85 () =>
86 resolve([
87 {
88 uid: "srv-3",
89 name: "doc-from-server.pdf",
90 url: "https://example.com/doc-from-server.pdf",
91 },
92 ]),
93 6000,
94 );
95 });