@betechspark/fileserver-react
v0.1.1
Published
React 19 hooks and components for betechspark File Server uploads, previews, and deletes.
Maintainers
Readme
@betechspark/fileserver-react
React integration for the betechspark File Server — TanStack Query hooks, context provider, and minimal upload/preview UI. Depends on @betechspark/fileserver-client.
npm: @betechspark/fileserver-react · Related: fileserver-client · fileserver-server
Installation
npm install @betechspark/fileserver-react @betechspark/fileserver-client @tanstack/react-queryPeer dependencies: react ^19, @tanstack/react-query ^5
Setup
Wrap your app with QueryClientProvider and FileserverProvider:
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createHttpFileserverGateway } from '@betechspark/fileserver-client';
import { FileserverProvider } from '@betechspark/fileserver-react';
import { useMemo, useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
const gateway = useMemo(
() =>
createHttpFileserverGateway({
baseUrl: process.env.NEXT_PUBLIC_FILESERVER_URL ?? 'http://127.0.0.1:3004',
}),
[]
);
return (
<QueryClientProvider client={queryClient}>
<FileserverProvider gateway={gateway}>{children}</FileserverProvider>
</QueryClientProvider>
);
}Examples
Complete upload + preview page
'use client';
import {
FileDropzone,
FilePreview,
useDeleteFile,
useFile,
useFileUpload,
} from '@betechspark/fileserver-react';
import { useState } from 'react';
export function FileManager({ ownerId }: { ownerId: string }) {
const upload = useFileUpload();
const deleteFile = useDeleteFile();
const [fileId, setFileId] = useState<string | null>(null);
const { data: meta, isLoading } = useFile(fileId ?? '');
return (
<div>
<FileDropzone
accept="image/*,application/pdf"
disabled={upload.isPending}
onFiles={(files) => {
const file = files[0];
if (!file) return;
upload.mutate(
{
file,
filename: file.name,
ownerId,
visibility: 'private',
useTus: file.size > 10 * 1024 * 1024,
},
{ onSuccess: (r) => setFileId(r.id) }
);
}}
/>
{upload.isError && <p>Upload failed: {upload.error.message}</p>}
{upload.isPending && <p>Uploading…</p>}
{fileId && (
<>
{isLoading ? <p>Loading meta…</p> : meta ? <p>{meta.filename} ({meta.sizeBytes} B)</p> : null}
<FilePreview fileId={fileId} alt="preview" />
<button
type="button"
disabled={deleteFile.isPending}
onClick={() =>
deleteFile.mutate(fileId, { onSuccess: () => setFileId(null) })
}
>
Delete
</button>
</>
)}
</div>
);
}Production — server-signed image URLs
Do not rely on client-side gateway.signDownload for private files. Fetch a signed URL from your API:
'use client';
import { useQuery } from '@tanstack/react-query';
function SignedImage({ fileId }: { fileId: string }) {
const { data, isLoading, isError } = useQuery({
queryKey: ['signed-url', fileId],
queryFn: async () => {
const res = await fetch(`/api/sign-download?fileId=${encodeURIComponent(fileId)}`);
if (!res.ok) throw new Error('Failed to sign URL');
return res.json() as Promise<{ url: string; expUnix: number }>;
},
staleTime: 240_000, // refresh before 300 s TTL expires
});
if (isLoading) return <p>Loading…</p>;
if (isError || !data) return <p>Unavailable</p>;
return <img src={data.url} alt="" />;
}Implement /api/sign-download with @betechspark/fileserver-server (see that package’s README).
Upload via your API (recommended for auth)
Proxy uploads through your backend instead of calling the File Server from the browser:
'use client';
import { useMutation } from '@tanstack/react-query';
function AvatarUpload({ userId }: { userId: string }) {
const upload = useMutation({
mutationFn: async (file: File) => {
const body = new FormData();
body.set('file', file);
body.set('userId', userId);
const res = await fetch('/api/avatar', { method: 'POST', body });
if (!res.ok) throw new Error(await res.text());
return res.json() as Promise<{ fileId: string; previewUrl: string }>;
},
});
return (
<input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) upload.mutate(file);
}}
/>
);
}Public files — direct URL without signing
For visibility: 'public' uploads, skip signing:
import { useFileSrc } from '@betechspark/fileserver-react';
function PublicImage({ fileId }: { fileId: string }) {
const baseUrl = process.env.NEXT_PUBLIC_FILESERVER_URL ?? 'http://127.0.0.1:3004';
const { data: src } = useFileSrc(fileId, { signed: false });
// useFileSrc returns `/files/{id}` — prepend base URL for cross-origin:
const url = src ? `${baseUrl.replace(/\/+$/, '')}${src}` : undefined;
return url ? <img src={url} alt="" /> : null;
}Hook reference
| Hook | Behavior |
|------|----------|
| useFileserverGateway() | Gateway from context |
| useFileUpload() | Mutation: uploadFile or tusUpload when useTus: true |
| useFile(fileId) | Query: metadata via getFileMeta |
| useFileSrc(fileId, options?) | Query: URL for <img src> (signed default true) |
| useDeleteFile() | Mutation: deleteFile |
UI components
| Component | Purpose |
|-----------|---------|
| FileDropzone | Drag-and-drop / click-to-select → onFiles(File[]) |
| FilePreview | <img> via useFileSrc |
Components use minimal inline styles — wrap them in your design system.
Detailed usage (step-by-step)
See USAGE.md in the monorepo for a numbered walkthrough.
Monorepo development
pnpm turbo run build --filter=@betechspark/fileserver-client --filter=@betechspark/fileserver-reactExports
FileserverProvider,useFileserverGatewayuseFileUpload,useFile,useFileSrc,useDeleteFileFileDropzone,FilePreview
Related
@betechspark/fileserver-client— HTTP gateway and use cases@betechspark/fileserver-server— server-side HMAC signing- betechspark File Server — Rust service (port 3004)
