React Admin February 2021 Update

Gildas GarciaFrançois Zaninotto
#react#react-admin

Marmelab and the open-source community contributed a lot to react-admin, the open-source frontend Framework for building B2B applications running in the browser on top of REST/GraphQL APIs, over the past quarter. This article lists the highlights published in the 3 latest minor versions (3.10, 3.11, and 3.12).

There is a lot to say, so let's dive in!

AutocompleteInput Now Has A Reset Button

By popular demand, the <AutocompleteInput> now allows users to reset the text they entered by clicking a reset button.

Autocomplete Reset

You just need to add the resettable prop to the <AutocompleteInput> components in your forms:

const CommentEdit = props => (
  <Edit {...props}>
    <SimpleForm>
      <TextInput disabled source="id" />
      <ReferenceInput source="post_id" reference="posts">
        <AutocompleteInput
          optionText={<OptionRenderer />}
+         resettable
        />
      </ReferenceInput>
      ...
    </SimpleForm>
  </Edit>
)

The feedback we received from users about this simple change in react-admin 3.10 is overwhelmingly positive. It's these little tweaks that make the user experience fluid and efficient!

TranslatableInput and TranslatableField

In localized (l10n) apps, it's common to store string values in different locales.

For instance, let's consider an Outdoor Tools & Equipment store available in English and French. A typical product record would contain the following data, where the name and description fields have one value per locale:

{
  id: 1234,
  unspsc: '27112700',
  name: {
    en: 'Husqvarna 440 Gas Chainsaw',
    fr: 'Tronçonneuse Husqvarna 440',
  },
  description: {
    en: 'Lightweight and efficient all-round saw, ideal for those looking for a chainsaw that is exceptionally easy to start and maneuver.',
    fr: "Légère et économique, idéale pour les professionnels à la recherche d'une tronçonneuse facile à manier.",
  }
  price: 299
}

To allow users to edit the translatable fields, you can use inputs with a nested source prop:

const ProductEdit = props => (
  <Edit {...props}>
    <SimpleForm>
      <TextInput source="unspsc" />
      <TextInput source="name.en" label="English name" />
      <TextInput source="name.fr" label="French name" />
      <TextInput source="description.en" label="English description" />
      <TextInput source="description.fr" label="French description" />
    </SimpleForm>
  </Edit>
);

But the visual result isn't very good, you have to invent one label per field, and this doesn't scale to an undetermined number of locales.

To improve the user experience when editing translatable fields, react-admin 3.12 introduces the <TranslatableInputs> component. It's a wrapper around <Input> components for translatable fields, and it allows to group them in tabs:

Translatable Inputs

import { TranslatableInputs } from 'react-admin';

const ProductEdit = props => (
  <Edit {...props}>
    <SimpleForm>
      <TextInput source="unspsc" />
      <TranslatableInputs locales={['en', 'fr']}>
        <TextInput source="name" />
        <TextInput source="description" />
      </TranslatableInputs>
    </SimpleForm>
  </Edit>
);

Just like for <TabbedForm>, the tab headers turn red when one of the inputs in the tab contains an error. We've refactored the way react-admin detects errors in a group of inputs (and introduced the concept of <FormGroup>) to make that possible.

You can use the corresponding Field component, <TranslatableFields>, to display translatable content in a Show view.

Server-Side Validation And Validation Upon Submission

In a form, even though the client-side validation passes, the server-side validation may fail (e.g. if a unicity constraint can only be applied securely on the server).

That means you must be able to support server-side validators and to react to a failed form submission. React-final-form (the form library used by react-admin) supports these two features, but react-admin used to force you to write a custom form component to benefit from them.

Starting with react-admin 3.10, form validators can be asynchronous, and this lets you use server-side validators:

