<List>

The <List> component is the root component for list pages. It fetches a list of records from the data provider, puts it in a ListContext, renders the default list page layout (title, buttons, filters, pagination), and renders its children. Usual children of <List>, like <Datagrid>, are responsible for displaying the list of records.

Usage

Here is the minimal code necessary to display a list of posts using a <Datagrid>:

// in src/posts.jsx
import { List, Datagrid, TextField, DateField, BooleanField } from 'react-admin';

export const PostList = () => (
    <List>
        <Datagrid>
            <TextField source="id" />
            <TextField source="title" />
            <DateField source="published_at" />
            <TextField source="category" />
            <BooleanField source="commentable" />
        </Datagrid>
    </List>
);

// in src/App.jsx
import { Admin, Resource } from 'react-admin';
import jsonServerProvider from 'ra-data-json-server';

import { PostList } from './posts';

const App = () => (
    <Admin dataProvider={jsonServerProvider('https://jsonplaceholder.typicode.com')}>
        <Resource name="posts" list={PostList} />
    </Admin>
);

export default App;

That’s enough to display a basic post list, with functional sort and pagination.

Simple posts list

You can find more advanced examples of <List> usage in the demos.

Props

Prop Required Type Default Description
children Required ReactNode - The components rendering the list of records.
actions Optional ReactElement - The actions to display in the toolbar.
aside Optional ReactElement - The component to display on the side of the list.
component Optional Component Card The component to render as the root element.
debounce Optional number 500 The debounce delay in milliseconds to apply when users change the sort or filter parameters.
disable Authentication Optional boolean false Set to true to disable the authentication check.
disable SyncWithLocation Optional boolean false Set to true to disable the synchronization of the list parameters with the URL.
empty Optional ReactElement - The component to display when the list is empty.
empty WhileLoading Optional boolean false Set to true to return null while the list is loading.
exporter Optional function - The function to call to export the list.
filters Optional ReactElement - The filters to display in the toolbar.
filter Optional object - The permanent filter values.
filter DefaultValues Optional object - The default filter values.
hasCreate Optional boolean false Set to true to show the create button.
pagination Optional ReactElement <Pagination> The pagination component to use.
perPage Optional number 10 The number of records to fetch per page.
queryOptions Optional object - The options to pass to the useQuery hook.
resource Optional string - The resource name, e.g. posts.
sort Optional object - The initial sort parameters.
storeKey Optional string | false - The key to use to store the current filter & sort. Pass false to disable
title Optional string - The title to display in the App Bar.
sx Optional object - The CSS styles to apply to the component.

Additional props are passed down to the root component (a MUI <Card> by default).

actions

By default, the <List> view displays a toolbar on top of the list. It contains:

  • A <FilterButton> to display the filter form if you set the filters prop
  • A <CreateButton> if the resource has a creation view, or if you set the hasCreate prop
  • An <ExportButton>

Actions Toolbar

You can replace this toolbar by your own using the actions prop. For instance, to add a <SelectColumnsButton> to let the user choose which columns to display in the list:

import {
    CreateButton,
    DatagridConfigurable,
    ExportButton,
    FilterButton,
    List,
    SelectColumnsButton,
    TopToolbar,
    SearchInput,
} from 'react-admin';
import IconEvent from '@mui/icons-material/Event';

const ListActions = () => (
    <TopToolbar>
        <SelectColumnsButton />
        <FilterButton/>
        <CreateButton/>
        <ExportButton/>
    </TopToolbar>
);

const postFilters = [
    <SearchInput source="q" alwaysOn />,
    <TextInput label="Title" source="title" defaultValue="Hello, World!" />,
];

export const PostList = () => (
    <List actions={<ListActions/>} filters={postFilters}>
        <DatagridConfigurable>
            ...
        </DatagridConfigurable>
    </List>
);

Use the useListContext hook to customize the actions depending on the list context, and the usePermissions to show/hide buttons depending on permissions. For example, you can hide the <CreateButton> when the user doesn’t have the right permission, and disable the <ExportButton> when the list is empty:

import {
    useListContext,
    usePermissions,
    TopToolbar,
    FilterButton,
    CreateButton,
    ExportButton,
    List
} from 'react-admin';
import IconEvent from '@mui/icons-material/Event';

