React Admin October 2020 Update

François Zaninotto
François ZaninottoOctober 07, 2020
#react#react-admin

React-admin, the open-source frontend Framework for building B2B applications running in the browser on top of REST/GraphQL APIs, received a lot of new features during the past quarter. These features will help you build more sophisticated admins, with improved user experience for complex tasks, and better developer experience for you. Let's dive in!

TypeScript Support

This is probably the most requested and the most visible feature: starting with version 3.9, react-admin exports TypeScript types.

Let's first reiterate one important fact: react-admin is and will remain a JavaScript library. You can still use react-admin without TypeScript. In fact, react-admin 3.9 exports an additional set of files that contain TypeScript types. If you don't use TypeScript in your codebase, you can simply ignore these files and continue working as before.

Whether your codebase uses TypeScript or not, you will see a dramatic improvement in the developer experience. Thanks to TypeScript types, your IDE should be able to autocomplete react-admin component props and hook parameters. It's like inline documentation, and it's a pleasure to use.

Typescript completion

If your codebase uses TypeScript, you'll get an additional benefit: at compilation time, TypeScript will check that your code uses the react-admin code correctly, and detect errors that might happen at runtime. So upgrading to react-admin 3.9 in a TypeScript project may require additional work to allow your project to compile, but it will make your codebase more robust.

Please note that we've published types for a subset of the entire react-admin codebase. We'll improve type coverage gradually over the next releases.

Filter Sidebar

React-admin lists traditionally used a <Filter> Button/Form combo: the user can click on a filter button to add filters in a form displayed on the top of the list. This user experience has one major drawback: the possible filter values are only apparent after a couple of clicks.

React-admin 3.8 introduces a new component for list filters: the <FilterList> sidebar.

Filter sidebar

Similar to what users usually see on e-commerce websites, it's a panel with many simple filters that can be enabled and combined using the mouse. The user experience is better than the Button/Form Combo, because the filter values are explicit, and it doesn't require typing anything in a form. But it's a bit less powerful, as only filters with a finite set of values (or intervals) can be used in the <FilterList>.

The <FilterList> component expects a list of <FilterListItem> as children. Each <FilterListItem> defines a filter label and a value, which is merged with the current filter value when enabled by the user. Here is an example usage for a list of customers:

import * as React from 'react';
import AccessTimeIcon from '@material-ui/icons/AccessTime';
import MonetizationOnIcon from '@material-ui/icons/MonetizationOnOutlined';
import MailIcon from '@material-ui/icons/MailOutline';
import LocalOfferIcon from '@material-ui/icons/LocalOfferOutlined';
import { FilterList, FilterListItem } from 'react-admin';
import {
    endOfYesterday,
    startOfWeek,
    subWeeks,
    startOfMonth,
    subMonths,
} from 'date-fns';

import segments from '../segments/data';

const LastVisitedFilter = () => (
    <FilterList label="Last visited" icon={<AccessTimeIcon />}>
        <FilterListItem
            label="Today"
            value={{
                last_seen_gte: endOfYesterday().toISOString(),
                last_seen_lte: undefined,
            }}
        />
        <FilterListItem
            label="This week"
            value={{
                last_seen_gte: startOfWeek(new Date()).toISOString(),
                last_seen_lte: undefined,
            }}
        />
        <FilterListItem
            label="Last week"
            value={{
                last_seen_gte: subWeeks(startOfWeek(new Date()), 1).toISOString(),
                last_seen_lte: startOfWeek(new Date()).toISOString(),
            }}
        />
        <FilterListItem
            label="This month"
            value={{
                last_seen_gte: startOfMonth(new Date()).toISOString(),
                last_seen_lte: undefined,
            }}
        />
        <FilterListItem
            label="Last month"
            value={{
                last_seen_gte: subMonths(startOfMonth(new Date()),1).toISOString(),
                last_seen_lte: startOfMonth(new Date()).toISOString(),
            }}
        />
        <FilterListItem
            label="Earlier"
            value={{
                last_seen_gte: undefined,
                last_seen_lte: subMonths(startOfMonth(new Date()),1).toISOString(),
            }}
        />
    </FilterList>
);

const HasOrderedFilter = () => (
    <FilterList
        label="Has ordered"
        icon={<MonetizationOnIcon />}
    >
        <FilterListItem
            label="True"
            value={{
                nb_commands_gte: 1,
                nb_commands_lte: undefined,
            }}
        />
        <FilterListItem
            label="False"
            value={{
                nb_commands_gte: undefined,
                nb_commands_lte: 0,
            }}
        />
    </FilterList>
);

