React Admin v3: Zoom in the Data Layer

François Zaninotto
François ZaninottoOctober 14, 2019
#react#react-admin#tutorial

React-admin v3, currently in beta, will soon be released as stable. It doesn't bring many new features, but focuses on improving the developer experience thanks to React hooks. And the most dramatic changes concern the way developers query the dataProvider - that is, how they interact with their API.

Let's see these differences in detail through a few concrete examples, including a focus on optimistic rendering and side effects.

useQuery and useMutation Hooks

The Query and Mutation components introduced in react-admin 2.8 made it easier to call the dataProvider. For instance, here is how to query the API for a user record in order to display a user profile:

import { Query } from 'react-admin';

const UserProfile = ({ record }) => (
    <Query
        type="GET_ONE"
        resource="users"
        payload={{ id: record.userId }}
    >
        {({ data, loading, error }) =>
            loading ? <Loading />
            : error ? <Error />
            : (
                <ul>
                    <li>Name: {data.name}</li>
                    <li>Email: {data.email}</li>
                </ul>
            );
        }
    </Query>
);

But the idiomatic way to call API endpoints in React 16.8 is hooks. So react-admin v3 provides hook alternatives to the Query and Mutation components, called useQuery and useMutation:

import { useQuery } from "react-admin";

const UserProfile = ({ userId }) => {
  const { data, loading, error } = useQuery({
    type: "getOne",
    resource: "users",
    payload: { id: userId },
  });

  if (loading) return <Loading />;
  if (error) return <Error />;
  if (!data) return null;

  return (
    <ul>
      <li>Name: {data.name}</li>
      <li>Email: {data.email}</li>
    </ul>
  );
};

Apart from the move from render props to hooks, there is a subtle difference between the v2 and v3 examples: the query type was GET_ONE in v2, it's getOne in v3. We'll explain why later in this article.

Optimistic Rendering For the Masses

useQuery calls the dataProvider when the component mounts, and displays the data when the response arrives. But most of the time, react-admin doesn't need to call the dataProvider to get data: it can just grab it from the Redux store. That's what the react-admin List and Edit component do: they use optimistic rendering, which means they render data from the Redux store before the result from the dataProvider arrives. In many cases, this means the result displays immediately.

To get the same functionality with react-admin v2, you had to dispatch a specific Redux action, and connect to an undocumented location in the Redux store to grab the result. In react-admin v3, it's as easy as replacing useQuery by useQueryWithStore, which has exactly the same API:

-import { useQuery } from 'react-admin';
+import { useQueryWithStore } from 'react-admin';