const validateUserCreation = async (values) => {
    const errors = {};
    if (!values.firstName) {
        errors.firstName = ['The firstName is required'];
    }
    if (!values.age) {
        errors.age = ['The age is required'];
    } else if (values.age < 18) {
        errors.age = ['Must be over 18'];
    }
    const isEmailUnique = await checkEmailIsUnique(values.userName);
    if (!isEmailUnique) {
        errors.email = ['Email already used'];
    }
    return errors
};
export const UserCreate = (props) => (
    <Create {...props}>
        <SimpleForm validate={validateUserCreation}>
            <TextInput label="First Name" source="firstName" />
            <TextInput label="Email" source="email" />
            <TextInput label="Age" source="age" />
        </SimpleForm>
    </Create>
);

This also works at the input level:

const validateEmailUnicity = async (value) => {
    const isEmailUnique = await checkEmailIsUnique(value);
    if (!isEmailUnique) {
        return 'Email already used';
    }
    return errors
};
const emailValidators = [required(), validateEmailUnicity];
export const UserCreate = (props) => (
    <Create {...props}>
        <SimpleForm>
            ...
            <TextInput label="Email" source="email" validate={emailValidators} />
            ...
        </SimpleForm>
    </Create>
);

And starting with react-admin 3.12, forms can be validated on the server-side upon submission. To display the validation errors, you must use a custom save function to pass the { returnPromise: true } option to the dataProvider:

import { useMutation } from 'react-admin';

export const UserCreate = (props) => {
    const [mutate] = useMutation();
    const save = useCallback(
        async (values) => {
            try {
                await mutate({
                    type: 'create',
                    resource: 'users',
                    payload: { data: values },
                }, { returnPromise: true });
            } catch (error) {
                if (error.body.errors) {
                    return error.body.errors;
                }
            }
        },
        [mutate],
    );
    return (
        <Create undoable={false} {...props}>
            <SimpleForm save={save}>
                <TextInput label="First Name" source="firstName" />
                <TextInput label="Age" source="age" />
            </SimpleForm>
        </Create>
    );
};

This change facilitates the integration of react-admin with API Platform, a popular PHP framework for building web APIs.

Optimistic Mutations

By default, react-admin uses optimistic updates - i.e. it applies updates locally and pushes them to the server after a delay that allows users to undo their changes. You can already opt-out and force the interface to wait for the server response to apply updates locally, by setting the undoable prop to false. We call that "pessimistic" updates.

In some cases, the ability to undo a mutation isn't necessary, but the mutation shouldn't be blocking either. A good example is a command that takes several minutes to complete: the user shouldn't be forced to wait for the completion to continue using the admin.

To support these cases, react-admin 3.12 introduces the concept of mutationMode, which supersedes the undoable option. It can take 3 values: pessimistic, optimistic, and undoable:

pessimisticoptimisticundoable
dataProvider callimmediateimmediatedelayed
local changeswhen dataProvider returnsimmediateimmediate
side effectswhen dataProvider returnsimmediateimmediate
cancellablenonoyes

You can use the mutationMode as an option in your own dataProvider calls:

import * as React from "react";
import { useUpdate, useNotify, useRedirect, Button } from 'react-admin';

const ApproveButton = ({ record }) => {
  const notify = useNotify();
  const redirect = useRedirect();
  const [approve, { loading }] = useUpdate(
    'comments',
    record.id,
    { isApproved: true },
    record,
    {
      // useUpdate is pessimistic by default, use optimistic instead
      mutationMode: 'optimistic',
      onSuccess: ({ data }) => {
        redirect('/comments');
        notify('Comment approved', 'info', {}, true);
      },
      onFailure: (error) => notify(`Error: ${error.message}`, 'warning'),
    }
  );
  return <Button label="Approve" onClick={approve} disabled={loading} />;
};

You can also pass the mutationMode prop to the <Edit> component to change the behavior of the Save and Delete actions:

const PostEdit = props => (
    <Edit mutationMode="optimistic" {...props}>
        // ...
    </Edit>
);