const HasNewsletterFilter = () => (
    <FilterList
        label="Has newsletter"
        icon={<MailIcon />}
    >
        <FilterListItem
            label="True"
            value={{ has_newsletter: true }}
        />
        <FilterListItem
            label="False"
            value={{ has_newsletter: false }}
        />
    </FilterList>
);

const SegmentFilter = () => (
    <FilterList
        label="Segment"
        icon={<LocalOfferIcon />}
    >
        {segments.map(segment => (
            <FilterListItem
                label={segment.name}
                key={segment.id}
                value={{ groups: segment.id }}
            />
        ))}
    </FilterList>
);

You can place these <FilterList> anywhere inside a <List>. The most common case is to put them in a sidebar that is on the left-hand side of the Datagrid. You can use the aside property for that:

import * as React from 'react';
import { Card as MuiCard, CardContent, withStyles } from '@material-ui/core';

import { LastVisitedFilter, HasOrderedFilter, HasNewsletterFilter, SegmentFilter } from './filters';

const Card = withStyles(theme => ({
    root: {
        [theme.breakpoints.up('sm')]: {
            order: -1, // display on the left rather than on the right of the list
            width: '15em',
            marginRight: '1em',
        },
        [theme.breakpoints.down('sm')]: {
            display: 'none',
        },
    },
}))(MuiCard);

const FilterSidebar = () => (
    <Card>
        <CardContent>
            <LastVisitedFilter />
            <HasOrderedFilter />
            <HasNewsletterFilter />
            <SegmentFilter />
        </CardContent>
    </Card>
);

const CustomerList = props => (
    <List aside={<FilterSidebar />}>
        // ...
    </List>
)

Tip: The <FilterList> Sidebar is not a good UI for small screens. You can choose to hide it on small screens (as in the previous example). A good tradeoff is to use <FilterList> on large screens, and the <Filter> Button/Form combo on Mobile.

While working on the <FilterList> component, we realized that the instructions to set up a custom list filter weren't good enough for advanced use cases. So we took the opportunity to rewrite the filters documentation completely. Check out the new documentation in Filtering the List, you'll probably learn some new tricks.

Live Search Form

The filter sidebar is not a form. Therefore, if your users need to enter complex filters, you'll have to recreate a filter form using react-final-form. However, if you only need one text input with a filter-as-you-type behavior, you'll find the <FilterLiveSearch> component convenient.

Filter Live Search

It outputs a form containing a single <SearchInput>, which modifies the page filter on change. That's usually what users expect for a full-text filter. <FilterLiveSearch> only needs a source field.

So for instance, to add a search filter on the customer full name, add the following line to the Sidebar:

+import { FilterLiveSearch } from 'react-admin';

const FilterSidebar = () => (
    <Card>
        <CardContent>
+           <FilterLiveSearch source="full_name" />
            <LastVisitedFilter />
            <HasOrderedFilter />
            <HasNewsletterFilter />
            <SegmentFilter />
        </CardContent>
    </Card>
);

User Menu Improvements

React-admin 3.9 facilitates the display of the name and avatar of the currently logged in user.

User name and avatar

You just have to implement a new method in your authProvider: authProvider.getIdentity(), and return the current user id, fullName, and avatar url:

// in src/authProvider.js
let userIdentity = {};

const authProvider = {
    login: ({ username, password }) => {
        // call the login endpoint, which returns id, fullName, avatar
        // ...
        // and store it in the closure
        userIdentity = { id, fullName, avatar };
    },
    getIdentity: () => {
        return userIdentity;
    }
    // ...
};

export default authProvider;

useListContext For Easier List Layout Customization

When you want to build a custom List layout, it's hard to know which variables are provided by react-admin, and which ones you have to pass down to other components like Filter, Datagrid, or Pagination. As a consequence, it used to be hard to develop a custom List layout (e.g. using a grid instead of a table).

Custom list layout

Starting with react-admin 3.7, the components that you can use in a List don't expect props to be passed down by their parents anymore. Instead, they grab these props from a ListContext using the useListContext hook, as in the following example;

import * as React from 'react';
import { GridList, GridListTile, GridListTileBar } from '@material-ui/core';
import { List, linkToRecord, NumberField, useListContext } from 'react-admin';
import { Link } from 'react-router-dom';