const UserProfile = ({ userId }) => {
-   const { data, loading, error } = useQuery({
+   const { data, loading, error } = useQueryWithStore({
        type: 'getOne',
        resource: 'users',
        payload: { id: userId }
    });

    if (loading) return <Loading />;
    if (error) return <Error />
    if (!data) return null;

    return (
        <ul>
            <li>Name: {data.name}</li>
            <li>Email: {data.email}</li>
        </ul>
    )
}

The return object of useQuery and useQueryWithStore contains both a loading and a loaded field. For useQuery, these two fields are opposite of one another. but for useQueryWithStore, there is a slight difference. After the first mount, the data is already in the store, so loaded is always true. That means it's better to use loaded than loading when using optimistic rendering - it hides the loader on subsequent mounts.

import { useQueryWithStore } from 'react-admin';

const UserProfile = ({ userId }) => {
-   const { data, loading, error } = useQueryWithStore({
+   const { data, loaded, error } = useQueryWithStore({
        type: 'getOne',
        resource: 'users',
        payload: { id: userId }
    });

-   if (loading) return <Loading />;
+   if (!loaded) return <Loading />;
    if (error) return <Error />
    if (!data) return null;

    return (
        <ul>
            <li>Name: {data.name}</li>
            <li>Email: {data.email}</li>
        </ul>
    )
}

To render optimistically, the <List> and <Edit> components use the useQueryWithStore hook to call the dataProvider. You can use this new hook with confidence: it's one of the pillars of the react-admin rendering model.

Specialized Data Provider Hooks

useQueryWithStore lets you call any verb on the dataProvider, and therefore has a generic signature. React-admin v3 also offers specialized hooks for each dataProvider verb: useGetList for GET_LIST, useGetOne for GET_ONE, etc. Here is how to use useGetOne for the UserProfile component:

-import { useQueryWithStore } from 'react-admin';
+import { useGetOne } from 'react-admin';

const UserProfile = ({ userId }) => {
-   const { data, loaded, error } = useQueryWithStore({
-       type: 'getOne',
-       resource: 'users',
-       payload: { id: userId }
-   });
+   const { data, loaded, error } = useGetOne('users', userId);

    if (!loaded) return <Loading />;
    if (error) return <Error />
    if (!data) return null;

    return (
        <ul>
            <li>Name: {data.name}</li>
            <li>Email: {data.email}</li>
        </ul>
    )
}

There are two benefits to these specialized dataProvider hooks:

  • they's shorter to write (and more expressive)
  • they allow compile-time argument validation (using TypeScript) and therefore help preventing errors

We recommend using them as much as possible.

Although react-admin is written mostly in TypeScript, applications using react-admin can use TypeScript or JavaScript. You don't have to use TypeScript to use react-admin v3!

Side Effects As Functions

In react-admin v2, when you wanted to do something special after the response arrives, you had to use a side effect object in onSuccess and onFailure options. For instance, to display a notification and redirect the user to another page after a mutation, you would do the following:

import { Mutation } from "react-admin";

const ApproveButton = ({ record }) => {
  return (
    <Mutation
      type="UPDATE"
      resource="comments"
      payload={{ id: record.id, data: { is_approved: true } }}
      options={{
        onSuccess: {
          notification: { body: "Comment approved" },
          redirectTo: "/comments",
        },
        onFailure: {
          notification: {
            body: "Error: comment not approved",
            level: "warning",
          },
        },
      }}
    >
      {(approve, { loading }) => (
        <FlatButton label="Approve" onClick={approve} disabled={loading} />
      )}
    </Mutation>
  );
};

export default ApproveButton;

React-admin had to use redux-saga to handle these side effects, which made the implementation kind of cryptic for most people. Adding your own side effects was very hard, too. And finally, it was necessary to look at the documentation very often to know how a side effect was named.

This lead us to transform onSuccess and onFailure options to functions. That way, side effects can be anything you want - and usually, they are a callback returned by a hook.

import { useMutation, useNotify, useRedirect } from "react-admin";

const ApproveButton = ({ record }) => {
  const notify = useNotify();
  const redirectTo = useRedirect();
  const [approve, { loading }] = useMutation(
    {
      type: "update",
      resource: "comments",
      payload: { id: record.id, data: { is_approved: true } },
    },
    {
      onSuccess: ({ data }) => {
        notify(`Comment #${data.id} approved`);
        redirectTo("/comments");
      },
      onFailure: error => {
        notify(`Comment approval error: ${error.message}`, "warning");
      },
    }
  );
  return <FlatButton label="Approve" onClick={approve} disabled={loading} />;
};

Using a function instead of an object allows side effects to be based on the data provider response (or the error it throw in case of failure). It is trivial to add custom side effects. We also think that the code is more readable that way.

In addition to useNotify and useRedirect, which are illustrated above, react-admin v3 provides 2 more side effect hooks: useRefresh and useUnselectAll.

New Data Provider Signature

React-admin v2 relies on Data Provider verbs: constants like GET_LIST, GET_ONE, etc. Data Providers use these verbs to determine the query they're supposed to run.

But with the rise of specialized hooks like useGetOne, and the possibility to detect bugs at compile time using TypeScript, these verbs were limiting. In react-admin v3, the query verbs are now methods that you can call on the dataProvider, because it's an object.

const dataProvider = {
  getList: (resource, params) => Promise,
  getOne: (resource, params) => Promise,
  getMany: (resource, params) => Promise,
  getManyReference: (resource, params) => Promise,
  create: (resource, params) => Promise,
  update: (resource, params) => Promise,
  updateMany: (resource, params) => Promise,
  delete: (resource, params) => Promise,
  deleteMany: (resource, params) => Promise,
};

That explains why the useQuery hook uses the getOne query type instead of GET_ONE.

React-admin v3 also bundles sophisticated TypeScript interfaces for the return type of each dataProvider method, that will allow you to detect subtle bugs at compile time.

If you have written a custom dataProvider for react-admin v2, it will still work with react-admin v3, because we've included a wrapper for legacy Data Providers. No need for a long rewrite.

Conclusion

It took us a long time to find the right API for Data Provider hooks. In fact, while working on react-admin v3, we've rewritten these hooks twice. But once we've started using the new API, we were confident that it improved the developer experience a great deal. We've even reached a point where we're using the Data Provider logic from react-admin in non react-admin projects.

We think you're going to love these hooks, too. To try them early on, upgrade to react-admin v3.

Did you like this article? Share it!