const ListActions = () => {
    const { total, isLoading } = useListContext();
    const { permissions } = usePermissions();
    return (
        <TopToolbar>
            <FilterButton />
            {permissions === "admin" && <CreateButton/>}
            <ExportButton disabled={isLoading || total === 0} />
        </TopToolbar>
    );
}

aside

The default <List> layout lets you render the component of your choice on the side of the list.

List with aside

Pass a React element as the aside prop for that purpose:

const Aside = () => (
    <div style={{ width: 200, margin: '4em 1em' }}>
        <Typography variant="h6">Post details</Typography>
        <Typography variant="body2">
            Posts will only be published once an editor approves them
        </Typography>
    </div>
);

const PostList = () => (
    <List aside={<Aside />}>
        ...
    </List>
);

The aside component can call the useListContext() hook to receive the same props as the <List> child component. This means you can display additional details of the current list in the aside component. For instance, you can display the total number of views of all posts in the list:

import { Typography } from '@mui/material';
import { useListContext } from 'react-admin';

const Aside = () => {
    const { data, isLoading } = useListContext();
    if (isLoading) return null;
    return (
        <div style={{ width: 200, margin: '4em 1em' }}>
            <Typography variant="h6">Posts stats</Typography>
            <Typography variant="body2">
                Total views: {data.reduce((sum, post) => sum + post.views, 0)}
            </Typography>
        </div>
    );
};

The aside prop is also the preferred way to add a Filter Sidebar to a list view:

// in src/PostFilterSidebar.js
import { SavedQueriesList, FilterLiveSearch, FilterList, FilterListItem } from 'react-admin';
import { Card, CardContent } from '@mui/material';
import MailIcon from '@mui/icons-material/MailOutline';
import CategoryIcon from '@mui/icons-material/LocalOffer';

export const PostFilterSidebar = () => (
    <Card sx={{ order: -1, mr: 2, mt: 9, width: 200 }}>
        <CardContent>
            <SavedQueriesList />
            <FilterLiveSearch />
            <FilterList label="Subscribed to newsletter" icon={<MailIcon />}>
                <FilterListItem label="Yes" value={{ has_newsletter: true }} />
                <FilterListItem label="No" value={{ has_newsletter: false }} />
            </FilterList>
            <FilterList label="Category" icon={<CategoryIcon />}>
                <FilterListItem label="Tests" value={{ category: 'tests' }} />
                <FilterListItem label="News" value={{ category: 'news' }} />
                <FilterListItem label="Deals" value={{ category: 'deals' }} />
                <FilterListItem label="Tutorials" value={{ category: 'tutorials' }} />
            </FilterList>
        </CardContent>
    </Card>
);
// in src/PostList.js
import { PostFilterSidebar } from './PostFilterSidebar';

export const PostList = () => (
    <List aside={<PostFilterSidebar />}>
        ...
    </List>
);

Tip: the <Card sx> prop in the PostFilterSidebar component above is here to put the sidebar on the left side of the screen, instead of the default right side.

children

<List> itself doesn’t render the list of records. It delegates this task to its children components. These children components grab the data from the ListContext and render them on screen.

List children

The most common List child is <Datagrid>:

export const BookList = () => (
    <List>
        <Datagrid>
            <TextField source="id" />
            <TextField source="title" />
            <DateField source="published_at" />
            <ReferenceManyCount label="Nb comments" reference="comments" target="post_id" link />
            <BooleanField source="commentable" label="Com." />
            <NumberField source="nb_views" label="Views" />
            <>
                <EditButton />
                <ShowButton />
            </>
        </Datagrid>
    </List>
);

React-admin provides several components that can read and display a list of records from a ListContext, each with a different layout:

  • <Datagrid> displays records in a table
  • <EditableDatagrid> displays records in a table AND lets users edit them inline
  • <SimpleList> displays records in a list without many details - suitable for mobile devices
  • <Tree> displays records in a tree structure
  • <Calendar> displays event records in a calendar
  • <SingleFieldList> displays records inline, showing one field per record

So for instance, you can use a <SimpleList> instead of a <Datagrid> on mobile devices:

// in src/posts.js
import * as React from 'react';
import { useMediaQuery } from '@mui/material';
import { List, SimpleList, Datagrid, TextField, ReferenceField } from 'react-admin';

