Skip to content

Declarative UI

Same behaviour as the hook examples, expressed with renderless components.

tsx
import { useMemo } from 'react';
import { useUploader } from 'react-upload-kit';
import { Dropzone, FileList, UploadTrigger } from 'react-upload-kit/components';
import type { UploadAdapter } from 'react-upload-kit';

interface ApiResponse {
  url: string;
}

function DeclarativeUploader({ adapter }: { adapter: UploadAdapter<ApiResponse> }) {
  const stableAdapter = useMemo(() => adapter, [adapter]);

  const uploader = useUploader<ApiResponse>({
    adapter: stableAdapter,
    accept: ['image/*', '.pdf'],
    maxFileSize: 10 * 1024 * 1024,
    maxFiles: 8,
    autoUpload: true,
    concurrency: 2,
    maxRetries: 1,
  });

  return (
    <div>
      <Dropzone
        onDrop={uploader.addFiles}
        accept={['image/*', '.pdf']}
        maxFileSize={10 * 1024 * 1024}
      >
        {({ getRootProps, getInputProps, isDragActive, isDragReject }) => (
          <div {...getRootProps()}>
            <input {...getInputProps()} />
            <p>
              {isDragReject
                ? 'These files are not allowed'
                : isDragActive
                  ? 'Drop the files here'
                  : 'Drag & drop files here'}
            </p>
          </div>
        )}
      </Dropzone>

      <UploadTrigger onSelect={uploader.addFiles} accept={['image/*', '.pdf']}>
        {({ open, inputProps }) => (
          <>
            <button type="button" onClick={open}>
              Or browse
            </button>
            <input {...inputProps} />
          </>
        )}
      </UploadTrigger>

      {uploader.rejections.map((rejection, index) => (
        <p key={`${rejection.file.name}-${index}`}>
          {rejection.file.name}: {rejection.errors.map((e) => e.message).join(', ')}
        </p>
      ))}

      <FileList
        files={uploader.files}
        onRemove={uploader.removeFile}
        onRetry={uploader.retryFile}
        onCancel={uploader.cancelFile}
        previewMaxWidth={96}
        previewMaxHeight={96}
      >
        {({ file, preview, isPreviewLoading, remove, retry, cancel }) => (
          <div>
            {isPreviewLoading && <span>…</span>}
            {preview && <img src={preview} alt="" />}
            <span>{file.file.name}</span>
            <span>{file.status}</span>
            <progress value={file.progress} max={100} />
            {file.status === 'uploading' && (
              <button type="button" onClick={cancel}>
                Cancel
              </button>
            )}
            {(file.status === 'error' || file.status === 'cancelled') && (
              <button type="button" onClick={retry}>
                Retry
              </button>
            )}
            <button type="button" onClick={remove}>
              Remove
            </button>
          </div>
        )}
      </FileList>
    </div>
  );
}

You still own layout, copy, and CSS. The components only avoid repeating hook wiring. See Components and the component API.

Released under the MIT License.