Use this prop unstead of undoable=false to make the actions pessimistic:

const PostEdit = props => (
    <Edit mutationMode="pessimistic" {...props}>
        // ...
    </Edit>
);

Improved Keyboard Navigation

Power users, and users with motor disabilities, rely on the keyboard rather than the mouse to navigate. React-admin 3.12 introduces 2 very important improvements for keyboard navigation:

  • The sidebar menu is now navigable with the keyboard (you can select a Menu with Tab and Enter)
  • After loading the admin, hitting the "Tab" key reveals a "Skip to content" button that allows to skip the app bar items and the menu items, and to jump directly to the content zone.

Keyboard navigation

This change is automatic and doesn't require any configuration.

Accessibility is one of our priorities in react-admin, and we're determined to continue improving it. Expect more changes like these in the future!

Use <List> Everywhere You Want

We've designed the <List> component to be used as the list route for a <Resource>. That means it's a page component, which reads and stores the state in the URL. But we've noticed many developers trying to use <List> (or <ListBase>) outside of a <Resource> context (e.g. in a custom route, or in a tab in another view). They are surprised that it doesn't work out of the box.

So we've updated the <List> and <ListBase> components to support that case and made the dependency to the route state explicit in the <Resource> code. That means you can now use <List> everywhere you want - provided you put it inside a ResourceContextProvider:

const TagsEdit = (props) => (
    <>
        <Edit {...props}>
            // ...
        </Edit>
        <ResourceContextProvider resource="posts">
            <ListBase basePath="/posts" filter={{ tags: [id]}}>
                <Datagrid>
                    <TextField source="title" />
                </Datagrid>
            </ListBase>
        </ResourceContextProvider>
    </>
)

When not used as <Resource list>, a <List> component stores its state (current sort, filters, and pagination) in memory rather than in the URL.

Contexts And Avoiding Cloning Elements

The first time you've seen this snippet, you may have thought: "What is this magic?"

const PostList = props => (
  <List {...props}>
    <Datagrid>
      <TextField source="title" />
      <TextField source="author" />
      <DateField source="publication_date" />
    </Datagrid>
  </List>
);

How does the <Datagrid> component know the records it has to show, the current sort field, the list of selected ids?

In order to provide a better Developer Experience (DX), we made an early decision to clone the elements you provide (like the <Datagrid>) so that we can inject them data computed at runtime or fetched from the server. So internally, the <List> component does something like this:

const List = ({ children, ...props }) => {
  const listControllerProps = useListControllerProps(props);
  return (
    <div>
      <ListToolbar {...listControllerProps} />
      {React.cloneElement(children, listControllerProps)}
      <Pagination {...listControllerProps} />
    </div>
  );
}

This simple approach has several drawbacks:

  • Even though the <Datagrid> requires some of the listControllerProps to work, we cannot mark them as required in propTypes and TypeScript types because you must be able to pass <Datagrid> with no props as a child of <List>. This leads to runtime bugs that can't be detected at compilation.
  • The <Datagrid> code doesn't make a difference between injected props (that are considered private) and props that developers can override. It makes the code harder to maintain.
  • Cloning elements prevents optimizations with React.memo() to work on large trees.

One alternative would be to use the function-as-child pattern:

const PostList = props => (
  <List {...props}>
    {listControllerProps => (
      <Datagrid {...listControllerProps}>
        {datagridProps => (
          <>
            <TextField source="title" {...datagridProps} />
            <TextField source="author" {...datagridProps} />
            <DateField source="publication_date" {...datagridProps} />
          </>
        )}
      </Datagrid>
    )}
  </List>
);

This is more explicit, but it's also much more cumbersome to write. Besides, it causes performance problems and forbids some of the advanced react-admin functionality. We've opted out of that technique in the react-admin core.

Instead, starting with version 3.7, we've started inverting the responsibilities for props passing: The parent (e.g. <List>) no longer has to push props to its children, it's the child (e.g. <Datagrid>) that must pull the props from a React context.

