The List View

The List view displays a list of records fetched from the API. The entry point for this view is the <List> component, which takes care of fetching the data. Then, it passes the data to an iterator view - usually <Datagrid>, which then delegates the rendering of each record property to <Field> components.

The List View

The <List> Component

The <List> component renders the list layout (title, buttons, filters, pagination), and fetches the list of records from the REST API. It then delegates the rendering of the list of records to its child component. Usually, it’s a <Datagrid>, responsible for displaying a table with one row for each post.

Tip: In Redux terms, <List> is a connected component, and <Datagrid> is a dumb component.

Here are all the props accepted by the <List> component:

Here is the minimal code necessary to display a list of posts:

// in src/App.js
import React from 'react';
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;

// in src/posts.js
import React from 'react';
import { List, Datagrid, TextField } from 'react-admin';

export const PostList = (props) => (
    <List {...props}>
        <Datagrid>
            <TextField source="id" />
            <TextField source="title" />
            <TextField source="body" />
        </Datagrid>
    </List>
);

That’s enough to display the post list:

Simple posts list

Page Title

The default title for a list view is “[resource] list” (e.g. “Posts list”). Use the title prop to customize the List view title:

// in src/posts.js
export const PostList = (props) => (
    <List {...props} title="List of posts">
        ...
    </List>
);

The title can be either a string, or an element of your own.

Actions

You can replace the list of default actions by your own element using the actions prop:

import React, { cloneElement, useMemo } from 'react';
import PropTypes from 'prop-types';
import {
    TopToolbar, CreateButton, ExportButton, Button, sanitizeListRestProps,
} from 'react-admin';
import IconEvent from '@material-ui/icons/Event';

const ListActions = ({
    currentSort,
    className,
    resource,
    filters,
    displayedFilters,
    exporter, // you can hide ExportButton if exporter = (null || false)
    filterValues,
    permanentFilter,
    hasCreate, // you can hide CreateButton if hasCreate = false
    basePath,
    selectedIds,
    onUnselectItems,
    showFilter,
    maxResults,
    total,
    ...rest
}) => (
    <TopToolbar className={className} {...sanitizeListRestProps(rest)}>
        {filters && cloneElement(filters, {
            resource,
            showFilter,
            displayedFilters,
            filterValues,
            context: 'button',
        })}
        <CreateButton basePath={basePath} />
        <ExportButton
            disabled={total === 0}
            resource={resource}
            sort={currentSort}
            filter={{ ...filterValues, ...permanentFilter }}
            exporter={exporter}
            maxResults={maxResults}
        />
        {/* Add your custom actions */}
        <Button
            onClick={() => { alert('Your custom action'); }}
            label="Show calendar"
        >
            <IconEvent />
        </Button>
    </TopToolbar>
);

ListActions.defaultProps = {
    selectedIds: [],
    onUnselectItems: () => null,
};

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

You can also use such a custom ListActions prop to omit or reorder buttons based on permissions. Just pass the permissions down from the List component:

export const PostList = ({ permissions, ...props }) => (
    <List {...props} actions={<PostActions permissions={permissions} {...props} />}>
        ...
    </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.

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

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 = props => (
    <List {...props} 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 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,
        }));
        jsonExport(data, {
            headers: ['id', 'post_id', 'post_title', 'body'],
        }, (err, csv) => {;
            downloadCSV(csv, 'comments');
        });
    });
};

