List

List

The List component is a container for displaying lists of records. It handles all the complex parts of data fetching, pagination, filtering, and error handling, so you can focus on how to display your data.

Think of it as a data wrapper: The List component fetches your data and makes it available to child components like DataTable. It's the backbone of any list page in your admin.

What It Does

The List component automatically:

  • Fetches data from your database using better-query
  • Handles pagination with page navigation
  • Manages filters and search
  • Shows loading states while fetching
  • Handles errors gracefully
  • Provides data to children through React context

Usage

Basic Example

The simplest way to display a list:

import { List, DataTable } from '@/components/admin';

export const PostList = () => (
  <List resource="posts">
    <DataTable>
      <DataTable.Col source="title" />
      <DataTable.Col source="author" />
      <DataTable.Col source="created_at" />
    </DataTable>
  </List>
);

What happens:

  1. List fetches all posts from your database
  2. DataTable receives the posts and displays them
  3. Pagination is added automatically if you have many posts
  4. Loading state shows while fetching

What is a resource? A resource is a type of data in your database, like "posts", "users", or "products". You define resources in your better-query configuration.

Props

PropTypeRequiredDescription
resourcestringYesName of the resource to fetch (e.g., "posts", "users")
titlestringNoPage title (defaults to resource name)
childrenReactNodeYesContent to display (usually DataTable)
actionsReactNodeNoAction buttons to show in the header
filtersFilterConfig[]NoAvailable filter options
perPagenumberNoNumber of items per page (default: 10)

The resource prop is key: Make sure the resource name matches what you defined in your better-query configuration. If you created a "user" resource, use resource="user".

Examples

Basic List

Display a simple list of records:

import { List, DataTable } from '@/components/admin';

export const PostList = () => (
  <List resource="posts">
    <DataTable>
      <DataTable.Col source="id" />
      <DataTable.Col source="title" />
      <DataTable.Col source="status" />
      <DataTable.Col source="created_at" />
    </DataTable>
  </List>
);

Best for: Simple lists where you just want to display data.

With Custom Actions

Add buttons to the list header:

import { List, DataTable, CreateButton } from '@/components/admin';
import { Button } from '@/components/ui/button';

export const PostList = () => (
  <List 
    resource="posts"
    actions={
      <div className="flex gap-2">
        <CreateButton />
        <Button variant="outline">Export</Button>
        <Button variant="outline">Import</Button>
      </div>
    }
  >
    <DataTable>
      <DataTable.Col source="title" />
      <DataTable.Col source="author" />
    </DataTable>
  </List>
);

What this adds:

  • Create button - Navigates to the create page
  • Export button - Your custom export functionality
  • Import button - Your custom import functionality

CreateButton is smart: It automatically navigates to /{resource}/create when clicked. For this example, it goes to /posts/create.

With Filters

Add filtering capabilities:

import { List, DataTable } from '@/components/admin';

const filters = [
  { 
    source: 'status', 
    type: 'select', 
    choices: [
      { id: 'draft', name: 'Draft' },
      { id: 'published', name: 'Published' },
      { id: 'archived', name: 'Archived' }
    ]
  },
  { 
    source: 'author', 
    type: 'text',
    placeholder: 'Search by author...'
  },
];

export const PostList = () => (
  <List resource="posts" filters={filters}>
    <DataTable>
      <DataTable.Col source="title" />
      <DataTable.Col source="status" />
      <DataTable.Col source="author" />
    </DataTable>
  </List>
);

What this adds:

  • Dropdown to filter by status
  • Text input to search by author
  • Filters update the data automatically

With Custom Title

Override the default page title:

import { List, DataTable } from '@/components/admin';

export const PostList = () => (
  <List resource="posts" title="All Blog Posts">
    <DataTable>
      <DataTable.Col source="title" />
      <DataTable.Col source="author" />
    </DataTable>
  </List>
);

Default behavior: Without a custom title, List shows "Posts" (the resource name, capitalized).

Complete Example

A production-ready list with all features:

import { List, DataTable, CreateButton, EditButton, DeleteButton } from '@/components/admin';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';