const PosterGrid = () => {
    const { ids, data, basePath } = useListContext();

    if (!ids || !data) return null; // when data isn't loaded yet

    return (
        <GridList cellHeight={180} cols={8}>
            {ids.map(id => (
                <GridListTile
                    component={Link}
                    key={id}
                    to={linkToRecord(basePath, data[id].id)}
                >
                    <img src={data[id].thumbnail} alt="" />
                    <GridListTileBar
                        title={data[id].reference}
                        subtitle={
                            <span>
                                {data[id].width}x{data[id].height},{' '}
                                <NumberField
                                    source="price"
                                    record={data[id]}
                                    color="inherit"
                                    options={{
                                        style: 'currency',
                                        currency: 'USD',
                                    }}
                                />
                            </span>
                        }
                    />
                </GridListTile>
            ))}
        </MuiGridList>
    );
};

const PosterList = props => (
    <List {...props}>
        <PosterGrid />
    </List>
)

If you developed custom List components, get the list controller data from the ListContext using the useListContext hook rather than from props:

-const Aside = ({ data, ids }) => {
+const Aside = () => {
+   const { data, ids } = useListContext();
    return (
        <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>
    );
};

The ListContext dramatically simplifies custom List layouts and lets you customize the User Experience to the tasks your users need to achieve. It's particularly useful in "embedded" lists, e.g. in an <ArrayField>.

<ListBase>: A Layout-Less List Component

We've even introduced a new component called <ListBase>, which fetches the data for the list and stores them in a ListContext, but renders nothing but its children. This new component allows building completely custom List layouts, like for instance a List width an Edit view in the sidebar.

Custom list layout

The following code snippet shows how to leverage <ListBase> for custom List layouts:

import { ListBase } from 'rect-admin';

const PostList = props => (
    <ListBase {...props} perPage={10}>
        <div>
             <Filter />
        </div>
        <Grid container>
            <Grid item xs={8}>
                <SimpleList primaryText={record => record.title} />
            </Grid>
            <Grid item xs={4}>
                List instructions...
            </Grid>
        </Grid>
        <Pagination />
        <div>
            Post related links...
        </div>
    </ListBase>
);

New Look For The Ecommerce Demo

We've revamped the Posters Galore demo to make it look less Googly.

React-admin demo

We wanted to showcase the power of the Material-UI theming capabilities (recently explained in an article in this blog). Also, one major criticism we've heard is that Material Design makes every app look like it's edited by Google. That's why the e-commerce demo now uses a new skin. Check its source for inspiration!

Miscellaneous Changes

  • You can now define custom success and error side effects in <DeleteButton> using the onSuccess and onFailure props
  • You can use the ra-data-simple-rest data provider with a backend that uses the X-total-Count header rather than the Content-Range header
  • We've added a new local Data Provider called ra-data-localstorage, which persists data between sessions. It makes react-admin suitable for local-first web apps.
  • We've removed recompose from the react-admin dependencies. This reduces the bundle size and marks an important step in the migration from higher-order components to hooks.
  • We've added a <SortButton> component to let users sort Lists when it's not using a <Datagrid>.
  • You can now force an always enabled <SaveButton> in the Form components (using the toolbar={<Toolbar alwaysEnableSaveButton />} prop).
  • You can now customize the Add and Remove buttons in <SimpleFormIterator>
  • You can now customize the true and false labels of <NullableBooleanInput>
  • You can now pass custom icons to <BooleanField> to show as values
  • You can now disable the notifications of the useCheckAuth() and useLogoutIfAccessDenied() hooks

In addition to three minor releases (3.7.0, 3.8.0, and 3.9.0), we've published 9 bug fix releases and made the react-admin code base more robust and capable of handling more corner cases. Check the Changelog for a list of all the changes.

We want to thank all the contributors for their work on features, bug fixes, documentation, and tests. React-admin wouldn't be such a high quality library without your contributions!

A Word About React-Admin Enterprise Edition

A month ago, we published a new react-admin edition dedicated to Enterprise customers. Since then, Marmelab kept on developing, testing and documenting additional modules for this edition. We'll soon publish a description of the latest features published in React-admin Enterprise Edition, but here is a quick glimpse:

  • A new package, ra-enterprise, bundling most of the Enterprise features in a zero-configuration replacement for react-admin
  • New form layouts (<AccordionForm>, <WizardForm>, <DialogForm>)
  • New Input component for editing one-to-many and many-to-many relationships: <DualListInput>
  • Lock support for real time apps
  • Mercure adapter for real time updates powered by API Platform
  • Support for branch deletion in tree structures
  • Support for expand in <EditableDatagrid>

If you're interested in any of these new features, give React-admin Enterprise Edition a try!

Wizard Form

Dual List Input

Multi-level menu

Did you like this article? Share it!