export const PostList = () => {
    const isSmall = useMediaQuery(theme => theme.breakpoints.down('sm'));
    return (
        <List>
            {isSmall ? (
                <SimpleList
                    primaryText={record => record.title}
                    secondaryText={record => `${record.views} views`}
                    tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
                />
            ) : (
                <Datagrid rowClick="edit">
                    <TextField source="id" />
                    <ReferenceField label="User" source="userId" reference="users">
                        <TextField source="name" />
                    </ReferenceField>
                    <TextField source="title" />
                    <TextField source="body" />
                </Datagrid>
            )}
        </List>
    );
};

You can also render the list of records using a custom React component. You’ll need to grab the data from the ListContext using <WithListContext>:

import { List, WithListContext } from 'react-admin';
import { Stack, Typography } from '@mui/material';

const BookList = () => (
    <List>
        <WithListContext render={({ data }) => (
            <Stack spacing={2} sx={{ padding: 2 }}>
                {data?.map(book => (
                    <Typography key={book.id}>
                        <i>{book.title}</i>, by {book.author} ({book.year})
                    </Typography>
                ))}
            </Stack>
        )} />
    </List>
);

Check Building a custom List Iterator for more details.

component

By default, the List view renders the main content area inside a Material UI <Card> element. The actual layout of the list depends on the child component you’re using (<Datagrid>, <SimpleList>, or a custom layout component).

Some List layouts display each record in a <Card>, in which case the user ends up seeing a card inside a card, which is bad UI. To avoid that, you can override the main area container by passing a component prop:

// use a div as root component
const PostList = () => (
    <List component="div">
        ...
    </List>
);

// use a custom component as root component
const PostList = () => (
    <List component={MyComponent}>
        ...
    </List>
);

The default value for the component prop is Card.

debounce

By default, <List> does not refresh the data as soon as the user enters data in the filter form. Instead, it waits for half a second of user inactivity (via lodash.debounce) before calling the dataProvider on filter change. This is to prevent repeated (and useless) calls to the API.

You can customize the debounce duration in milliseconds - or disable it completely - by passing a debounce prop to the <List> component:

// wait 1 seconds instead of 500 milliseconds befoce calling the dataProvider
const PostList = () => (
    <List debounce={1000}>
        ...
    </List>
);

disableAuthentication

By default, all pages using <List> require the user to be authenticated - any anonymous access redirects the user to the login page.

If you want to allow anonymous access to a List page, set the disableAuthentication prop to true.

import { List } from 'react-admin';

const BoolkList = () => (
    <List disableAuthentication>
        ...
    </List>
);

disableSyncWithLocation

By default, react-admin synchronizes the <List> parameters (sort, pagination, filters) with the query string in the URL (using react-router location) and the Store.

When you use a <List> component anywhere else than as <Resource list>, you may want to disable this synchronization to keep the parameters in a local state, independent for each <List> instance. This allows to have multiple lists on a single page. The drawback is that a hit on the “back” button doesn’t restore the previous list parameters. To do so, pass the disableSyncWithLocation prop.

const Dashboard = () => (
    <div>
        // ...
        <ResourceContextProvider value="posts">
            <List disableSyncWithLocation>
                <SimpleList
                    primaryText={record => record.title}
                    secondaryText={record => `${record.views} views`}
                    tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
                />
            </List>
        </ResourceContextProvider>
        <ResourceContextProvider value="comments">
            <List disableSyncWithLocation>
                <SimpleList
                    primaryText={record => record.title}
                    secondaryText={record => `${record.views} views`}
                    tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
                />
            </List>
        </ResourceContextProvider>
    </div>
)

Tip: As disableSyncWithLocation also disables the persistence of the list parameters in the Store, the storeKey prop is ignored when disableSyncWithLocation is set to true.

Please note that the selection state is not synced in the URL but in a global store using the resource as key. Thus, all lists in the page using the same resource will share the same selection state. This is a design choice because if row selection is not tied to a resource, then when a user deletes a record it may remain selected without any ability to unselect it. If you want the selection state to be local, you will have to implement your own useListController hook and pass a custom key to the useRecordSelection hook. You will then need to implement your own DeleteButton and BulkDeleteButton to manually unselect rows when deleting records.

empty

When there is no result, and there is no active filter, and the resource has a create page, react-admin displays a special page inviting the user to create the first record.

Empty invite

You can use the empty prop to replace that page by a custom component:

import { Box, Button, Typography } from '@mui/material';
import { CreateButton, List } from 'react-admin';

const Empty = () => (
    <Box textAlign="center" m={1}>
        <Typography variant="h4" paragraph>
            No products available
        </Typography>
        <Typography variant="body1">
            Create one or import from a file
        </Typography>
        <CreateButton />
        <Button onClick={/* ... */}>Import</Button>
    </Box>
);

const ProductList = () => (
    <List empty={<Empty />}>
        ...
    </List>
);

The empty component can call the useListContext() hook to receive the same props as the List child component.

You can also set the empty props value to false to bypass the empty page display and render an empty list instead.

import { List } from 'react-admin';

const ProductList = () => (
    <List empty={false}>
        ...
    </List>
);

emptyWhileLoading

Default layout components (<Datagrid> and <SimpleList>) return null when the data is loading. If you use a custom layout component instead, you’ll have to handle the case where the data is not yet defined.

That means that the following will fail on load with a “ReferenceError: data is not defined” error:

import { List, useListContext } from 'react-admin';
import { Stack, Typography } from '@mui/icons-material/Star';

const SimpleBookList = () => {
    const { data } = useListContext();
    return (
        <Stack spacing={2}>
            {data.map(book => (
                <Typography key={book.id}>
                    <i>{book.title}</i>, by {book.author} ({book.year})
                </Typography>
            ))}
        </Stack>
    );
}

const BookList = () => (
    <List>
        <SimpleBookList />
    </List>
);

You can handle this case by getting the isLoading variable from the useListContext hook:

const SimpleBookList = () => {
    const { data, isLoading } = useListContext();
    if (isLoading) return null;
    return (
        <Stack spacing={2}>
            {data.map(book => (
                <Typography key={book.id}>
                    <i>{book.title}</i>, by {book.author} ({book.year})
                </Typography>
            ))}
        </Stack>
    );
}

The <List emptyWhileLoading> prop provides a convenient shortcut for that use case. When enabled, <List> won’t render its child until data is defined.

const BookList = () => (
-   <List>
+   <List emptyWhileLoading>
        <SimpleBookList />
    </List>
);

exporter

Among the default list actions, react-admin includes an <ExportButton>. This button is disabled when there is no record in the current <List>.

By default, clicking this button will:

  1. Call the dataProvider with the current sort and filter (but without pagination),
  2. Transform the result into a CSV string,
  3. Download the CSV file.

The columns of the CSV file match all the fields of the records in the dataProvider response. That means that the export doesn’t take into account the selection and ordering of fields in your <List> via Field components. If you want to customize the result, pass a custom exporter function to the <List>. This function will receive the data from the dataProvider (after step 1) and replace steps 2-3 (i.e. it’s in charge of transforming, converting, and downloading the file).

Tip: For CSV conversion, you can import jsonexport, a CSV to JSON converter which is already a react-admin dependency. And for CSV download, take advantage of react-admin’s downloadCSV function.

Here is an example for a Posts exporter, omitting, adding, and reordering fields:

// in PostList.js
import { List, downloadCSV } from 'react-admin';
import jsonExport from 'jsonexport/dist';

const exporter = posts => {
    const postsForExport = posts.map(post => {
        const { backlinks, author, ...postForExport } = post; // omit backlinks and author
        postForExport.author_name = post.author.name; // add a field
        return postForExport;
    });
    jsonExport(postsForExport, {
        headers: ['id', 'title', 'author_name', 'body'] // order fields in the export
    }, (err, csv) => {
        downloadCSV(csv, 'posts'); // download as 'posts.csv` file
    });
};

const PostList = () => (
    <List exporter={exporter}>
        ...
    </List>
)

In many cases, you’ll need more than simple object manipulation. You’ll need to augment your objects based on relationships. For instance, the export for comments should include the title of the related post - but the export only exposes a post_id by default. For that purpose, the exporter receives a fetchRelatedRecords function as the second parameter. It fetches related records using your dataProvider.getMany() method and returns a promise.

Here is an example for a Comments exporter, fetching related Posts:

// in CommentList.js
import { List, downloadCSV } from 'react-admin';
import jsonExport from 'jsonexport/dist';

const exporter = (records, fetchRelatedRecords) => {
    // will call dataProvider.getMany('posts', { ids: records.map(record => record.post_id) }), ignoring duplicate and empty post_id
    fetchRelatedRecords(records, 'post_id', 'posts').then(posts => {
        const data = records.map(record => ({
                ...record,
                post_title: posts[record.post_id].title,
        }));
        return jsonExport(data, {
            headers: ['id', 'post_id', 'post_title', 'body'],
        }, (err, csv) => {
            downloadCSV(csv, 'comments');
        });
    });
};

const CommentList = () => (
    <List exporter={exporter}>
        ...
    </List>
)

Tip: If you need to call another verb in the exporter, take advantage of the third parameter passed to the function: it’s the dataProvider function.

Tip: The <ExportButton> limits the main request to the dataProvider to 1,000 records. If you want to increase or decrease this limit, pass a maxResults prop to the <ExportButton> in a custom <ListActions> component.

Tip: React-admin also provides a <BulkExportButton> component that depends on the exporter, and that you can use in the bulkActionButtons prop of the <Datagrid> component.

Tip: For complex (or large) exports, fetching all the related records and assembling them client-side can be slow. In that case, create the CSV on the server side, and replace the <ExportButton> component by a custom one, fetching the CSV route.

Tip: You may also remove the <ExportButton> by passing false to the exporter prop: exporter={false}

Tip: Looking for an <ImportButton>? React-admin doesn’t provide this feature, but the community has an excellent third-party module for CSV import: benwinding/react-admin-import-csv.

filters: Filter Inputs

You can add an array of filter Inputs to the List using the filters prop:

const postFilters = [
    <SearchInput source="q" alwaysOn />,
    <TextInput label="Title" source="title" defaultValue="Hello, World!" />,
];

export const PostList = () => (
    <List filters={postFilters}>
        ...
    </List>
);

Tip: Don’t mix up this filters prop, expecting an array of <Input> elements, with the filter props, which expects an object to define permanent filters (see below).

Tip: Filters will render as disabled inputs or menu items (depending on filter context) if passed the prop disabled.

Filter Inputs are regular inputs. <List> hides them all by default, except those that have the alwaysOn prop.

You can also display filters as a sidebar:

For more details about customizing filters, see the Filtering the List documentation.

filter: Permanent Filter

You can choose to always filter the list, without letting the user disable this filter - for instance to display only published posts. Write the filter to be passed to the data provider in the filter props:

// in src/posts.js
export const PostList = () => (
    <List filter={{ is_published: true }}>
        ...
    </List>
);

The actual filter parameter sent to the data provider is the result of the combination of the user filters (the ones set through the filters component form), and the permanent filter. The user cannot override the permanent filters set by way of filter.

filterDefaultValues

To set default values to filters, you can either pass an object literal as the filterDefaultValues prop of the <List> element, or use the defaultValue prop of any input component.

There is one exception: inputs with alwaysOn don’t accept defaultValue. You have to use the filterDefaultValues for those.

// in src/posts.js
const postFilters = [
    <TextInput label="Search" source="q" alwaysOn />,
    <BooleanInput source="is_published" alwaysOn />,
    <TextInput source="title" defaultValue="Hello, World!" />,
];

export const PostList = () => (
    <List filters={postFilters} filterDefaultValues={{ is_published: true }}>
        ...
    </List>
);

Tip: The filter and filterDefaultValues props have one key difference: the filterDefaultValues can be overridden by the user, while the filter values are always sent to the data provider. Or, to put it otherwise:

const filterSentToDataProvider = { ...filterDefaultValues, ...filterChosenByUser, ...filter };

hasCreate

The List page shows a Create button if the resource has a create view, or if the hasCreate prop is set to true. Using this prop lets you force the display of the create button, or hide it.

export const PostList = () => (
    <List hasCreate={false}>
        ...
    </List>
);

pagination

By default, the <List> view displays a set of pagination controls at the bottom of the list.

Pagination

The pagination prop allows to replace the default pagination controls by your own.

// in src/MyPagination.js
import { Pagination, List } from 'react-admin';

const PostPagination = () => <Pagination rowsPerPageOptions={[10, 25, 50, 100]} />;

export const PostList = () => (
    <List pagination={<PostPagination />}>
        ...
    </List>
);

Tip: If you want the new pages to be automatically fetched when users scroll down, you can use the <InfiniteList> component.

See Paginating the List for details.

perPage

By default, the list paginates results by groups of 10. You can override this setting by specifying the perPage prop:

// in src/posts.js
export const PostList = () => (
    <List perPage={25}>
        ...
    </List>
);

Note: The default pagination component’s rowsPerPageOptions includes options of 5, 10, 25 and 50. If you set your List perPage to a value not in that set, you must also customize the pagination so that it allows this value, or else there will be an error.