const filters = [
  { source: 'status', type: 'select', choices: statusChoices },
  { source: 'author', type: 'text' },
  { source: 'category', type: 'select', choices: categoryChoices },
];

export const PostList = () => (
  <List 
    resource="posts" 
    title="Blog Posts"
    filters={filters}
    perPage={25}
    actions={
      <div className="flex gap-2">
        <CreateButton />
        <Button variant="outline">Export CSV</Button>
      </div>
    }
  >
    <DataTable>
      <DataTable.Col 
        source="title" 
        sortable
        render={(title, record) => (
          <a href={`/posts/${record.id}`} className="font-medium hover:underline">
            {title}
          </a>
        )}
      />
      <DataTable.Col 
        source="status"
        render={(status) => (
          <Badge variant={status === 'published' ? 'default' : 'secondary'}>
            {status}
          </Badge>
        )}
      />
      <DataTable.Col source="author" sortable />
      <DataTable.Col source="created_at" sortable />
      <DataTable.Col 
        label="Actions"
        render={(_, record) => (
          <div className="flex gap-2">
            <EditButton record={record} size="sm" />
            <DeleteButton record={record} size="sm" />
          </div>
        )}
      />
    </DataTable>
  </List>
);

This example includes:

  • Custom title
  • Multiple filters
  • 25 items per page
  • Custom action buttons
  • Sortable columns
  • Custom cell rendering
  • Row actions (edit, delete)

How It Works

The List component connects your data to the UI:

┌────────────────────────────────────────┐
│         <List resource="posts">        │
│                                         │
│  1. Fetches posts from database        │
│  2. Handles pagination                 │
│  3. Manages loading/error states       │
│  4. Provides data to children          │
│                                         │
│         ↓                               │
│    <DataTable>                          │
│  - Receives posts data                 │
│  - Displays in table                   │
│  - Adds sorting/filtering              │
└────────────────────────────────────────┘

Why this separation?

  • List handles data fetching (the "what")
  • Child components handle display (the "how")
  • You can swap DataTable for cards, grids, or custom views

Data Integration

The List component automatically:

Fetches Data

Uses better-query to fetch records:

// Behind the scenes, List does this:
const { list } = useQuery("posts", query);
const { data, isLoading, error } = list.useQuery();

You don't need to write this code - List handles it automatically.

Handles Pagination

Automatically adds pagination controls when you have many records:

<List resource="posts" perPage={10}>
  {/* Pagination controls added automatically */}
</List>

Manages State

Shows appropriate UI for different states:

  • Loading - Shows loading spinner
  • Error - Shows error message
  • Empty - Shows "No records found"
  • Success - Shows your data

Provides Context

Makes data available to all child components through React Context. Any component inside <List> can access the data using hooks.

Performance: List uses React Query for caching. If you navigate away and come back, the data might load instantly from cache instead of fetching again.

Installation

Install the List component using the CLI:

npx better-admin add list

Already installed? The List component is often included when you initialize Better Admin. Check your src/components/admin/ directory.

Troubleshooting

"Resource not found"

Problem: The resource name doesn't match your configuration.

Solution: Make sure you've defined the resource in your better-query setup:

lib/query.ts
const postResource = createResource({
  name: "post",  // ← This must match your List resource prop
  schema: { /* ... */ }
});

export const query = betterQuery({
  resources: [postResource],
});
// Then use the same name:
<List resource="post">  {/* ← Matches resource name */}

No data showing

Problem: List loads but shows "No records found".

Solution: Check these common issues:

  1. Does your database have records?
  2. Do you have permission to read this resource?
  3. Check browser console for errors

Pagination not showing

Problem: You have many records but no pagination controls.

Solution: Pagination appears automatically when you have more than perPage records. The default is 10 items per page.

// Show more items per page
<List resource="posts" perPage={25}>

Filters not working

Problem: Filter inputs show but don't filter data.

Solution: Make sure your filter fields match database fields:

const filters = [
  { source: 'status', type: 'select', choices: statusChoices },
  //       ^^^^^^ Must match database field name
];

Still having issues? Check that your better-query resource has proper permissions. The read permission must return true for list queries to work.

Learn about related components and concepts: