# useObjectList (https://v2.s3.dimah.dev/docs/react/hooks/object-list)

`useObjectList` keeps the open folder, the breadcrumb, cursor pages, search,
and sort. It does not know where the rows live and it does not call S3. You
pass a **source**: a `query` that loads one page, `getKey` for a stable row
id, and `getFile` to read a row's name, object key, size, and type.

```tsx title="components/file-browser.tsx"
"use client";

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

type FileRow = { id: string; key: string; name: string; size: number };

export function FileBrowser() {
  const list = useObjectList<FileRow>({
    query: ({ parent, cursor, sort, search, signal }) =>
      fetchFiles({ folderId: parent, cursor, sort, search, signal }),
    getKey: (file) => file.id,
    getFile: (file) => ({
      name: file.name,
      objectKey: file.key,
      size: file.size,
    }),
  });

  return (
    <ul>
      {list.entries.map((entry) => (
        <li key={entry.key}>
          {entry.kind === "folder" ? (
            <button type="button" onClick={() => list.openFolder(entry.folder)}>
              {entry.name}/
            </button>
          ) : (
            entry.name
          )}
        </li>
      ))}
    </ul>
  );
}
```

`entries` lists folders, then files. Each has a `key` that is unique across
both kinds, a `name`, and either `folder` or `item` and `file`. `items` and
`folders` are there too when you want them apart.

Keep the source stable — define it outside the component, or in `useState` /
`useMemo` — so the entries are not rebuilt on every render. `getFile` should
be pure.

## Query [#query]

The query receives:

- `parent` — folder id, or `null` at the root. The id is opaque: a database id, a path, or a key prefix.
- `cursor` — `null` on the first page. Return `nextCursor: null` on the last page.
- `sort` — `{ field, direction }`, or `null` to leave the order to the source.
- `search` — trimmed text, `""` when nothing is searched.
- `signal` — aborted when the folder, sort, search, or `queryKey` changes, when the list reloads, or when the hook unmounts.

Return `folders` from the first page only. Later pages append `items`.

Changing the folder or `queryKey` clears the rows and shows the loading
state. Use `queryKey` for filters of your own, such as the route. Changing
the sort or search keeps the current rows on screen, with `isRefreshing`,
until the new first page arrives. Opening another folder clears the search.

A query that rejects sets `status` to `error`. `error` is an `APIError`; a
rejection that is not one becomes `UNKNOWN_ERROR`, with the original as its
`cause`.

## Usage and limits [#usage-and-limits]

Return `usage` from a page when you know how full the catalog is:

```ts
return {
  items,
  nextCursor,
  usage: {
    totalBytes: 734_003_200,
    objectCount: 120,
    maxBytes: 1_073_741_824, // optional cap
    maxFiles: 500, // optional cap
  },
};
```

`list.usage` keeps the totals from the latest page that sent them. A later
page may leave `usage` out; a first page that leaves it out clears it. The
caps are for display; the server still enforces its own quota.

## Uploads and folders [#uploads-and-folders]

A source can add two optional functions. The hook returns them as
`list.uploadOptions` and `list.createFolder`.

- **`uploadOptions(parent)`** — the [request options](https://v2.s3.dimah.dev/docs/react/hooks/upload#request-options) for files uploaded into a folder (`null` is the root). It turns on drop-on-folder.
- **`createFolder({ parent, name })`** — creates an empty folder. It turns on "New folder".

```ts
const source: ObjectListSource<FileRow> = {
  query,
  getKey: (file) => file.id,
  uploadOptions: (parent) => ({ metadata: { folder: parent ?? "" } }),
  createFolder: async ({ parent, name }) => {
    const folder = await api.createFolder({ parent, name });
    return { id: folder.id, name: folder.name };
  },
};
```

`list.createFolder(name)` creates the folder inside the open folder and adds
it to the loaded folders. It rejects with an `APIError`. If the list moved to
another folder while the call ran, the new folder is not added.

With your own controls, pass the options to the upload hook. Drop targets use
`getDropTargetProps`, so folder rows keep their own click and keyboard
handlers:

```tsx
const upload = useUpload({ route: "files", maxFiles: 20, queue: true });

<div
  {...upload.getRootProps(undefined, () => ({
    uploadOptions: list.uploadOptions?.(list.parent),
  }))}
>
  <input {...upload.getInputProps()} />
  {list.folders.map((folder) => (
    <button
      key={folder.id}
      type="button"
      onClick={() => list.openFolder(folder)}
      {...upload.getDropTargetProps(undefined, () => ({
        uploadOptions: list.uploadOptions?.(folder.id),
      }))}
    >
      {folder.name}
    </button>
  ))}
</div>;
```

A drop on a folder row inside the dropzone goes to that folder. The
[`ObjectList`](https://v2.s3.dimah.dev/docs/react/ui/components/object-list) component does this
wiring for you when you pass it `upload`.

## Sources [#sources]

### memoryObjectSource [#memoryobjectsource]

`memoryObjectSource` from `@dimah-s3/react/engine` serves records you already
hold: folders from a `/`-separated path, then search, sort, and pages on the
client. Pass a function to read the latest records on each request.

```ts
import { useObjectList } from "@dimah-s3/react";
import { memoryObjectSource } from "@dimah-s3/react/engine";

const source = memoryObjectSource(files, {
  getKey: (file) => file.id,
  getFile: (file) => ({
    name: file.name,
    objectKey: file.key,
    size: file.size,
    updatedAt: file.updatedAt,
  }),
  getFolder: (file) => file.folder, // "Invoices/2026", or null for the root
  pageSize: 50,
  limits: { maxBytes: 1_073_741_824, maxFiles: 500 },
});

const list = useObjectList(source);
```

`usage` is sent with each page when `limits` is set, or when you pass
`usage: true`. It totals every record, not only the open folder. Pass
`usage: false` to turn it off.

Search matches the name, ignoring case and accents; pass `matches` to change
that. Sort understands `name`, `size`, and `updatedAt`; pass `sortValue` for
other fields. Records without a value sort last.

`readObjectListLevel` is the folder step on its own: it splits a flat list
into the folders and files at one level. Pass the full set for that listing.
One page of a database query cannot be grouped after the fact.

### dbObjectSource [#dbobjectsource]

`dbObjectSource(api.db, options)` from `@dimah-s3/db/client` lists the rows
of the [`db()`](https://v2.s3.dimah.dev/docs/db) plugin. By default it lists one folder at a time:
`parent` is a folder path, the first page carries the child folders, uploads
go to the open folder, and "New folder" calls `api.db.createFolder`. Pass
`folders: false` for a flat list with no folder actions.

```tsx title="components/db-files.tsx"
"use client";

import { useState } from "react";
import { dbClient, dbObjectSource } from "@dimah-s3/db/client";
import {
  createS3Client,
  useDelete,
  useDownload,
  useObjectList,
  useUpload,
} 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 [source] = useState(() => dbObjectSource(api.db, { route: "files" }));
  const list = useObjectList({ ...source, queryKey: ["files"] });
  const upload = useUpload({ route: "files", maxFiles: 20, queue: true });
  const download = useDownload({ route: "files" });
  const remove = useDelete({ route: "files" });

  return (
    <ObjectList
      list={list}
      upload={upload}
      download={download}
      delete={remove}
    />
  );
}
```

Search and sort run on the server: the search text matches the filename,
`name` sorts by filename, `updatedAt` by last change, and `size` by verified
size. `usage` is the scope's total with its quota caps, not the open
folder's. Scope comes from the server `resolveScope` option; the query does
not accept one.

## Navigation [#navigation]

Omit `parent` and the hook owns the folder. Pass `parent` and
`onParentChange` together when the URL owns it. `parent={null}` is the
controlled root. `defaultTrail` labels the starting path; its last id should
match `defaultParent`. `sort` / `onSortChange` and `search` /
`onSearchChange` work the same way, with `defaultSort` and `defaultSearch`
for uncontrolled starts.

```tsx
const list = useObjectList({
  ...source,
  parent: folderId,
  onParentChange: setFolderId,
});
```

`openFolder`, `navigate(trail)`, and `goUp()` move between folders.

## Loading and edits [#loading-and-edits]

- `loadMore()` loads the next page. It is ignored while another request is in flight.
- `refresh()` reloads every page on screen and swaps them in at once.
- `retry()` repeats what failed: the next page, or the reload.
- `status` is `idle`, `loading`, `refreshing`, `loading-more`, `ready`, or `error`. `isLoading`, `isRefreshing`, and `isLoadingMore` mirror it.
- `setItems(updater)`, `setFolders(updater)`, and `removeItems(keys)` edit the loaded rows without a request, for example after a rename. The next reload replaces them.

## Selection [#selection]

`useObjectListSelection(list)` adds checkbox state over the loaded entries.
It is headless: draw your own checkboxes, or pass it to `ObjectList` as
`selection`.

```tsx
const selection = useObjectListSelection(list, {
  mode: "multiple",
  isSelectable: (entry) => entry.kind === "file",
  onSelectionChange: (selected) => console.log(selected.length),
});

selection.toggle(entry.key, { range: event.shiftKey });
selection.selectAll();
selection.selectedItems; // your records
```

By default only files can be selected, and the selection clears when the
folder changes and drops entries a reload no longer returns. `mode: "single"`
keeps one entry. `persist: true` keeps entries from other folders, as in a
picker that collects files from several places.

## UI [#ui]

[`ObjectList`](https://v2.s3.dimah.dev/docs/react/ui/components/object-list) renders the list and
the selection, with search, sort, a grid view, upload, folders, usage, and a
file menu. It does not call these hooks.

## Props [#props]

```ts
import type {
  ObjectListSource,
  UseObjectListOptions,
  UseObjectListReturn,
  UseObjectListSelectionOptions,
  UseObjectListSelectionReturn,
} from "@dimah-s3/react";
```

<AutoTypeTable
  path="packages/react/src/list/types.ts"
  name="UseObjectListOptions"
/>

<AutoTypeTable
  path="packages/react/src/hooks/use-object-list.ts"
  name="UseObjectListReturn"
/>

### File [#file]

<AutoTypeTable path="packages/react/src/list/types.ts" name="ObjectListFile" />

### Usage [#usage]

<AutoTypeTable path="packages/react/src/list/types.ts" name="ObjectListUsage" />

### memoryObjectSource [#memoryobjectsource-1]

<AutoTypeTable
  path="packages/react/src/list/memory-object-source.ts"
  name="MemoryObjectSourceOptions"
/>

### Selection [#selection-1]

<AutoTypeTable
  path="packages/react/src/hooks/use-object-list-selection.ts"
  name="UseObjectListSelectionOptions"
/>

<AutoTypeTable
  path="packages/react/src/hooks/use-object-list-selection.ts"
  name="UseObjectListSelectionReturn"
/>