// in src/MyPagination.js
-import { List } from 'react-admin';
+import { List, Pagination } from 'react-admin';

export const PostList = () => (
-    <List perPage={6}>
+    <List perPage={6} pagination={<Pagination rowsPerPageOptions={[6, 12, 24, 36]} />}>
        ...
    </List>
);

queryOptions

<List> accepts a queryOptions prop to pass options to the react-query client.

This can be useful e.g. to pass a custom meta to the dataProvider.getList() call.

import { List } from 'react-admin';

const PostList = () => (
    <List queryOptions={{ meta: { foo: 'bar' } }}>
        ...
    </List>
);

With this option, react-admin will call dataProvider.getList() on mount with the ` meta: { foo: ‘bar’ }` option.

You can also use the queryOptions prop to override the default error side effect. By default, when the dataProvider.getList() call fails, react-admin shows an error notification.

You can override this behavior and pass custom side effects by providing a queryOptions prop:

import { useNotify, useRedirect, List } from 'react-admin';

const PostList = () => {
    const notify = useNotify();
    const redirect = useRedirect();

    const onError = (error) => {
        notify(`Could not load list: ${error.message}`, { type: 'error' });
        redirect('/dashboard');
    };

    return (
        <List queryOptions={{ onError }}>
            ...
        </List>
    );
}

The onError function receives the error from the dataProvider call (dataProvider.getList()), which is a JavaScript Error object (see the dataProvider documentation for details).

resource

By default, <List> operates on the current ResourceContext (defined at the routing level), so under the /posts path, the resource prop will be posts. You may want to force a different resource for a list. In this case, pass a custom resource prop, and it will override the ResourceContext value.

export const UsersList = () => (
    <List resource="users">
        ...
    </List>
);

sort

Pass an object literal as the sort prop to determine the default field and order used for sorting:

export const PostList = () => (
    <List sort={{ field: 'published_at', order: 'DESC' }}>
        ...
    </List>
);

sort defines the default sort order ; the list remains sortable by clicking on column headers.

For more details on list sort, see the Sorting The List section below.

storeKey

By default, react-admin stores the list parameters (sort, pagination, filters) in localStorage so that users can come back to the list and find it in the same state as when they left it. React-admin uses the current resource as the identifier to store the list parameters (under the key ${resource}.listParams).

If you want to display multiple lists of the same resource and keep distinct store states for each of them (filters, sorting and pagination), you must give each list a unique storeKey property. You can also disable the persistence of list parameters in the store by setting the storeKey prop to false.

In the example below, both lists NewerBooks and OlderBooks use the same resource (‘books’), but their list parameters are stored separately (under the store keys 'newerBooks' and 'olderBooks' respectively). This allows to use both components in the same app, each having its own state (filters, sorting and pagination).

import {
    Admin,
    CustomRoutes,
    Resource,
    List,
    Datagrid,
    TextField,
} from 'react-admin';
import { Route } from 'react-router-dom';

const NewerBooks = () => (
    <List
        resource="books"
        storeKey="newerBooks"
        sort={{ field: 'year', order: 'DESC' }}
    >
        <Datagrid>
            <TextField source="id" />
            <TextField source="title" />
            <TextField source="author" />
            <TextField source="year" />
        </Datagrid>
    </List>
);

const OlderBooks = () => (
    <List
        resource="books"
        storeKey="olderBooks"
        sort={{ field: 'year', order: 'ASC' }}
    >
        <Datagrid>
            <TextField source="id" />
            <TextField source="title" />
            <TextField source="author" />
            <TextField source="year" />
        </Datagrid>
    </List>
);

const Admin = () => {
    return (
        <Admin dataProvider={dataProvider}>
            <CustomRoutes>
                <Route path="/newerBooks" element={<NewerBooks />} />
                <Route path="/olderBooks" element={<OlderBooks />} />
            </CustomRoutes>
            <Resource name="books" />
        </Admin>
    );
};

Tip: The storeKey is actually passed to the underlying useListController hook, which you can use directly for more complex scenarios. See the useListController doc for more info.

Note: Selection state will remain linked to a resource-based key regardless of the storeKey. This is a design choice because if row selection is not tied to a resource, then when a user deletes a record it may remain selected without any ability to unselect it. If you want the selection state to be local, you will have to implement your own useListController hook and pass a custom key to the useRecordSelection hook. You will then need to implement your own DeleteButton and BulkDeleteButton to manually unselect rows when deleting records.

