Data Fetching

You can build a react-admin app on top of any API, whether it uses REST, GraphQL, RPC, or even SOAP, regardless of the dialect it uses. This works because react-admin doesn’t use fetch directly. Instead, it uses a Data Provider object to interface with your API and React Query to handle data fetching.

The Data Provider

In a react-admin app, you don’t write API calls using fetch or axios. Instead, you communicate with your API through an object called the dataProvider.

Backend agnostic

The dataProvider exposes a predefined interface that allows react-admin to query any API in a normalized way.

For instance, to query the API for a single record, react-admin calls dataProvider.getOne():

const response = await dataProvider.getOne('posts', { id: 123 });
console.log(response.data); // { id: 123, title: "hello, world" }

The Data Provider is responsible for transforming these method calls into HTTP requests and converting the responses into the format expected by react-admin. In technical terms, a Data Provider is an adapter for an API.

A Data Provider must implement the following methods:

const dataProvider = {
    async getList(resource, { sort, filter, pagination }) => ({ data: Record[], total: number }), 
    async getOne(resource, { id }) => ({ data: Record }),
    async getMany(resource, { ids }) => ({ data: Record[] }),
    async getManyReference(resource, { target, id, sort, filter, pagination }) => ({ data: Record[], total: number }), 
    async create(resource, { data }) => ({ data: Record }),
    async update(resource, { id, data }) => ({ data: Record }),
    async updateMany(resource, { ids, data }) => ({ data: Identifier[] }),
    async delete(resource, { id } ) => ({ data: Record }),
    async deleteMany(resource, { ids }) => ({ data: Identifier[] }),
}

Tip: A Data Provider can have additional methods beyond these 9. For example, you can add custom methods for non-REST API endpoints, tree structure manipulations, or realtime updates.

The Data Provider is a key part of react-admin’s architecture. By standardizing the Data Provider interface, react-admin can offer powerful features, like reference handling, optimistic updates, and autogenerated CRUD components.

Backend Agnostic

Thanks to this adapter system, react-admin can communicate with any API. It doesn’t care if your API is a REST API, a GraphQL API, a SOAP API, a JSON-RPC API, or even a local API. It doesn’t care if your API is written in PHP, Python, Ruby, Java, or JavaScript. It doesn’t care if your API is a third-party API or a homegrown API.

React-admin ships with more than 50 data providers for popular API flavors.

You can also write your own Data Provider to fit your backend’s particularities. Data Providers can use fetch, axios, apollo-client, or any other library to communicate with APIs. The Data Provider is also the ideal place to add custom HTTP headers, authentication, etc.

Check out the Data Provider Setup documentation for more details on how to set up a Data Provider in your app.

Calling The Data Provider

Many react-admin components use the Data Provider: page components like <List> and <Edit>, reference components like <ReferenceField> and <ReferenceInput>, action Buttons like <DeleteButton> and <SaveButton>, and many more.

If you need to call the Data Provider directly from your components, you can use the specialized hooks provided by react-admin:

For instance, to call dataProvider.getOne(), use the useGetOne hook:

import { useGetOne } from 'react-admin';
import { Loading, Error } from './MyComponents';