So basically <List> now works as follows:

const List = ({ children, ...props }) => {
  const listControllerProps = useListControllerProps(props);
  return (
    <div>
      <ListContextProvider value={listControllerProps}>
        <ListToolbar />
        {children}
        <Pagination />
      </ListContextProvider>
    </div>
  );
}

Components used inside a <List> can request props computed above them by calling useListContext().

With version 3.10, we generalized this principle and introduced more contexts: ResourceContext, RecordContext and SaveContext, usable in <Create>, <Edit> and <Show> views. These contexts allow to write custom components without expecting props to be passed by the parent. For instance, if you want to write a custom button for updating a record in an <Edit> view:

-const ResetViews = ({ record, resources }) => {
+const ResetViews = () => {
+ const record = useRecordContext();
+ const resource = useResourceContext();

  const [handleClick, { loading }] = useUpdate(
        resource,
        record.id,
        { views: 0 },
        record,
    );

  return (
    <button onClick={handleClick} disabled={loading}>
      Reset views
    </button>
  );
}

Tip: React-admin still uses React.cloneElement() for backwards compatibility reasons. So even though it's now easier to write a custom component for react-admin views, the performance benefits will only appear once we remove that compatibility layer - in the next major release, 4.0.

We encourage you to use these contexts in your custom components to prepare for future major versions.

React-admin already uses them a lot, especially <SaveContext>, which allows some advanced scenarios around Create and Edit that were very difficult before.

Easier Customization for Create, Edit, and Show

The 3.7 version introduced the <ListBase> component and several hooks which allow one to create completely custom list views, leveraging the ListContext. In the same spirit, we introduced the same base components for the other views: <CreateBase>, <EditBase>, and <ShowBase>. They render nothing but their children and handle all the heavy lifting required for those views (fetching, updating, etc) by leveraging several new contexts.

Use these components e.g. to create custom layouts for an Edit page, as in the following example:

import * as React from 'react';
import { EditBase, SimpleForm } from 'react-admin';
import { Grid } from '@material-ui/core';

const PostEdit = ({ children, ...props }) => (
  <EditBase {...props}>
      <Grid container>
          <Grid item xs={8}>
              <SimpleForm>
                  ...
              </SimpleForm>
          </Grid>
          <Grid item xs={4}>
              Edit instructions...
          </Grid>
      </Grid>
      <div>
          Post related links...
      </div>
  </EditBase>
);

Customize The Ready Screen

When a react-admin <Admin> component renders with no child <Resource>, react-admin assumes the user is a developer starting a new app and displays the following screen, which we call the "Ready screen":

The ready screen

But there are legitimate cases where you may have an admin without any child <Resource>, e.g. to allow users to add resources dynamically. In that case, you must be able to display alternative content for the ready screen.

The ability to do just this was added in react-admin 3.10:

import * as React from 'react';
import { Admin } from 'react-admin';

const Ready = () => (
    <div>
        <h1>Admin ready</h1>
        <p>You can now add resources</p>
    </div>
)

const App = () => (
    <Admin ready={Ready}>
        ...
    </Admin>
);

Customize Failed Authentication Side Effects

The authProvider.checkAuth() and authProvider.checkError() methods should return a rejected Promise when the user isn't authenticated. By default, the user is then redirected to the Login screen with a notification inviting them to log in.

Starting with react-admin 3.10, you can now customize the redirection and the error message, by returning a custom object in the rejected Promise:

// in src/authProvider.js
export default {
  login: ({ username, password }) => { /* ... */ },
  getIdentity: () => { /* ... */ },
  logout: () => { /* ... */ },
  checkError: (error) => {
    const status = error.status;
    if (status === 401 || status === 403) {
      localStorage.removeItem('auth');
-     return Promise.reject();
+     return Promise.reject({
+       redirectTo: '/credentials-required',
+       message: 'login.required'
+     });
    }
    // other error code (404, 500, etc): no need to log out
    return Promise.resolve();
  },
  checkAuth: () => {
    if (!localStorage.getItem('auth')) {
-     return Promise.reject();
+     return Promise.reject({
+       redirectTo: '/credentials-required',
+       message: 'login.required'
+     });
    }
    return Promise.resolve();
    }
    // ...
}

You can even disable the notification altogether by passing false to message. This is useful in scenarios where the login page belongs to another app, as when using OpenId Connect (OIDC).

Performance Improvements

We've once again profiled react-admin in complex apps, tracked down bottlenecks, and removed the most offending ones. Users with old computers should benefit the most from these changes, as react-admin now needs fewer render cycles upon user interaction.

The following capture shows how fast react-admin feels when the data has already been fetched from the API. Navigation is instantaneous, even for complex pages:

Speed improvements

In particular, we disabled the Material Design ripple effect on buttons. That change really speeds up large lists, which usually contain at least one button per row. The ripple effect is mostly intended for mobile users, so desktop users should see a net gain. You can reenable it via the global application theme.

Also, we've changed the way the <List> controller handles filter addition and removal. Users should see a major speedup when they change the filters in a list - especially if the data was already fetched in the past.

Finally, we've tweaked how the <ReferenceField> and <ReferenceArrayField> load data. We only display a loader animation if loading references takes more than one second. Not rendering these animations make the navigation experience snappier, especially on large lists.

Speaking of performance and large lists, a reminder: In a <Datagrid>, if the fields never change, use the optimized prop to avoid re-renders and make the UI more responsive:

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

const PostList = props => (
  <List {...props}>
-   <Datagrid>
+   <Datagrid optimized>
      <TextField source="title">
      <TextField source="body">
      <TextField source="author">
  </List>
)

Domain Telemetry

Starting 3.11, react-admin applications send an anonymous request on mount to a telemetry server operated by <marmelab. You can see this request by looking at the Network tab of your browser DevTools:

https://react-admin-telemetry.marmelab.com/react-admin-telemetry

The only data sent to the telemetry server is the admin domain (e.g. "example.com") - no personal data is ever sent, and no cookie is included in the response. The react-admin team uses these domains to track the usage of the framework.

You can opt out of telemetry by simply adding disableTelemetry to the <Admin> component:

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

const App = () => (
    <Admin disableTelemetry>
        // ...
    </Admin>
);

Miscellaneous Changes

There are many, many more minor changes in 3.10, 3.11, and 3.12, including;

  • Add support for <ArrayInput disabled>
  • Add ability to disable redirection after logout
  • Add ability to customize the notification element in the <Login> page
  • Add ability to disable redirection after logout
  • Add ability to disable the <UserMenu> without rewriting the <AppBar>
  • Add loading state support to the children of <ReferenceInput>
  • Add <MenuItemLink TooltipPops> to override tooltips in menus
  • Add <SimpleForm component> to override the root component in forms

Check out the react-admin changelog for more details.

A Word About React-Admin Enterprise Edition

The open-source version of react-admin is not the only one to benefit from great new features. Over the past months, Marmelab has been hard at work polishing 3 new modules for the Enterprise Version:

  • ra-search integrates your search engine with an "Omnibox" experience, allowing users to find content much faster than before.

ra-search demo

  • ra-calendar lets you display and manipulate events, drag and resize appointments, and browse a calendar.

ra-calendar

  • ra-enterprise offers preconfigured components replacing the default react-admin ones to quickly integrate the Enterprise Edition modules.

  • And let's not forget Saved Queries a new feature of the ra-preferences module, allowing users to save custom combinations of filter & sort parameters for later reuse.

Saved queries

We've published a separate blog post covering these new Enterprise features. And if you're interested in any of these new features, give React-admin Enterprise Edition a try!

Did you like this article? Share it!