title

The default title for a list view is the plural name of the resource (e.g. “Posts”).

List title

Use the title prop to customize the List view title:

export const PostList = () => (
    <List title="List of posts">
        ...
    </List>
);

The title can be either a string or a React element.

sx: CSS API

The <List> component accepts the usual className prop but you can override many class names injected to the inner components by React-admin thanks to the sx property (see the sx documentation for syntax and examples). This property accepts the following subclasses:

Rule name Description
& .RaList-actions Applied to the actions container
& .RaList-main Applied to the main container
& .RaList-content Applied to the child component inside the main container
& .RaList-noResults Applied to the component shown when there is no result

Here is an example:

const PostList = () => (
    <List
        sx={{
            backgroundColor: 'yellow',
            '& .RaList-content': {
                backgroundColor: 'red',
            },
        }}
    >
            ...
    </List>
);

Tip: The List component classes can also be customized for all instances of the component with its global css name RaList as describe here

Scaffolding a List page

You can use <ListGuesser> to quickly bootstrap an List view on top of an existing API, without adding the fields one by one.

// in src/App.js
import * as React from "react";
import { Admin, Resource, ListGuesser } from 'react-admin';
import { dataProvider } from './dataProvider';

const App = () => (
    <Admin dataProvider={dataProvider}>
        {/* ... */}
        <Resource name="comments" list={ListGuesser} />
    </Admin>
);

Just like <List>, <ListGuesser> fetches the data. It then analyzes the response, and guesses the fields it should use to display a basic <Datagrid> with the data. It also dumps the components it has guessed in the console, so you can copy it into your own code.

Guessed List

You can learn more by reading the <ListGuesser> documentation.

Infinite Scroll Pagination

By default, the <List> component displays the first page of the list of records. To display the next page, the user must click on the “next” button. This is called “finite pagination”. An alternative is to display the next page automatically when the user scrolls to the bottom of the list. This is called “infinite pagination”.

To achieve infinite pagination, replace the <List> component with the <InfiniteList> component.

import {
-   List,
+   InfiniteList,
    Datagrid,
    TextField,
    DateField
} from 'react-admin';

const BookList = () => (
-   <List>
+   <InfiniteList>
        <Datagrid>
            <TextField source="id" />
            <TextField source="title" />
            <DateField source="author" />
        </Datagrid>
-   </List>
+   </InfiniteList>
);

<InfiniteList> is a drop-in replacement for <List>. It accepts the same props, and uses the same view layout. Check the <InfiniteList> documentation for more information.

Live Updates

If you want to subscribe to live updates on the list of records (topic: resource/[resource]), use the <ListLive> component instead.

-import { List, Datagrid, TextField } from 'react-admin';
+import { Datagrid, TextField } from 'react-admin';
+import { ListLive } from '@react-admin/ra-realtime';

const PostList = () => (
-   <List>
+   <ListLive>
        <Datagrid>
            <TextField source="title" />
        </Datagrid>
-   </List>
+   </ListLive>
);

The list will automatically update when a new record is created, or an existing record is updated or deleted.

Adding meta To The DataProvider Call

Use the queryOptions prop to pass a custom meta to the dataProvider.getList() call.

import { List } from 'react-admin';

const PostList = () => (
    <List queryOptions={{ meta: { foo: 'bar' } }}>
        ...
    </List>
);

Rendering An Empty List

When there is no data, react-admin displays a special page inviting the user to create the first record. This page can be customized using the empty prop.

You can set the empty props value to false to render an empty list instead.

import { List } from 'react-admin';

const ProductList = () => (
    <List empty={false}>
        ...
    </List>
)

Disabling Parameters Persistence

By default, react-admin stores the list parameters (sort, pagination, filters) in localStorage so that users can come back to the list and find it in the same state as when they left it. This also synchronizes the list parameters across tabs.

You can disable this feature by setting the storeKey prop to false:

import { List } from 'react-admin';

const ProductList = () => (
    <List storeKey={false}>
        ...
    </List>
)

Controlled Mode

<List> deduces the resource and the list parameters from the URL. This is fine for a page showing a single list of records, but if you need to display more than one list in a page, you probably want to define the list parameters yourself.

In that case, use the resource, sort, filter, and perPage props to set the list parameters.

