2.0 beta is ready. Install with @beta.
dimah-s3v1.7.2

useObjectList

Headless hook that pages a folder catalog from any query you pass in.

useObjectList keeps the current folder, the breadcrumb, and cursor pagination. It does not know where the rows live and it does not call S3. You pass a query function.

components/file-browser.tsx
"use client";

import { useObjectList } from "@dimah-s3/react";

export function FileBrowser() {
  const list = useObjectList({
    query: ({ parent, cursor, signal }) =>
      fetch(`/api/files?folder=${parent ?? ""}&cursor=${cursor ?? ""}`, {
        signal,
      }).then((response) => response.json()),
    queryKey: ["files"],
  });

  return (
    <ul>
      {list.folders.map((folder) => (
        <li key={folder.id}>
          <button type="button" onClick={() => list.openFolder(folder)}>
            {folder.name}
          </button>
        </li>
      ))}
      {list.items.map((file) => (
        <li key={file.id}>{file.name}</li>
      ))}
    </ul>
  );
}

The query receives:

  • parent — folder id, or null at the root. The id is opaque. A database id, a path, or a key prefix are all valid.
  • cursor — null on the first page. Return nextCursor: null on the last page.
  • signal — aborted when the folder changes, refresh() runs, or the hook unmounts.

Return folders only from the first page. Later pages append items. Changing queryKey clears the list and loads the first page again. Use it for filters such as the route name.

Omit parent and the hook owns navigation. Pass parent and onParentChange together when the URL owns it. parent={null} is the controlled root. defaultTrail labels the starting path. The last id should match defaultParent.

const list = useObjectList({
  query,
  parent: folderId,
  onParentChange: setFolderId,
});

readObjectListLevel splits a flat list into the folders and files at one level. getFolder returns a /-separated path, or null for the root. Pass the full set for that listing. A single page of a database query cannot be grouped after the fact. A paged tree returns folders from your own query. See Folders.

import { readObjectListLevel } from "@dimah-s3/react";

const level = readObjectListLevel(records, {
  parent,
  getFolder: (record) => record.folder,
});

With db()

dbObjectQuery adapts api.db.listObjects for a flat catalog. parent is sent as the key prefix. The adapter does not invent folder rows: generated keys contain a uuid segment, so splitting the key is not a folder tree. dbObjectListAccessors maps each row onto ObjectList: row id, object key, filename, content type, and verified size.

components/db-files.tsx
"use client";

import {
  dbClient,
  dbObjectListAccessors,
  dbObjectQuery,
} from "@dimah-s3/db/client";
import { createS3Client, useObjectList } from "@dimah-s3/react";
import { ObjectList } from "@dimah-s3/ui";

const s3Client = createS3Client({
  basePath: "/api/s3",
  plugins: [dbClient()],
});

export function DbFiles() {
  const api = s3Client.useApi();
  const list = useObjectList({
    query: dbObjectQuery(api.db.listObjects, { route: "docs" }),
    queryKey: ["docs"],
  });
  const files = dbObjectListAccessors();

  return <ObjectList list={list} {...files} />;
}

Scope still comes from the server resolveScope. The query does not accept a scope.

UI

ObjectList renders this state. Pass upload on that component to drop or paste files into the open folder. Pass usage from the query and usageLimits on the component to show how full the catalog is. It does not call the hook. Skip it and render list.items yourself when you want different markup.

Props

import type {
  UseObjectListOptions,
  UseObjectListReturn,
} from "@dimah-s3/react";

Prop

Type

Prop

Type

On this page