const UserProfile = ({ userId }) => {
    const { data: user, isPending, error } = useGetOne('users', { id: userId });

    if (isPending) return <Loading />;
    if (error) return <Error />;
    if (!user) return null;

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

You can also call the useDataProvider hook to access the dataProvider directly:

import { useDataProvider } from 'react-admin';

const BanUserButton = ({ userId }) => {
    const dataProvider = useDataProvider();
    const handleClick = () => {
        dataProvider.update('users', { id: userId, data: { isBanned: true } });
    };
    return <Button label="Ban user" onClick={handleClick} />;
};

The Querying the API documentation lists all the hooks available for querying the API, as well as the options and return values for each.

React Query

React-admin uses TanStack Query to call the Data Provider. Specialized hooks like useGetOne use TanStack Query’s hooks under the hood and accept the same options.

You can use any of TanStack Query’s hooks in your code:

For instance, you can use useMutation to call the dataProvider.update() directly. This lets you track the mutation’s status and add side effects:

import { useDataProvider, useNotify } from 'react-admin';
import { useQuery } from '@tanstack/react-query';

const BanUserButton = ({ userId }) => {
    const dataProvider = useDataProvider();
    const notify = useNotify();
    const { mutate, isPending } = useMutation({
        mutationFn: () => dataProvider.update('users', { id: userId, data: { isBanned: true } }),
        onSuccess: () => notify('User banned'),
    });
    return <Button label="Ban user" onClick={() => mutate()} disabled={isPending} />;
};

Check out the TanStack Query documentation for more information on how to use it.

Local API Mirror

React-admin caches query data locally in the browser and automatically reuses it to answer future queries whenever possible. By structuring and indexing the data by resource name and ID, React-admin offers several advantages:

  • Stale-While-Revalidate: React-admin renders the UI immediately using cached data while fetching fresh data from the server in the background. Once the server response arrives, the UI seamlessly updates with the latest data.
  • Data Sharing Between Views: When navigating from a list view to a show view, React-admin reuses data from the list to render the show view instantly, eliminating the need to wait for the dataProvider.getOne() response.
  • Optimistic Updates: When a user deletes or updates a record, React-admin immediately updates the local cache to reflect the change, providing instant UI feedback. The server request follows, and if it fails, React-admin reverts the local data and notifies the user.
  • Auto Refresh: React-admin invalidates dependent queries after a successful mutation. TanStack Query then refetches the necessary data, ensuring the UI remains up-to-date automatically.

For example, when a user deletes a book in a list, React-admin immediately removes it, making the row disappear. After the API confirms the deletion, React-admin invalidates the list’s cache, refreshes it, and another record appears at the end of the list.

The local API mirror significantly enhances both the user experience (with a snappy and responsive UI) and the developer experience (by abstracting caching, invalidation, and optimistic updates).

Mutation Mode

React-admin provides three approaches for handling updates and deletions:

  • Undoable (default): React-admin updates the UI immediately and displays an undo button. During this time, it doesn’t send a request to the server. If the user clicks the undo button, React-admin restores the previous UI state and cancels the server request. If the user doesn’t click the undo button, it sends the request to the server after the delay.
  • Optimistic: React-admin updates the UI immediately and sends the request to the server simultaneously. If the server request fails, the UI is reverted to its previous state to maintain consistency.
  • Pessimistic: React-admin sends the request to the server first. After the server confirms success, the UI is updated. If the request fails, it displays an error message to inform the user.

Success message with undo

For each mutation hook or component, you can specify the mutation mode:

const DeletePostButton = ({ record }) => {
    const [deleteOne] = useDelete(
        'posts',
        { id: record.id },
        { mutationMode: 'pessimistic' }
    );
    const handleClick = () => deleteOne();
    return <Button label="Delete" onClick={handleClick} />;
};

For details, refer to the Querying the API chapter.

Custom Data Provider Methods

Your API backend may expose non-CRUD endpoints, e.g., for calling Remote Procedure Calls (RPC).

For instance, let’s say your API exposes an endpoint to ban a user based on its id:

POST /api/user/123/ban

The react-admin way to expose these endpoints to the app components is to add a custom method in the dataProvider:

import simpleRestDataProvider from 'ra-data-simple-rest';

const baseDataProvider = simpleRestDataProvider('http://path.to.my.api/');

export const dataProvider = {
    ...baseDataProvider,
    banUser: (userId: string) => {
        return fetch(`/api/user/${userId}/ban`, { method: 'POST' })
            .then(response => response.json());
    },
}

export interface MyDataProvider extends DataProvider {
    banUser: (userId: string) => Promise<Record<string, any>>;
}

Then you can use react-query’s useMutation hook to call the dataProvider.banUser() method:

import { useDataProvider } from 'react-admin';
import { useMutation } from '@tanstack/react-query';

import type { MyDataProvider } from './dataProvider';

const BanUserButton = ({ userId }: { userId: string }) => {
    const dataProvider = useDataProvider<MyDataProvider>();
    const { mutate, isPending } = useMutation({
        mutationFn: () => dataProvider.banUser(userId)
    });
    return <Button label="Ban" onClick={() => mutate()} disabled={isPending} />;
};

Check the Calling Custom Methods documentation for more details.

Authentication

The dataProvider often needs to send an authentication token in API requests. The authProvider manages the authentication process. Here’s how the two work together:

  1. The user logs in with their email and password
  2. React-admin calls authProvider.login() with these credentials.
  3. The authProvider sends the login request to the authentication backend.
  4. The backend validates the credentials and returns an authentication token.
  5. The authProvider stores the token in localStorage
  6. When making requests, the dataProvider reads the token from localStorage and adds it to the request headers.

You must implement the interaction between the authProvider and dataProvider. Here’s an example for the auth provider:

// in authProvider.js
const authProvider = {
    async login({ username, password })  {
        const request = new Request('https://mydomain.com/authenticate', {
            method: 'POST',
            body: JSON.stringify({ username, password }),
            headers: new Headers({ 'Content-Type': 'application/json' }),
        });
        let response;
        try {
            response = await fetch(request);
        } catch (_error) {
            throw new Error('Network error');
        }
        if (response.status < 200 || response.status >= 300) {
            throw new Error(response.statusText);
        }
        const { token } = await response.json();
        localStorage.setItem('token', token);
    },
    async logout() {
        localStorage.removeItem('token');
    },
    // ...
};

Many Data Providers, like simpleRestProvider, support authentication. Here’s how you can configure it to include the token:

// in dataProvider.js
import { fetchUtils } from 'react-admin';
import simpleRestProvider from 'ra-data-simple-rest';

const fetchJson = (url, options = {}) => {
    options.user = {
        authenticated: true,
        token: localStorage.getItem('token') // Include the token
    };
    return fetchUtils.fetchJson(url, options);
};
const dataProvider = simpleRestProvider('http://path.to.my.api/', fetchJson);

Check your Data Provider’s documentation for specific configuration options.

Relationships

React-admin simplifies working with relational APIs by managing related records at the component level. You can leverage relationship support without modifying your Data Provider or API.

For instance, let’s imagine an API exposing CRUD endpoints for books and authors:

┌──────────────┐       ┌────────────────┐
│ books        │       │ authors        │
│--------------│       │----------------│
│ id           │   ┌───│ id             │
│ author_id    │╾──┘   │ first_name     │
│ title        │       │ last_name      │
│ published_at │       │ date_of_birth  │
└──────────────┘       └────────────────┘

The Book show page should display a book title and the name of its author. In a server-side framework, you would issue a SQL query with a JOIN clause. In React-admin, components request only the data they need, and React-admin handles the relationship resolution.

const BookShow = () => (
    <Show>
        <SimpleShowLayout>
            <TextField source="id" />
            <TextField source="title" />
            <ReferenceField source="author_id" reference="authors" />
            <TextField source="year" />
        </SimpleShowLayout>
    </Show>
);

In the example above, two components call the Data Provider on mount:

  • The Show component calls dataProvider.getOne('books') and receives a book with an author_id field
  • The ReferenceField component reads the current book record and calls dataProvider.getOne('authors') using the author_id value

This approach improves the developer experience as you don’t need to build complex queries for each page. Components remain independent of each other and are easy to compose.

However, this cascade of Data Provider requests can appear inefficient regarding user-perceived performance. React-admin includes several optimizations to mitigate this:

  • Local API Mirror (see above)
  • Partial Rendering: React-admin first renders the page with the book data and updates it when the author data arrives. This ensures users see data as soon as possible.
  • Query Aggregation: React-admin intercepts all calls to dataProvider.getOne() for related data when a <ReferenceField> is used in a list. It aggregates and deduplicates the requested ids and issues a single dataProvider.getMany() request. This technique effectively addresses the n+1 query problem, reduces server queries, and accelerates list view rendering.
  • Smart Loading Indicators: <ReferenceField> renders blank placeholders during the first second to avoid layout shifts when the response arrives. If the response takes longer, React-admin shows a spinner to indicate progress while maintaining a smooth user experience.
  • Embedded Data and Prefetching: Data providers can return data from related resources in the same response as the requested resource. React-admin uses this feature to avoid additional network requests and to display related data immediately.

Even on complex pages that aggregate data from multiple resources, Reference components optimize API requests, reducing their number while ensuring users quickly see the data they need.

Relationship support in React-admin works out of the box with any API that provides foreign keys. No special configuration is required for your API or Data Provider.

Here is a list of react-admin’s relationship components:

To learn more about relationships, check out this tutorial: Handling Relationships in React Admin.

If a relationship component doesn’t fit your specific use case, you can always use a custom data provider method to fetch the required data.

Realtime

React-admin offers powerful realtime features to help you build collaborative applications based on the Publish / Subscribe (PubSub) pattern. The Realtime documentation explains how to use them.

These features are part of the Enterprise Edition.

Realtime Data Provider

The realtime features are backend agnostic. Just like CRUD operations, realtime operations rely on the data provider, using additional methods:

  • dataProvider.subscribe(topic, callback)
  • dataProvider.unsubscribe(topic, callback)
  • dataProvider.publish(topic, event) (optional - publication is often done server-side)

In addition, to support the lock features, the dataProvider must implement four more methods:

  • dataProvider.lock(resource, { id, identity, meta })
  • dataProvider.unlock(resource, { id, identity, meta })
  • dataProvider.getLock(resource, { id, meta })
  • dataProvider.getLocks(resource, { meta })

You can implement these features using any realtime backend, including:

Check the Realtime Data Provider documentation for more information and for helpers to build your own realtime data provider.

Realtime Hooks And Components

Once your data provider has enabled realtime features, you can use these hooks and components to build realtime applications:

Refer to the Realtime documentation for more information.