Skip to content

Give a Second API to Your React-admin App

April 19, 2019Romain Batby5 min read

react-admin-api-cover

React-admin is a frontend framework for building back-offices running in the browser on top of REST/GraphQL APIs, using ES6, React and Material Design.
React Admin (previously named Admin-on-rest) is an Open Source project maintained by marmelab.

If you do not know what React Admin is capable of, you can have a sneak peek in this video.

Before react-admin setting up an administration was tedious. Now it takes a few days.
Actually, you only have to specify the endpoint of your API and then React-admin handles all the CRUD operations.
You can focus on creating your pages thanks to built-in components.

Suppose we want to create a small administration of all soccer players. All the data we need (name, teams…) except goals, come from the same API : https://soccer/. (It should be named football, its proper name 🏈)

First step, we want to display the list of players ranked by ballon d'or points, alongside their name and team.

Ballon d'or honours the best player each year, based on voting by soccer journalists (like github stars but for soccer players).

players-list

Here is the code I wrote :

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

const PlayersList = (props) => (
  <List {...props}>
    <Datagrid>
      <TextField source="name" />
      <TextField source="team" />
      <NumberField source="points" />
    </Datagrid>
  </List>
);

export default PlayersList;

Seven tiny lines of code and I got my players list, sortable by ballon d'or points — ain’t that sweet?

The second API dilemma

Everything works perfectly until we want our administration to handle a resource stored on another API.
We want to display how many goals each player has scored during the year using the european-top-strikers API (is Kylian Mbappé going to score more than Leo Messi?).

At this point, things get a little bit spicier.
Let's look at it a bit closer!

How we would display total of scored goals following the react-admin documentation:

<ReferenceField label="goals" source="name" reference="goals" linkType={false}>
  <TextField source="total" />
</ReferenceField>

But goals scored is not data available via the soccer API.

For this reason, the reference does not work and react-admin cannot display the total of goals.

How does react-admin comunicate with API?

Here is an overview of the react-admin architecture:

Data Provider architecture

From client to API, the react-admin interface transforms the users' actions into a data query.
Then this query is given to the data provider and finally communicated by HTTP to the API.

A data-provider is a simple adapter used to communicate with an API.
It takes the url of the API and the http client to make the request.
Here is a simple data-provider :

const dataProvider = (apiUrl, httpClient = fetchUtils.fetchJson) => {
  /**
   * @param {string} type Request type, e.g GET_LIST, GET_ONE, DELETE, POST..
   * @param {string} resource Resource name, e.g. "players"
   * @param {Object} payload Request parameters. Depends on the request type
   * @returns {Promise} the Promise for a data response
   */
  return (type, resource, params) => {
    const { url, options } = convertDataRequestToHTTP(type, resource, params);

    return httpClient(url, options).then((response) =>
      convertHTTPResponse(response, type, resource, params)
    );
  };
};

As we can see, the dataProvider takes as an argument only one API url.

So what can we do to handle our new API?

Bronze medal: Add a switch statement

The first possibility is to tell react-admin to call our top scorer API when the data query concerns goals.

We can do this by adding a switch statement on the resource in the data-providerfile.

In the below example, we call another url when the data query is about the goals resource.
Instead of calling our main API${apiUrl}/${resource}?${stringify(query)} as we do for all resources, we call european-top-strikers API.

const convertDataRequestToHTTP = (type, resource, params) => {
    let url = '';
    const options = {};
    switch (type) {
        case GET_LIST: {
          const { page, perPage } = params.pagination;
          const { field, order } = params.sort;
          switch (resource) {
            case 'goals': {
              url = 'https://european-top-strikers/goals';
              break;
            }
            default: {
              const query = {
                sort: JSON.stringify([field, order]),
                page,
                per_page: perPage,
                filter: JSON.stringify(params.filter),
              };
              url = `${apiUrl}/${resource}?${stringify(query)}`;
            }
          }
          break;
        }
        ....

Advantages :

  • One single line of code to fix any problematic request (here LIST of goals).
  • Easy to understand : At a glance, we see which resource and which request type are concerned.

Downsides :

  • Duplicated code : You need to add a switch case statement for each request type you want to handle.
  • Harder to read : each request type needs to have its proper switch statement. Your data-provider is quickly getting messy.

Silver medal: Create a super data-provider using the facade pattern

The facade pattern hides the complexities of the larger system and provides a simpler interface to the client.

Our super data provider file will call different providers according to the resource given by the data query.

import httpClient from "./http-client";
import dataProvider from "./data-provider";

const superDataProvider = (type, resource, params) => {
    if (resource === 'goals') {
        return dataProvider('https://european-top-strikers', httpClient)(type, resource, params);
    }
    return dataProvider('https://soccer', httpClient)(type, resource, params);
};

export default superDataProvider;

Advantages :

  • Handle all possible request types at once.
  • Still easy to understand.
  • We do not modify the data-provider.

Downsides :

  • We still have this unneccesary conditional statement. It is necessary to add a condition for each resource retrieved on the second API.
  • What would happen if we want a reusable data-provider for different admin which share same resource names? For example, if our app jointly handles soccer goals and handball goals.

Gold medal: Prefix every resource to dynamically call the right APIs

The trick consists in prefixing the resource name with its specific url.

 <Admin
        authProvider={authProvider}
        dashboard={Dashboard}
        dataProvider={dataProvider('https://')}
        history={history}
        theme={theme}
        title="Admin"
      >
        <Resource name="soccer/goals" {...soccerGoals} />
        <Resource name="handball/goals" {...HandballGoals} />
</Admin>

Then the magic happens.
The data-provider when concatenating the API url and the resource name, dynamically calls the right url.

default: {
            const query = {
              sort: JSON.stringify([field, order]),
              page,
              per_page: perPage,
              filter: JSON.stringify(params.filter),
            };
            url = `${apiUrl}/${resource}?${stringify(query)}`; // https://handall/goals
          }

Advantages :

  • We do not need to modify the data-provider.
  • There is no need to create a new component.

Downsides :

  • The url given to the data-provider is less understandable as we take the common part of all APIs(eg. https:// here)

Disqualified: create HOC components to pass your resource

At first, I had created a HOC withGoalsthat retrieved goals from the right API and injected it as props into my component.

Then I had created my own GoalsField component, directly outputting the material-ui Typographycomponent.

import React from 'react';
import Typography from '@material-ui/core/Typography';

const GoalsField = ({record, goals}) => {
  const {name} = record;
  const totalGoals = goals.find((goals) => goal.striker.name === name).length();
  return <Typography>{totalGoals}</Typography>
};

export default withGoals(GoalsField);

<GoalsField addLabel label="Goals" {...props} source={props} />

Everytime I had to develop a new component connected to the goals ressource, I was not able to use the react-admin library (remember this code compared to this one). You should definitely not use this solution.

Summary

React-admin is a front-end library to quickly deploy administration.

To handle APIs, React-admin uses an adapter approach, with a concept called Data Provider.

There are at least 3 different solutions to handle multiple APIs at the same time :

  • Add a switch statement on the resource
  • Use different data-providers according to a resource
  • Prefix every resource with specific url to dynamically call the right APIs