import { List, SimpleList } from 'react-admin';
import { Container, Typography } from '@mui/material';

const Dashboard = () => (
    <Container>
        <Typography>Latest posts</Typography>
        <List 
            resource="posts"
            sort={{ field: 'published_at', order: 'DESC' }}
            filter={{ is_published: true }}
            perPage={10}
        >
            <SimpleList
                primaryText={record => record.title}
                secondaryText={record => `${record.views} views`}
            />
        </List>
        <Typography>Latest comments</Typography>
        <List
            resource="comments"
            sort={{ field: 'published_at', order: 'DESC' }}
            perPage={10}
        >
            <SimpleList
                primaryText={record => record.author.name}
                secondaryText={record => record.body}
                tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
            />
        </List>
    </Container>
)

Note: If you need to set the list parameters to render a list of records related to another record, there are better components than <List> for that. Check out the following components, specialized in fetching and displaying a list of related records:

If the <List> children allow to modify the list state (i.e. if they let users change the sort order, the filters, the selection, or the pagination), then you should also use the disableSyncWithLocation prop to prevent react-admin from changing the URL. This is the case e.g. if you use a <Datagrid>, which lets users sort the list by clicking on column headers.

import { List, Datagrid, TextField, NumberField, DateField } from 'react-admin';
import { Container, Typography } from '@mui/material';

const Dashboard = () => (
    <Container>
        <Typography>Latest posts</Typography>
        <List 
            resource="posts"
            sort={{ field: 'published_at', order: 'DESC' }}
            filter={{ is_published: true }}
            perPage={10}
            disableSyncWithLocation
        >
            <Datagrid bulkActionButtons={false}>
                <TextField source="title" />
                <NumberField source="views" />
            </Datagrid>
        </List>
        <Typography>Latest comments</Typography>
        <List
            resource="comments"
            sort={{ field: 'published_at', order: 'DESC' }}
            perPage={10}
            disableSyncWithLocation
        >
            <Datagrid bulkActionButtons={false}>
                <TextField source="author.name" />
                <TextField source="body" />
                <DateField source="published_at" />
            </Datagrid>
        </List>
    </Container>
)

Note: If you render more than one <Datagrid> for the same resource in the same page, they will share the selection state (i.e. the checked checkboxes). This is a design choice because if row selection is not tied to a resource, then when a user deletes a record it may remain selected without any ability to unselect it. You can get rid of the checkboxes by setting <Datagrid bulkActionButtons={false}>.

Headless Version

Besides fetching a list of records from the data provider, <List> renders the default list page layout (title, buttons, filters, a Material-UI <Card>, pagination) and its children. If you need a custom list layout, you may prefer the <ListBase> component, which only renders its children in a ListContext.

import { ListBase, WithListContext } from 'react-admin';
import { Card, CardContent, Container, Stack, Typography } from '@mui/material';

const ProductList = () => (
    <ListBase>
        <Container>
            <Typography variant="h4">All products</Typography>
            <WithListContext render={({ isLoading, data }) => (
                    !isLoading && (
                        <Stack spacing={1}>
                            {data.map(product => (
                                <Card key={product.id}>
                                    <CardContent>
                                        <Typography>{product.name}</Typography>
                                    </CardContent>
                                </Card>
                            ))}
                        </Stack>
                    )
                )} />
            <WithListContext render={({ isLoading, total }) => (
                !isLoading && <Typography>{total} results</Typography>
            )} />
        </Container>
    </ListBase>
);

The previous example leverages <WithListContext> to grab the data that <ListBase> stores in the ListContext.

If you don’t need the ListContext, you can use the useListController hook, which does the same data fetching as <ListBase> but lets you render the content.

import { useListController } from 'react-admin';
import { Card, CardContent, Container, Stack, Typography } from '@mui/material';

const ProductList = () => {
    const { isLoading, data, total } = useListController();
    return (
        <Container>
            <Typography variant="h4">All products</Typography>
                {!isLoading && (
                    <Stack spacing={1}>
                        {data.map(product => (
                            <Card key={product.id}>
                                <CardContent>
                                    <Typography>{product.name}</Typography>
                                </CardContent>
                            </Card>
                        ))}
                    </Stack>
                )}
            {!isLoading && <Typography>{total} results</Typography>}
        </Container>
    );
};

useListController returns callbacks to sort, filter, and paginate the list, so you can build a complete List page. Check the useListControllerhook documentation for details.