const CommentList = props => (
    <List {...props} 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, as explained in the previous section.

Tip: React-admin also provides a <BulkExportButton> component that depends on the exporter, and that you can use in the bulkActionButtons prop of the <List> 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.

Bulk Action Buttons

Bulk action buttons are buttons that affect several records at once, like mass deletion for instance. In the <Datagrid> component, the bulk actions toolbar appears when a user ticks the checkboxes in the first column of the table. The user can then choose a button from the bulk actions toolbar. By default, all list views have a single bulk action button, the bulk delete button. You can add other bulk action buttons by passing a custom element as the bulkActionButtons prop of the <List> component:

import React, { Fragment } from 'react';
import Button from '@material-ui/core/Button';
import { BulkDeleteButton } from 'react-admin';
import ResetViewsButton from './ResetViewsButton';

const PostBulkActionButtons = props => (
    <Fragment>
        <ResetViewsButton label="Reset Views" {...props} />
        {/* default bulk delete action */}
        <BulkDeleteButton {...props} />
    </Fragment>
);

export const PostList = (props) => (
    <List {...props} bulkActionButtons={<PostBulkActionButtons />}>
        ...
    </List>
);

Bulk Action Buttons

Tip: React-admin provides 2 components that you can use in bulkActionButtons: <BulkDeleteButton>, and <BulkExportButton>.

Tip: You can also disable bulk actions altogether by passing false to the bulkActionButtons prop. When using a Datagrid inside a List with disabled bulk actions, the checkboxes column won’t be added.

Bulk action button components receive several props allowing them to perform their job:

  • resource: the currently displayed resource (eg posts, comments, etc.)
  • basePath: the current router base path for the resource (eg /posts, /comments, etc.)
  • filterValues: the filter values. This can be useful if you want to apply your action on all items matching the filter.
  • selectedIds: the identifiers of the currently selected items.

Here is an example leveraging the useUpdateMany hook, which sets the views property of all posts to 0:

// in ./ResetViewsButton.js
import React from 'react';
import {
    Button,
    useUpdateMany,
    useRefresh,
    useNotify,
    useUnselectAll,
} from 'react-admin';
import { VisibilityOff } from '@material-ui/icons';

const ResetViewsButton = ({ selectedIds }) => {
    const refresh = useRefresh();
    const notify = useNotify();
    const unselectAll = useUnselectAll();
    const [updateMany, { loading }] = useUpdateMany(
        'posts',
        selectedIds,
        { views: 0 },
        {
            onSuccess: () => {
                refresh();
                notify('Posts updated');
                unselectAll('posts');
            },
            onFailure: error => notify('Error: posts not updated', 'warning'),
        }
    );

    return (
        <Button
            label="simple.action.resetViews"
            disabled={loading}
            onClick={updateMany}
        >
            <VisibilityOff />
        </Button>
    );
};

export default ResetViewsButton;

But most of the time, bulk actions are mini-applications with a standalone user interface (in a Dialog). Here is the same ResetViewsAction implemented behind a confirmation dialog:

// in ./ResetViewsButton.js
import React, { Fragment, useState } from 'react';
import {
    Button,
    Confirm,
    useUpdateMany,
    useRefresh,
    useNotify,
    useUnselectAll,
} from 'react-admin';

const ResetViewsButton = ({ selectedIds }) => {
    const [open, setOpen] = useState(false);
    const refresh = useRefresh();
    const notify = useNotify();
    const unselectAll = useUnselectAll();
    const [updateMany, { loading }] = useUpdateMany(
        'posts',
        selectedIds,
        { views: 0 },
        {
            onSuccess: () => {
                refresh();
                notify('Posts updated');
                unselectAll('posts');
            },
            onFailure: error => notify('Error: posts not updated', 'warning'),
        }
    );
    const handleClick = () => setOpen(true);
    const handleDialogClose = () => setOpen(false);

    const handleConfirm = () => {
        updateMany();
        setOpen(false);
    };

    return (
        <Fragment>
            <Button label="Reset Views" onClick={handleClick} />
            <Confirm
                isOpen={open}
                loading={loading}
                title="Update View Count"
                content="Are you sure you want to reset the views for these items?"
                onConfirm={handleConfirm}
                onClose={handleDialogClose}
            />
        </Fragment>
    );
}

export default ResetViewsButton;

Tip: <Confirm> leverages material-ui’s <Dialog> component to implement a confirmation popup. Feel free to use it in your admins!

Tip: <Confirm> text props such as title and content are translatable. You can pass use translation keys in these props.

Tip: You can customize the text of the two <Confirm> component buttons using the cancel and confirm props which accept translation keys. You can customize the icons by setting the ConfirmIcon and CancelIcon props, which accept a SvgIcon type.

Tip: React-admin doesn’t use the <Confirm> component internally, because deletes and updates are applied locally immediately, then dispatched to the server after a few seconds, unless the user chooses to undo the modification. That’s what we call optimistic rendering. You can do the same for the ResetViewsButton by setting undoable: true in the last argument of useUpdateMany(), as follows:

// in ./ResetViewsButton.js
import React from 'react';
import {
    Button,
    Confirm,
    useUpdateMany,
    useRefresh,
    useNotify,
    useUnselectAll,
} from 'react-admin';
import { VisibilityOff } from '@material-ui/icons';

const ResetViewsButton = ({ selectedIds }) => {
    const refresh = useRefresh();
    const notify = useNotify();
    const unselectAll = useUnselectAll();
    const [updateMany, { loading }] = useUpdateMany(
        'posts',
        selectedIds,
        { views: 0 },
        {
            onSuccess: () => {
                refresh();
-               notify('Posts updated');
+               notify('Posts updated', 'info', '{}, true); // the last argument forces the display of 'undo' in the notification
                unselectAll('posts');
            },
            onFailure: error => notify('Error: posts not updated', 'warning'),
+           undoable: true
        }
    );

    return (
        <Button
            label="simple.action.resetViews"
            disabled={loading}
            onClick={updateMany}
        >
            <VisibilityOff />
        </Button>
    );
};

Filters

You can add a filter component to the list using the filters prop:

const PostFilter = (props) => (
    <Filter {...props}>
        <TextInput label="Search" source="q" alwaysOn />
        <TextInput label="Title" source="title" defaultValue="Hello, World!" />
    </Filter>
);

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

The filter component must be a <Filter> with <Input> children.

Tip: Don’t mix up this filters prop, expecting a React element, with the filter props, which expects an object to define permanent filters (see below).

The Filter component accepts the usual className prop but you can override many class names injected to the inner components by React-admin thanks to the classes property (as most Material UI components, see their documentation about it). This property accepts the following keys:

  • form: applied to the root element when rendering as a form.
  • button: applied to the root element when rendering as a button.

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

For more details about the filters prop, see the Filtering the List secion below.

Records Per Page

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 = (props) => (
    <List {...props} perPage={25}>
        ...
    </List>
);

Default Sort Field

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

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

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

Disabling Sorting

It is possible to disable sorting for a specific field by passing a sortable property set to false:

// in src/posts.js
import React from 'react';
import { List, Datagrid, TextField } from 'react-admin';

export const PostList = (props) => (
    <List {...props}>
        <Datagrid>
            <TextField source="id" sortable={false} />
            <TextField source="title" />
            <TextField source="body" />
        </Datagrid>
    </List>
);

Specify Sort Field

By default, a column is sorted by the source property. To define another attribute to sort by, set it via the sortBy property:

// in src/posts.js
import React from 'react';
import { List, Datagrid, TextField } from 'react-admin';

export const PostList = (props) => (
    <List {...props}>
        <Datagrid>
            <ReferenceField label="Post" source="id" reference="posts" sortBy="title">
                <TextField source="title" />
            </ReferenceField>
            <FunctionField
                label="Author"
                sortBy="last_name"
                render={record => `${record.author.first_name} ${record.author.last_name}`}
            />
            <TextField source="body" />
        </Datagrid>
    </List>
);

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 REST client in the filter props:

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

The actual filter parameter sent to the REST client 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.

Filter Default Values

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 PostFilter = (props) => (
    <Filter {...props}>
        <TextInput label="Search" source="q" alwaysOn />
        <BooleanInput source="is_published" alwaysOn />
        <TextInput source="title" defaultValue="Hello, World!" />
    </Filter>
);

export const PostList = (props) => (
    <List {...props} filters={<PostFilter />} 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, ...filters };

Aside component

You may want to display additional information on the side of the list. Use the aside prop for that, passing the component of your choice:

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

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

The aside component receives the same props as the List child component, including the following:

  • basePath,
  • currentSort,
  • data,
  • defaultTitle,
  • filterValues,
  • ids,
  • page,
  • perPage,
  • resource,
  • selectedIds,
  • total,
  • version,

That means you can display additional details of the current list in the aside component:

const Aside = ({ data, ids }) => (
    <div style={{ width: 200, margin: '1em' }}>
        <Typography variant="h6">Posts stats</Typography>
        <Typography variant="body2">
            Total views: {ids.map(id => data[id]).reduce((sum, post) => sum + post.views, 0)}
        </Typography>
    </div>
);

Empty page

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.

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

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

const Empty = ({ basePath, resource }) => (
    <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 basePath={basePath} />
        <Button onClick={...}>Import</Button>
    </Box>
);

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

The empty component receives the same props as the List child component, including the following:

  • basePath,
  • currentSort,
  • data,
  • defaultTitle,
  • filterValues,
  • ids,
  • page,
  • perPage,
  • resource,
  • selectedIds,
  • total,
  • version,

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 layouts also use 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 = props => (
    <List component="div" {...props}>
        ...
    </List>
);

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

The default value for the component prop is Card.

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 classes property (as most Material UI components, see their documentation about it). This property accepts the following keys:

  • root: alternative to using className. Applied to the root element.
  • header: applied to the page header
  • actions: applied to the actions container
  • noResults: applied to the component shown when there is no result

Here is an example of how you can override some of these classes:

You can customize the list styles by passing a classes object as prop, through useStyles(). Here is an example:

import { makeStyles } from '@material-ui/core';

const useStyles = makeStyles({
    header: {
        backgroundColor: '#ccc',
    },
});

const PostList = props => {
    const classes = useStyles();
    return (
        <List {...props} classes={{ header: classes.header }}>
            <Datagrid>
                ...
            </Datagrid>
        </List>
    );
}

export PostList;

Filtering The List

One of the most important features of the List page is the ability to filter the results. React-admin does its best to offer a powerful feature functionality, and to get out of the way when you want to go further.

List Filters

The next sections explain hows to use the filter functionality:

Filter Query Parameter

React-admin uses the filter query parameter from the URL to determine the filters to apply to the list. To change the filters, react-admin simply changes this filter query parameter, and the <List> components fetches dataProvider.getList() again with the new filters.

Here is a typical List URL:

https://myadmin.dev/#/posts?displayedFilters=%7B%22commentable%22%3Atrue%7D&filter=%7B%22commentable%22%3Atrue%2C%22q%22%3A%22lorem%20%22%7D&order=DESC&page=1&perPage=10&sort=published_at

Once decoded, the filter query parameter reveals as a JSON value:

filter={"commentable":true,"q":"lorem "}

You can change the filters by updating the query parameter, e.g. using the <Link> component or the history.push() method from react-router.

The filter Prop

To facilitate the display and update of the filter, the <List> component exposes a filters prop.

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

<List> clones the component passed as filters twice:

  • once with with the prop context="form", to render the filter form
  • once with with the prop context="button", to render the filter button

The component passed as filters should know how to render differently according to the context prop.

That’s the case of the react-admin <Filter> component:

  • <Filter context="form"> renders an inline form based on its children which must be <Input> components
  • <Filter context="button"> renders an dropdown allowing to enable filters based on the source prop of its children.
const PostFilter = (props) => (
    <Filter {...props}>
        <TextInput label="Search" source="q" alwaysOn />
        <TextInput label="Title" source="title" defaultValue="Hello, World!" />
    </Filter>
);

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

Tip: For technical reasons, react-admin does not accept children of <Filter> having both a defaultValue and alwaysOn. To set default values for always on filters, use the filterDefaultValues prop of the component instead.

Tip: Once a user sets a filter, react-admin persists the filter value in the application state, so that when the user comes back to the list, they should see the filtered list. That’s a design choice.

Linking To A Pre-Filtered List

As the filter values are taken from the URL, you can link to a pre-filtered list by setting the query parameter.

For instance, if you have a list of tags, you can display a button for each category to link to the list of posts filtered by that tag:

import React from 'react';
import Button from '@material-ui/core/Button';
import { Link } from 'react-router-dom';
import { stringify } from 'query-string';

const LinkToRelatedProducts = ({ record }) => {
    const translate = useTranslate();
    return record ? (
        <Button
            color="primary"
            component={Link}
            to={{
                pathname: '/posts',
                search: stringify({
                    page: 1,
                    perPage: 25,
                    sort: 'id',
                    order: 'DESC',
                    filter: JSON.stringify({ category_id: record.id }),
                }),
            }}
        >
            All posts with the category {record.name} ; 
        </Button>
    ) : null;
};

You can use this button e.g. as a child of <Datagrid>. You can also create a custom Menu button with that technique to link to the unfiltered list by setting the filter value to {}.

Tip: You have to pass all the query string parameters - not just filter. That’s a current limitation of react-admin.

In addition to the usual input types (<TextInput>, <SelectInput>, <ReferenceInput>, etc.), you can use the <SearchInput>, which is designed especially for the filter form. It’s like a <TextInput resettable> with a magnifier glass icon - exactly the type of input users look for when they want to do a full-text search.

`<SearchInput>`

import { Filter, SearchInput, TextInput } from 'react-admin';

const PostFilter = props => (
    <Filter {...props}>
        <SearchInput source="q" alwaysOn />
    </Filter>
);

In the example given above, the q filter triggers a full-text search on all fields. It’s your responsibility to implement the full-text filtering capabilities in your dataProvider, or in your API.

Quick Filters

Users usually dislike using their keyboard to filter a list (especially on mobile). A good way satisfy thus user requirement is to turn filters into quick filter. A Quick filter is a filter with a non-editable defaultValue. Users can only enable or disable them.

`<QuickFilter>`

import { Filter, SearchInput } from 'react-admin';
import { makeStyles, Chip } from '@material-ui/core';

const useQuickFilterStyles = makeStyles(theme => ({
    chip: {
        marginBottom: theme.spacing(1),
    },
}));
const QuickFilter = ({ label }) => {
    const translate = useTranslate();
    const classes = useQuickFilterStyles();
    return <Chip className={classes.chip} label={translate(label)} />;
};

const PostFilter = props => (
    <Filter {...props}>
        <SearchInput source="q" alwaysOn />
        <QuickFilter source="commentable" label="Commentable" defaultValue={true} />
        <QuickFilter source="views_lte" label="Low views" defaultValue={150} />
        <QuickFilter source="tags" label="Tagged Code" defaultValue={[3]} />
    </Filter>
);

Tip: It’s currently not possible to use two quick filters for the same source.

Filter On Submit

By default, the filter form doesn’t provide a submit button, and submits automatically after the user has finished interacting with the form. This provides a smooth user experience, but for some APIs, it can cause too many calls. In that case, the solution is to add a submit button to the filter form.

Filters with submit button

React-admin doesn’t provide any component for that, but it’s a good opportunity to illustrate the internals of the <Filter> component. We’ll actually provide an alternative implementation to <Filter>.

As explained earlier, <List> clones the filters element twice. It passes special props to the element to let it interact with the URI query parameter more easily:

  • filterValues: Value of the filters based on the URI, e.g. {"commentable":true,"q":"lorem "}
  • setFilters(): Callback to set the filter values, e.g. setFilters({"commentable":true})
  • displayedFilters: Names of the filters displayed in the form, e.g. ['commentable','title']
  • showFilter(): Callback to display an additional filter in the form, e.g. showFilter('views')
  • hideFilter(): Callback to hide a filter in the form, e.g. hideFilter('title')

First, let’s create a Filter component that renders either a button or a form depending on the context:

const PostFilter = props => {
  return props.context === "button" ? (
    <PostFilterButton {...props} />
  ) : (
    <PostFilterForm {...props} />
  );
};

The <PostListFilterButton> will simply show the filter form on click. We’ll take advantage of the showFilter prop injected to the Filter component:

import { Button } from "@material-ui/core";
import ContentFilter from "@material-ui/icons/FilterList";

const PostFilterButton = ({ showFilter }) => (
  <Button
    size="small"
    color="primary"
    onClick={() => showFilter("main")}
    startIcon={<ContentFilter />}
  >
    Filter
  </Button>
);

Normally, showFilter() adds one input to the displayedFilters list. As the filter form will be entirely hidden or shown, we use showFilter() with a virtual “main” input, which represents the entire form.

Next is the form component, based on react-final-form. The form inputs appear directly in the form, and the form submission triggers the setFilters() callback passed as parameter:

import React from "react";
import { Form } from "react-final-form";
import { Box, Button, InputAdornment } from "@material-ui/core";
import SearchIcon from "@material-ui/icons/Search";
import { TextInput, NullableBooleanInput } from "react-admin";

const PostFilterForm = ({
  displayedFilters,
  filterValues,
  setFilters,
  hideFilter,
  open
}) => {
  if (!displayedFilters.main) return null;

  const onSubmit = values => {
    if (Object.keys(values).length > 0) {
      setFilters(values);
    } else {
      hideFilter("main");
    }
  };

  const resetFilter = () => {
    setFilters({}, []);
  };

return (
    <div>
      <Form onSubmit={onSubmit} initialValues={filterValues}>
        {({ handleSubmit }) => (
          <form onSubmit={handleSubmit}>
            <Box mt={8} />
            <Box display="flex" alignItems="flex-end" mb={1}>
              <Box component="span" mr={2}>
                {/* Full-text search filter. We don't use <SearchFilter> to force a large form input */}
                <TextInput
                  resettable
                  helperText={false}
                  source="q"
                  label="Search"
                  InputProps=
                />
              </Box>
              <Box component="span" mr={2}>
                {/* Commentable filter */}
                <NullableBooleanInput helperText={false} source="commentable" />
              </Box>
              <Box component="span" mr={2} mb={1.5}>
                <Button variant="outlined" color="primary" type="submit">
                  Filter
                </Button>
              </Box>
              <Box component="span" mb={1.5}>
                <Button variant="outlined" onClick={resetFilter}>
                  Close
                </Button>
              </Box>
            </Box>
          </form>
        )}
      </Form>
    </div>
  );
};

To finish, we pass the <PostFilter> component to the <List> component using the filters prop:

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

You can use a similar approach to customize the list filter completely, e.g. to display the filters in a sidebar, or as a line in the datagrid, etc.

The <ListGuesser> component

Instead of a custom List, you can use the ListGuesser to determine which fields to use based on the data returned by the API.

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

const App = () => (
    <Admin dataProvider={jsonServerProvider('https://jsonplaceholder.typicode.com')}>
        <Resource name="posts" 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, where you can copy it into your own code. Use this feature to quickly bootstrap a List on top of an existing API, without adding the fields one by one.

Guessed List

React-admin provides guessers for the List view (ListGuesser), the Edit view (EditGuesser), and the Show view (ShowGuesser).

Tip: Do not use the guessers in production. They are slower than manually-defined components, because they have to infer types based on the content. Besides, the guesses are not always perfect.

useListController

The <List> components takes care of two things:

  1. (the “controller”) Fetching data based on the URL and transforming it
  2. (the “view”) Rendering the page title, the actions, the content and aside areas

In some cases, you may want to customize the view entirely (i.e. keep the code for step 1, and provide your own code for step 2). For these cases, react-admin provides a hook called useListController(), which contain just the controller part of the <List>.

This hook takes one object as input (the props passed to a <List> component) and returns the fetched data and callbacks for the List view. In fact, it returns a lot of variables because the List page is complex: based on the URL, the List controller deduces filters, pagination, ordering, it provides callbacks to update them, it fetches the data, etc.

You can use useListController() to create your own custom List view, like this one:

import { 
    useListController,
    ListToolbar,
    BulkActionsToolbar,
    Pagination,
    Datagrid
} from 'react-admin';

const MyList = props => {
    const controllerProps = useListController(props);
    const {
        // fetched data
        data, // an id-based dictionary of the list data, e.g. { 123: { id: 123, title: 'hello world' }, 456: { ... } }
        ids, // an array listing the ids of the records in the list, e.g [123, 456, ...]
        total, // the total number of results for the current filters, excluding pagination. Useful to build the pagination controls. e.g. 23 
        loaded, // boolean that is false until the data is available
        loading, // boolean that is true on mount, and false once the data was fetched
        // pagination
        page, // the current page. Starts at 1
        perPage, // the number of results per page. Defaults to 25
        setPage, // a callback to change the page, e.g. setPage(3)
        setPerPage, // a callback to change the number of results per page, e.g. setPerPage(25)
        // sorting
        currentSort, // a sort object { field, order }, e.g. { field: 'date', order: 'DESC' } 
        setSort, // a callback to change the sort, e.g. setSort('name', 'ASC')
        // filtering
        displayedFilters, // a dictionary of the displayed filters, e.g. { title: true, nationality: true }
        filterValues, // a dictionary of filter values, e.g. { title: 'lorem', nationality: 'fr' }
        setFilters, // a callback to update the filters, e.g. setFilters(filters, displayedFilters)
        showFilter, // a callback to show one of the filters, e.g. showFilter('title', defaultValue)
        hideFilter, // a callback to hide one of the filters, e.g. hidefilter('title')
        // row selection
        selectedIds, // an array listing the ids of the selcted rows, e.g. [123, 456]
        onSelect, // callback to change the list of selected rows, e.g onSelect([456, 789])
        onToggleItem, // callback to toggle the selection of a given record based on its id, e.g. onToggleItem(456)
        onUnselectItems, // callback to clear the selection, e.g. onUnselectItems();
        // misc
        basePath, // deduced from the location, useful for action buttons
        defaultTitle, // the translated title based on the resource, e.g. 'Posts'
        resource, // the resource name, deduced from the location. e.g. 'posts'
        version, // integer used by the refresh feature
    } = controllerProps;
    return (
        <div>
            <h1>{defaultTitle}</h1>
            <ListToolbar
                    filters={props.filters}
                    {...controllerProps}
                    actions={props.actions}
                    permanentFilter={props.filter}
                />
            <BulkActionsToolbar {...controllerProps}>
                {props.bulkActionButtons}
            </BulkActionsToolbar>
            {cloneElement(children, {
                ...controllerProps,
                hasBulkActions: props.bulkActionButtons !== false,
            })}
            <Pagination {...controllerProps} />
        </div>
    );
}

const PostList = props => (
    <MyList {...props}>
        <Datagrid>
            ...
        </Datagrid>
    </MyList>
)

This custom List view has no aside component - it’s up to you to add it in pure React.

Tip: You don’t have to clone the child element. If you can’t reuse an existing list view component like <Datagrid> or <SimpleList>, feel free to write the form code inside your custom MyList component.

The <Datagrid> component

The Datagrid component renders a list of records as a table. It is usually used as a child of the <List> and <ReferenceManyField> components.

Here are all the props accepted by the component:

It renders as many columns as it receives <Field> children.

// in src/posts.js
import React from 'react';
import { List, Datagrid, TextField, EditButton } from 'react-admin';

export const PostList = (props) => (
    <List {...props}>
        <Datagrid>
            <TextField source="id" />
            <TextField source="title" />
            <TextField source="body" />
            <EditButton />
        </Datagrid>
    </List>
);

The Datagrid is an iterator component: it receives an array of ids, and a data store, and is supposed to iterate over the ids to display each record. Another example of iterator component is <SingleFieldList>.

Body element

By default, <Datagrid> renders its body using <DatagridBody>, an internal react-admin component. You can pass a custom component as the row prop to override that default. And by the way, <DatagridBody> has a row property set to <DatagridRow> by default for the same purpose. <DatagridRow> receives the row record, the resource, and a copy of the <Datagrid> children. That means you can create custom datagrid logic without copying several components from the react-admin source.

For instance, to show the selection checkbox only for records that have a selectable field set to true, you can override <DatagridRow> and <DatagridBody> as follows:

// in src/PostList.js
import React from 'react';
import { Datagrid, DatagridBody, List, TextField } from 'react-admin';
import TableCell from '@material-ui/core/TableCell';
import TableRow from '@material-ui/core/TableRow';
import Checkbox from '@material-ui/core/Checkbox';

const MyDatagridRow = ({ record, resource, id, onToggleItem, children, selected, basePath }) => (
    <TableRow key={id}>
        {/* first column: selection checkbox */}
        <TableCell padding="none">
            {record.selectable && <Checkbox
                checked={selected}
                onClick={() => onToggleItem(id)}
            />}
        </TableCell>
        {/* data columns based on children */}
        {React.Children.map(children, field => (
            <TableCell key={`${id}-${field.props.source}`}>
                {React.cloneElement(field, {
                    record,
                    basePath,
                    resource,
                })}
            </TableCell>
        ))}
    </TableRow>
);

const MyDatagridBody = props => <DatagridBody {...props} row={<MyDatagridRow />} />;
const MyDatagrid = props => <Datagrid {...props} body={<MyDatagridBody />} />;

const PostList = props => (
    <List {...props}>
        <MyDatagrid>
            <TextField source="title" />
            ...
        </MyDatagrid>
    </List>
)

export default PostList;

Row Style Function

You can customize the <Datagrid> row style (applied to the <tr> element) based on the record, thanks to the rowStyle prop, which expects a function.

For instance, this allows to apply a custom background to the entire row if one value of the record - like its number of views - passes a certain threshold.

const postRowStyle = (record, index) => ({
    backgroundColor: record.nb_views >= 500 ? '#efe' : 'white',
});
export const PostList = (props) => (
    <List {...props}>
        <Datagrid rowStyle={postRowStyle}>
            ...
        </Datagrid>
    </List>
);

rowClick

You can catch clicks on rows to redirect to the show or edit view by setting the rowClick prop:

export const PostList = (props) => (
    <List {...props}>
        <Datagrid rowClick="edit">
            ...
        </Datagrid>
    </List>
);

rowClick accepts the following values:

  • “edit” to redirect to the edition vue
  • “show” to redirect to the show vue
  • “expand” to open the expand panel
  • “toggleSelection” to trigger the onToggleItem function
  • a function (id, basePath, record) => path to redirect to a custom path

Tip: If you pass a function, it can return edit, show or a router path. This allows to redirect to either edit or show after checking a condition on the record. For example:

const postRowClick = (id, basePath, record) => record.editable ? 'edit' : 'show';

Tip: If you pass a function, it can also return a promise allowing you to check an external API before returning a path. For example:

import fetchUserRights from './fetchUserRights';

const postRowClick = (id, basePath, record) => fetchUserRights().then(({ canEdit }) => canEdit ? 'edit' : 'show');

expand

To show more data from the resource without adding too many columns, you can show data in an expandable panel below the row on demand, using the expand prop. For instance, this code shows the body of a post in an expandable panel:

const PostPanel = ({ id, record, resource }) => (
    <div dangerouslySetInnerHTML={{ __html: record.body }} />
);

const PostList = props => (
    <List {...props}>
        <Datagrid expand={<PostPanel />}>
            <TextField source="id" />
            <TextField source="title" />
            <DateField source="published_at" />
            <BooleanField source="commentable" />
            <EditButton />
        </Datagrid>
    </List>
)

isRowSelectable

You can customize which rows will show a selection checkbox using the isRowSelectable prop. It expects a function that will receive the record of each <DatagridRow> and returns a boolean expression. For instance, this code shows a checkbox only for rows with an id greater than 300:

export const PostList = props => (
    <List {...props}>
        <Datagrid isRowSelectable={ record => record.id > 300 }>
            ...
        </Datagrid>
    </List>
);

expandable panel

The expand prop expects an component as value. When the user chooses to expand the row, the Datagrid render the component, and passes the current record, id, and resource.

Tip: Since the expand element receives the same props as a detail view, you can actually use a <Show> view as component for the expand prop:

const PostShow = props => (
    <Show
        {...props}
        /* disable the app title change when shown */
        title=" "
    >
        <SimpleShowLayout>
            <RichTextField source="body" />
        </SimpleShowLayout>
    </Show>
);

const PostList = props => (
    <List {...props}>
        <Datagrid expand={<PostShow />}>
            <TextField source="id" />
            <TextField source="title" />
            <DateField source="published_at" />
            <BooleanField source="commentable" />
            <EditButton />
        </Datagrid>
    </List>
)

The result will be the same as in the previous snippet, except that <Show> encloses the content inside a material-ui <Card>.

Tip: You can go one step further and use an <Edit> view as expand component, albeit with a twist:

const PostEdit = props => (
    <Edit
        {...props}
        /* disable the app title change when shown */
        title=" "
    >
        <SimpleForm
            /* The form must have a name dependent on the record, because by default all forms have the same name */
            form={`post_edit_${props.id}`}
        >
            <RichTextInput source="body" />
        </SimpleForm>
    </Edit>
);

const PostList = props => (
    <List {...props}>
        <Datagrid expand={<PostEdit />}>
            <TextField source="id" />
            <TextField source="title" />
            <DateField source="published_at" />
            <BooleanField source="commentable" />
            <EditButton />
        </Datagrid>
    </List>
)

CSS API

The Datagrid component accepts the usual className prop but you can override many class names injected to the inner components by React-admin thanks to the classes property (as most Material UI components, see their documentation about it). This property accepts the following keys:

  • table: alternative to using className. Applied to the root element.
  • tbody: applied to the tbody
  • headerCell: applied to each header cell
  • row: applied to each row
  • rowEven: applied to each even row
  • rowOdd: applied to each odd row
  • rowCell: applied to each row cell

Here is an example of how you can override some of these classes:

You can customize the <Datagrid> styles by passing a classes object as prop, through useStyles(). Here is an example:

import React from 'react';
import { makeStyles } from '@material-ui/core';

const useStyles = makeStyles({
    row: {
        backgroundColor: '#ccc',
    },
});

const PostList = props => {
    const classes = useStyles();
    return (
        <List {...props}>
            <Datagrid classes={{ row: classes.row }}>
                ...
            </Datagrid>
        </List>
    );
}

export PostList;

Tip: If you want to override the header and cell styles independently for each column, use the headerClassName and cellClassName props in <Field> components. For instance, to hide a certain column on small screens:

import React from 'react';
import { makeStyles } from '@material-ui/core';

const useStyles = makeStyles(theme => ({
    hiddenOnSmallScreens: {
        [theme.breakpoints.down('md')]: {
            display: 'none',
        },
    },
}));

const PostList = props => {
    const classes = useStyles();
    return (
        <List {...props}>
            <Datagrid>
                <TextField source="id" />
                <TextField source="title" />
                <TextField
                    source="views"
                    headerClassName={classes.hiddenOnSmallScreens}
                    cellClassName={classes.hiddenOnSmallScreens}
                />
            </Datagrid>
        </List>
    );
};

export default PostList;

This feature has a limit, though. Datagrid inspects its children for headerClassName and cellClassName props. This means you can’t use these props in a wrapped component:

const useStyles = makeStyles({
    priceCell: { backgroundColor: 'blue' },
});

const PriceField = props => {
    const classes = useStyles();
    return <TextField cellClassName={classes.priceCell} {...props} />;
};

// the cell class name won't be applied here because Datagrid doesn't see it in its children
export const ProductList = (props) => (
    <List {...props}>
        <Datagrid>
            <PriceField source="price" />
        </Datagrid>
    </List>
);

For this kind of use case, you need to use a custom datagrid body component.

Performance

when displaying large pages of data, you might experience some performance issues. This is mostly due to the fact that we iterate over the <Datagrid> children and clone them.

In such cases, you can opt-in for an optimized version of the <Datagrid> by setting its optimized prop to true. Be aware that you can’t have dynamic children, such as those displayed or hidden by checking permissions, when using this mode.

const PostList = props => (
    <List {...props}>
        <Datagrid optimized>
            <TextField source="id" />
            <TextField source="title" />
            <TextField source="views" />
        </Datagrid>
    </List>
);

export default withStyles(styles)(PostList);

Tip: You can use the Datagrid component with custom queries:

import keyBy from 'lodash/keyBy'
import { useQuery, Datagrid, TextField, Pagination, Loading } from 'react-admin'

const CustomList = () => {
    const [page, setPage] = useState(1);
    const perPage = 50;
    const { data, total, loading, error } = useQuery({
        type: 'GET_LIST'
        resource: 'posts'
        payload: {
            pagination: { page, perPage },
            sort: { field: 'id', order: 'ASC' },
            filter: {},
        }
    });

    if (loading) {
        return <Loading />
    }
    if (error) {
        return <p>ERROR: {error}</p>
    }
    return (
        <>
            <Datagrid
                data={keyBy(data, 'id')}
                ids={data.map(({ id }) => id)}
                currentSort={{ field: 'id', order: 'ASC' }}
                basePath="/posts" // required only if you set use "rowClick"
                rowClick="edit"
            >
                <TextField source="id" />
                <TextField source="name" />
            </Datagrid>
            <Pagination
                page={page}
                perPage={perPage}
                setPage={setPage}
                total={total}
            />
        </>
    )
}

The <SimpleList> component

For mobile devices, a <Datagrid> is often unusable - there is simply not enough space to display several columns. The convention in that case is to use a simple list, with only one column per row. The <SimpleList> component serves that purpose, leveraging material-ui’s <List> and <ListItem> components. You can use it as <List> or <ReferenceManyField> child:

// in src/posts.js
import React from 'react';
import { List, SimpleList } from 'react-admin';

export const PostList = (props) => (
    <List {...props}>
        <SimpleList
            primaryText={record => record.title}
            secondaryText={record => `${record.views} views`}
            tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
            linkType={record => record.canEdit ? "edit" : "show"}
        />
    </List>
);

<SimpleList> iterates over the list data. For each record, it executes the primaryText, secondaryText, linkType, leftAvatar, leftIcon, rightAvatar, and rightIcon props function, and passes the result as the corresponding <ListItem> prop.

Tip: To use a <SimpleList> on small screens and a <Datagrid> on larger screens, use material-ui’s useMediaQuery hook:

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

export const PostList = (props) => {
    const isSmall = useMediaQuery(theme => theme.breakpoints.down('sm'));
    return (
        <List {...props}>
            {isSmall ? (
                <SimpleList
                    primaryText={record => record.title}
                    secondaryText={record => `${record.views} views`}
                    tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
                    linkType={record => record.canEdit ? "edit" : "show"}   
                />
            ) : (
                <Datagrid>
                    ...
                </Datagrid>
            )}
        </List>
    );
}

Tip: The <SimpleList> items link to the edition page by default. You can also set the linkType prop to show directly to link to the <Show> page instead.

// in src/posts.js
import React from 'react';
import { List, SimpleList } from 'react-admin';

export const PostList = (props) => (
    <List {...props}>
        <SimpleList
            primaryText={record => record.title}
            secondaryText={record => `${record.views} views`}
            tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
            linkType="show"
        />
    </List>
);

Setting the linkType prop to false (boolean, not string) removes the link in all list items.

The <SingleFieldList> component

When you want to display only one property of a list of records, instead of using a <Datagrid>, use the <SingleFieldList>. It expects a single <Field> as child. It’s especially useful for <ReferenceManyField> or <ReferenceArrayField> components:

// Display all the tags for the current post
<ReferenceArrayField
    label="Tags"
    reference="tags"
    source="tags"
>
    <SingleFieldList>
        <ChipField source="name" />
    </SingleFieldList>
</ReferenceArrayField>

ReferenceManyFieldSingleFieldList

Tip: The <SingleFieldList> items link to the edition page by default. You can set the linkType prop to show to link to the <Show> page instead.

// Display all the tags for the current post
<ReferenceArrayField
    label="Tags"
    reference="tags"
    source="tags"
>
    <SingleFieldList linkType="show">
        <ChipField source="name" />
    </SingleFieldList>
</ReferenceArrayField>

Using a Custom Iterator

A <List> can delegate to any iterator component - <Datagrid> is just one example. An iterator component must accept at least two props:

  • ids is an array of the ids currently displayed in the list
  • data is an object of all the fetched data for this resource, indexed by id.

For instance, what if you prefer to show a list of cards rather than a datagrid?

Custom iterator

You’ll need to create your own iterator component as follows:

// in src/comments.js
import React from 'react';
import Card from '@material-ui/core/Card';
import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent';
import CardHeader from '@material-ui/core/CardHeader';
import Avatar from '@material-ui/core/Avatar';
import PersonIcon from '@material-ui/core/Avatar';
import { List, TextField, DateField, ReferenceField, EditButton } from "react-admin";

const cardStyle = {
    width: 300,
    minHeight: 300,
    margin: '0.5em',
    display: 'inline-block',
    verticalAlign: 'top'
};
const CommentGrid = ({ ids, data, basePath }) => (
    <div style={{ margin: '1em' }}>
    {ids.map(id =>
        <Card key={id} style={cardStyle}>
            <CardHeader
                title={<TextField record={data[id]} source="author.name" />}
                subheader={<DateField record={data[id]} source="created_at" />}
                avatar={<Avatar icon={<PersonIcon />} />}
            />
            <CardContent>
                <TextField record={data[id]} source="body" />
            </CardContent>
            <CardContent>
                about&nbsp;
                <ReferenceField label="Post" resource="comments" record={data[id]} source="post_id" reference="posts" basePath={basePath}>
                    <TextField source="title" />
                </ReferenceField>
            </CardContent>
            <CardActions style={{ textAlign: 'right' }}>
                <EditButton resource="posts" basePath={basePath} record={data[id]} />
            </CardActions>
        </Card>
    )}
    </div>
);
CommentGrid.defaultProps = {
    data: {},
    ids: [],
};

export const CommentList = (props) => (
    <List title="All comments" {...props}>
        <CommentGrid />
    </List>
);

As you can see, nothing prevents you from using <Field> components inside your own components… provided you inject the current record. Also, notice that components building links require the basePath component, which is also injected.

Displaying Fields depending on the user permissions

You might want to display some fields or filters only to users with specific permissions.

Before rendering the List, react-admin calls the authProvider.getPermissions() method, and passes the result to the component as the permissions prop. It’s up to your authProvider to return whatever you need to check roles and permissions inside your component.

const UserFilter = ({ permissions, ...props }) =>
    <Filter {...props}>
        <TextInput
            label="user.list.search"
            source="q"
            alwaysOn
        />
        <TextInput source="name" />
        {permissions === 'admin' ? <TextInput source="role" /> : null}
    </Filter>;

export const UserList = ({ permissions, ...props }) => {
    const isSmall = useMediaQuery(theme => theme.breakpoints.down('sm'));
    return (
        <List
            {...props}
            filters={<UserFilter permissions={permissions} {...props} />}
            sort={{ field: 'name', order: 'ASC' }}
        >
            {isSmall ? (
                <SimpleList
                    primaryText={record => record.name}
                    secondaryText={record =>
                        permissions === 'admin' ? record.role : null}
                />
            ): (
                <Datagrid>
                    <TextField source="id" />
                    <TextField source="name" />
                    {permissions === 'admin' && <TextField source="role" />}
                    {permissions === 'admin' && <EditButton />}
                    <ShowButton />
                </Datagrid>
            )}
        </List>;
    )
}

Tip: Note how the permissions prop is passed down to the custom filters component.

Pagination

Here are all the props required by the component:

  • page: The current page number (integer). First page is 1.
  • perPage: The number of records per page.
  • setPage: function(page: number) => void. A function that set the current page number.
  • total: The total number of records.
  • actions: A component that displays the pagination buttons (default: PaginationActions)
  • limit: An element that is displayed if there is no data to show (default: <PaginationLimit>)

You don’t need to fill these props when you pass the Pagination component to the List component through the pagination prop: <List pagination={<Pagination />}>.

You can also replace the default pagination element by your own. For instance, you can modify the default pagination by adjusting the “rows per page” selector.

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

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

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

Tip: Pass an empty array to rowsPerPageOptions to disable the rows per page selection.

Alternately, if you want to replace the default pagination by a “<previous - next>” pagination, create a pagination component like the following:

import Button from '@material-ui/core/Button';
import ChevronLeft from '@material-ui/icons/ChevronLeft';
import ChevronRight from '@material-ui/icons/ChevronRight';
import Toolbar from '@material-ui/core/Toolbar';

const PostPagination = ({ page, perPage, total, setPage }) => {
    const nbPages = Math.ceil(total / perPage) || 1;
    return (
        nbPages > 1 &&
            <Toolbar>
                {page > 1 &&
                    <Button color="primary" key="prev" icon={ChevronLeft} onClick={() => setPage(page - 1)}>
                        Prev
                    </Button>
                }
                {page !== nbPages &&
                    <Button color="primary" key="next" icon={ChevronRight} onClick={() => setPage(page + 1)} labelPosition="before">
                        Next
                    </Button>
                }
            </Toolbar>
    );
}

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

But if you just want to change the color property of the pagination button, you can extend the existing components:

import {
    List,
    Pagination as RaPagination,
    PaginationActions as RaPaginationActions,
} from 'react-admin';

export const PaginationActions = props => <RaPaginationActions {...props} color="secondary" />;

export const Pagination = props => <RaPagination {...props} ActionsComponent={PaginationActions} />;

export const UserList = props => (
    <List {...props} pagination={<Pagination />}>
    </List>
);