@smarthivelabs-devs/storage-react
v0.2.1
Published
React hooks and UI state helpers for SmartHive Storage.
Readme
@smarthivelabs-devs/storage-react
React hooks and UI state helpers for SmartHive Storage.
Install
npm install @smarthivelabs-devs/storage-react @smarthivelabs-devs/storage-sdkSetup
Wrap your app (or the subtree that needs storage) in <StorageProvider>. Pass either a pre-built client or a config object:
import { StorageProvider } from '@smarthivelabs-devs/storage-react';
// Option A — config object (client created internally)
export function App() {
return (
<StorageProvider
config={{
baseUrl: process.env.NEXT_PUBLIC_STORAGE_URL!,
appCode: process.env.NEXT_PUBLIC_STORAGE_APP_CODE!,
apiKey: process.env.NEXT_PUBLIC_STORAGE_API_KEY!,
}}
>
<YourApp />
</StorageProvider>
);
}
// Option B — pre-built client instance
import { SmartHiveStorageClient } from '@smarthivelabs-devs/storage-sdk';
const client = new SmartHiveStorageClient({ baseUrl: '...', appCode: '...', apiKey: '...' });
export function App() {
return <StorageProvider client={client}><YourApp /></StorageProvider>;
}useStorageUpload()
Upload files with full lifecycle state. Uploads cancel automatically if the component unmounts mid-upload.
import { useStorageUpload } from '@smarthivelabs-devs/storage-react';
function UploadButton() {
const { upload, retry, reset, status, progress, result, error, isUploading } = useStorageUpload();
const handleFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
try {
const uploaded = await upload({ bucket: 'avatars', file, visibility: 'PRIVATE' });
console.log('Done:', uploaded.url);
} catch {
// error is already in the `error` state field
}
};
return (
<div>
<input type="file" onChange={handleFile} disabled={isUploading} />
{status === 'uploading' && <progress value={progress} max={100} />}
{status === 'success' && <img src={result!.url} alt="Uploaded file" />}
{status === 'error' && (
<>
<p>Upload failed: {String(error)}</p>
<button onClick={retry}>Retry</button>
<button onClick={reset}>Clear</button>
</>
)}
</div>
);
}Return values
| Field | Type | Description |
|-------|------|-------------|
| upload | (input: UploadInput) => Promise<UploadResult> | Starts an upload |
| retry | () => Promise<UploadResult \| null> | Retries the last failed upload |
| reset | () => void | Resets state back to 'idle' and aborts any in-progress upload |
| status | 'idle' \| 'uploading' \| 'success' \| 'error' | Current upload state |
| isUploading | boolean | Shorthand for status === 'uploading' |
| progress | number | 0–100 progress estimate |
| result | UploadResult \| null | Set on success |
| error | unknown | Set on failure |
useStorageFile(fileId)
Fetch file metadata. Re-fetches when fileId changes. Cancels the in-flight request on unmount or fileId change.
import { useStorageFile } from '@smarthivelabs-devs/storage-react';
function FileCard({ fileId }: { fileId: string }) {
const { data, isLoading, error, reload } = useStorageFile(fileId);
if (isLoading) return <p>Loading…</p>;
if (error) return <button onClick={reload}>Retry</button>;
if (!data) return null;
return (
<div>
<img src={data.url} alt={data.originalFilename} />
<p>{data.originalFilename} — {(data.sizeBytes / 1024).toFixed(1)} KB</p>
</div>
);
}Pass null or undefined to skip fetching (useful while waiting for an ID from another request):
const { data } = useStorageFile(selectedFileId ?? null);useStorageUrl(fileId)
Fetch just the delivery URL for a file.
const { data, isLoading } = useStorageUrl(fileId);
return isLoading ? <Spinner /> : <img src={data?.url} alt="file" />;useEntityFiles(entityType, entityId)
List all files attached to an entity. Returns AsyncState<EntityFileLink[]>.
import { useEntityFiles } from '@smarthivelabs-devs/storage-react';
function PostImages({ postId }: { postId: string }) {
const { data: links, isLoading } = useEntityFiles('post', postId);
if (isLoading) return <p>Loading images…</p>;
return (
<div>
{links?.map((link) => (
<img key={link.id} src={link.file.url} alt={link.file.originalFilename} />
))}
</div>
);
}Pass null for either argument to skip fetching:
const { data } = useEntityFiles(entityType ?? null, entityId ?? null);Displaying Private Files
Private files require a signed URL. Since getSignedUrl requires your API key, call it from your server (e.g. a Next.js route handler or server action), then use the returned URL client-side.
// Server action (Next.js)
// app/actions.ts
'use server';
import { createStorageServerActionClient } from '@smarthivelabs-devs/storage-next';
export async function generateSignedUrl(fileId: string) {
const client = createStorageServerActionClient();
const signed = await client.getSignedUrl(fileId, { expiresInSeconds: 300, maxUses: 1 });
return signed.url; // full URL with token embedded
}// Client component: call the server action, use the URL directly
import { generateSignedUrl } from './actions';
function PrivateImage({ fileId }: { fileId: string }) {
const [src, setSrc] = useState<string | null>(null);
useEffect(() => {
generateSignedUrl(fileId).then(setSrc);
}, [fileId]);
return src ? <img src={src} alt="private file" /> : <span>Loading…</span>;
}The signed URL is valid for the window configured on the server (e.g. 5 minutes). Refresh it before expiry if you need longer access.
useStorageDropzone(options?)
Drag-and-drop file collection. Returns props to spread on a container element.
import { useStorageDropzone, useStorageUpload } from '@smarthivelabs-devs/storage-react';
function Dropzone() {
const { upload } = useStorageUpload();
const { isDragging, getRootProps } = useStorageDropzone({
onFiles: (files) => files.forEach((f) => upload({ bucket: 'docs', file: f })),
});
return (
<div
{...getRootProps()}
style={{ border: isDragging ? '2px solid blue' : '2px dashed gray', padding: 40 }}
>
Drop files here
</div>
);
}useStorageClient()
Access the SmartHiveStorageClient instance directly for operations not covered by the hooks.
import { useStorageClient } from '@smarthivelabs-devs/storage-react';
function DeleteButton({ fileId }: { fileId: string }) {
const client = useStorageClient();
return <button onClick={() => client.delete(fileId)}>Delete</button>;
}AsyncState shape
All read hooks return AsyncState<T>:
type AsyncState<T> = {
data: T | null;
error: unknown;
isLoading: boolean;
reload: () => Promise<T | null>;
};Abort behavior
useStorageFile,useStorageUrl, anduseEntityFileseach create anAbortControlleron mount and cancel the in-flight fetch on unmount or when their key changes — no stale state or memory leaks.useStorageUploadaborts the in-progress upload on unmount or when a newupload()call is made before the previous one completes. Callingreset()also cancels any in-progress upload.
