Fields For Relationships
React-admin provides numerous components, called ‘Reference’ components, to deal with relationships between records. In fact, react-admin and the dataProvider
interface are actually designed to facilitate the implementation of relational features such as:
- showing the comments related to a post
- showing the author of a post
- choosing the author of a post
- adding tags to a post
React-admin handles relationships regardless of the capacity of the API to manage relationships. As long as you can provide a dataProvider
for your API, all the relational features will work.
React-admin provides helpers to fetch related records, depending on the type of relationship, and how the API implements it.
One-To-Many
When one record has many related records, this is called a one-to-many relationship. For instance, if an author has written several books, authors
has a one-to-many relationship with books
.
To fetch the books of an author with react-admin, you can use:
<ReferenceManyField>
when the API uses a foreign key (e.g. each book has anauthor_id
field)<ReferenceArrayField>
when the API uses an array of foreign keys (e.g. each author has abook_ids
field)<ArrayField>
when the API embeds an array of records (e.g. each author has abooks
field)
Many-To-One
On the other hand, many-to-one relationships are the opposite of one-to-many relationships (e.g. each book has one author). To fetch the author of a book, you can use:
<ReferenceField>
when the API uses a foreign key (e.g. each book has anauthor_id
field)- Deep Field Source, when the API embeds the related record (e.g. each book has an
author
field containing an object)
Other kinds of relationships often reduce to one-to-many relationships.
One-To-One
For instance, one-to-one relationships (e.g. a book has one book_detail
) are a special type of one-to-many relationship with a cardinality of 1. To fetch the details of a book, you can use:
<ReferenceOneField>
when the API uses a foreign key (e.g. eachbook_detail
has abook_id
field)<ReferenceField>
when the API uses a reverse foreign key (e.g. eachbook
has abook_detail_id
field)- Deep Field Source, when the API embeds the related record (e.g. each book has a
book_detail
field containing an object)
Many-To-Many
Also, many-to-many relationships are often modeled as two successive one-to-many relationships. For instance, if a book is co-authored by several people, we can model this as a one-to-many relationship between the book and the book_authors, and a one-to-many relationship between the book_authors and the authors. To fetch the books of an author, use:
<ReferenceManyToManyField>
when the API uses a join table (e.g. abook_authors
table with bothbook_id
andauthor_id
fields)<ReferenceArrayField>
when the API uses an array of foreign keys (e.g. each author has abook_ids
field, and each book has anauthor_ids
field)<ArrayField>
, when the API embeds an array of records (e.g. each author has abooks
field, and each book has anauthors
field)
Deep Field Source
When a many-to-one relationship (e.g. the author of a book) is materialized by an embedded object, then you don’t need a Reference field - you can just use any regular field, and use a compound field name (e.g. “author.first_name”).
┌──────────────────┐
│ books │
│------------------│
│ id │
│ author │
│ └ first_name │
│ └ last_name │
│ └ date_of_birth │
│ title │
│ published_at │
└──────────────────┘
Here is an example usage:
const BookShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="title" />
<DateField source="published_at" />
<FunctionField
label="Author"
render={record => `${record.author.first_name} ${record.author.last_name}`}
/>
<DateField label="Author DOB" source="author.date_of_birth" />
</SimpleShowLayout>
</Show>
);
<ArrayField>
This field fetches a one-to-many relationship, e.g. the books of an author, when using an array embedded objects.
┌───────────────────────────┐
│ author │
│---------------------------│
│ id │
│ first_name │
│ last_name │
│ date_of_birth │
│ books │
│ └ { title, published_at} │
│ └ { title, published_at} │
│ └ { title, published_at} │
└───────────────────────────┘
Here is an example usage:
const AuthorShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="first_name" />
<TextField source="last_name" />
<DateField source="date_of_birth" />
<ArrayField source="books">
<Datagrid>
<TextField source="title" />
<DateField source="published_at" />
</Datagrid>
</ArrayField>
</SimpleShowLayout>
</Show>
);
<ArrayField>
creates a ListContext
with the embedded records, so you can use any component relying on this context (<Datagrid>
, <SimpleList>
, etc.).
<ReferenceField>
This field fetches a many-to-one relationship, e.g. the author of a book, when using a foreign key.
┌──────────────┐ ┌────────────────┐
│ books │ │ authors │
│--------------│ │----------------│
│ id │ ┌───│ id │
│ author_id │╾──┘ │ first_name │
│ title │ │ last_name │
│ published_at │ │ date_of_birth │
└──────────────┘ └────────────────┘
Here is an example usage:
const BookShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="title" />
<DateField source="published_at" />
<ReferenceField label="Author" source="author_id" reference="authors">
<FunctionField render={record => record && `${record.first_name} ${record.last_name}`} />
</ReferenceField>
<ReferenceField label="Author DOB" source="author_id" reference="authors">
<DateField source="date_of_birth" />
</ReferenceField>
</SimpleShowLayout>
</Show>
);
<ReferenceField>
uses the current record
(a book in this example) to read the id of the reference using the foreign key (author_id
). Then, it uses dataProvider.getOne('authors', { id })
fetch the related author.
<ReferenceField>
creates a RecordContext
with the reference record, so you can use any component relying on this context (<TextField>
, <SimpleShowLayout>
, etc.).
Tip: You don’t need to worry about the fact that these components calls <ReferenceField>
twice on the same table. React-admin will only make one call to the API.
This is fine, but what if you need to display the author details for a list of books?
const BookList = () => (
<List>
<Datagrid>
<TextField source="title" />
<DateField source="published_at" />
<ReferenceField label="Author" source="author_id" reference="authors">
<FunctionField render={record => `${record.first_name} ${record.last_name}`} />
</ReferenceField>
<ReferenceField label="Author DOB" source="author_id" reference="authors">
<DateField source="date_of_birth" />
</ReferenceField>
</Datagrid>
</List>
);
If each row of the book list triggers one call to dataProvider.getOne('authors', { id })
, and if the list counts many rows (say, 25), the app will be very slow - and possibly blocked by the API for abusive usage. This is another version of the dreaded “n+1 problem”.
Fortunately, <ReferenceField>
aggregates and deduplicates all the renders made in a page, and creates an optimised request. In the example above, instead of n calls to dataProvider.getOne('authors', { id })
, the book list will make one call to dataProvider.getMany('authors', { ids })
.
<ReferenceManyField>
This field fetches a one-to-many relationship, e.g. the books of an author, when using a foreign key.
┌────────────────┐ ┌──────────────┐
│ authors │ │ books │
│----------------│ │--------------│
│ id │───┐ │ id │
│ first_name │ └──╼│ author_id │
│ last_name │ │ title │
│ date_of_birth │ │ published_at │
└────────────────┘ └──────────────┘
Here is an example usage:
const AuthorShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="first_name" />
<TextField source="last_name" />
<DateField source="date_of_birth" />
<ReferenceManyField reference="books" target="author_id">
<Datagrid>
<TextField source="title" />
<DateField source="published_at" />
</Datagrid>
</ReferenceManyField>
</SimpleShowLayout>
</Show>
);
<ReferenceManyField>
uses the current record
(an author in this example) to build a filter for the list of books on the foreign key field (author_id
). Then, it uses dataProvider.getManyReference('books', { target: 'author_id', id: book.id })
fetch the related books.
<ReferenceManyField>
creates a ListContext
with the related records, so you can use any component relying on this context (<Datagrid>
, <SimpleList>
, etc.).
Tip: For many APIs, there is no difference between dataProvider.getList()
and dataProvider.getManyReference()
. The latter is a specialized version of the former, with a predefined filter
. But some APIs expose related records as a sub-route, and therefore need a special method to fetch them. For instance, the books of an author can be exposed via the following endpoint:
GET /authors/:id/books
That’s why <ReferenceManyField>
uses the getManyReference()
method instead of getList()
.
<ReferenceArrayField>
This field fetches a one-to-many relationship, e.g. the books of an author, when using an array of foreign keys.
┌────────────────┐ ┌──────────────┐
│ authors │ │ books │
│----------------│ │--------------│
│ id │ ┌───│ id │
│ first_name │ │ │ title │
│ last_name │ │ │ published_at │
│ date_of_birth │ │ └──────────────┘
│ book_ids │╾──┘
└────────────────┘
Here is an example usage:
const AuthorShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="first_name" />
<TextField source="last_name" />
<DateField source="date_of_birth" />
<ReferenceArrayField reference="books" source="book_ids">
<Datagrid>
<TextField source="title" />
<DateField source="published_at" />
</Datagrid>
</ReferenceArrayField>
</SimpleShowLayout>
</Show>
);
<ReferenceArrayField>
reads the list of book_ids
in the current record
(an author in this example). Then, it uses dataProvider.getMany('books', { ids })
fetch the related books.
<ReferenceArrayField>
creates a ListContext
with the related records, so you can use any component relying on this context (<Datagrid>
, <SimpleList>
, etc.).
You can also use it in a List page:
const AuthorList = () => (
<List>
<Datagrid>
<TextField source="first_name" />
<TextField source="last_name" />
<DateField source="date_of_birth" />
<ReferenceArrayField reference="books" source="book_ids">
<SingleFieldList>
<TextField source="title" />
</SingleFieldList>
</ReferenceArrayField>
</Datagrid>
</List>
);
Just like for <ReferenceField>
, <ReferenceArrayField>
aggregates and deduplicates all the renders made in a page, and creates an optimised request. So for the entire list of authors, it will make only one call to dataProvider.getMany('books', { ids })
.
<ReferenceManyToManyField>
This Enterprise Edition field displays a many-to-many relationship implemented with two one-to-many relationships and a join table.
┌──────────────────┐ ┌──────────────┐ ┌───────────────┐
│ books │ │ book_authors │ │ authors │
│------------------│ │--------------│ │---------------│
│ id │───┐ │ id │ │ id │
│ title │ └──╼│ book_id │ ┌──│ first_name │
│ published_at │ │ author_id │╾──┘ │ last_name │
└──────────────────┘ │ is_public │ │ date_of_birth │
└──────────────┘ └───────────────┘
Here is how you would display the books of an author:
const AuthorShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="first_name" />
<TextField source="last_name" />
<DateField source="date_of_birth" />
<ReferenceManyToManyField
reference="books"
through="book_authors"
using="author_id,book_id"
>
<Datagrid>
<TextField source="title" />
<DateField source="published_at" />
</Datagrid>
</ReferenceManyToManyField>
<EditButton />
</SimpleShowLayout>
</Show>
);
And here is how you would display the authors of a book:
const BookShow = props => (
<Show>
<SimpleShowLayout>
<TextField source="title" />
<DateField source="published_at" />
<ReferenceManyToManyField
reference="authors"
through="book_authors"
using="book_id,author_id"
>
<Datagrid>
<FunctionField
label="Author"
render={record => `${record.first_name} ${record.last_name}`}
/>
<DateField source="date_of_birth" />
</Datagrid>
</ReferenceManyToManyField>
<EditButton />
</SimpleShowLayout>
</Show>
);
<ReferenceManyToManyField>
creates a ListContext
with the related records, so you can use any component relying on this context (<Datagrid>
, <SimpleList>
, etc.).
<ReferenceOneField>
This field fetches a one-to-one relationship, e.g. the details of a book, when using a foreign key.
┌──────────────┐ ┌──────────────┐
│ books │ │ book_details │
│--------------│ │--------------│
│ id │───┐ │ id │
│ title │ └──╼│ book_id │
│ published_at │ │ genre │
└──────────────┘ │ ISBN │
└──────────────┘
Here is how to use it:
const BookShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="title" />
<DateField source="published_at" />
<ReferenceOneField label="Genre" reference="book_details" target="book_id">
<TextField source="genre" />
</ReferenceOneField>
<ReferenceOneField label="ISBN" reference="book_details" target="book_id">
<TextField source="ISBN" />
</ReferenceOneField>
</SimpleShowLayout>
</Show>
);
<ReferenceOneField>
behaves like <ReferenceManyField>
: it uses the current record
(a book in this example) to build a filter for the book details with the foreign key (book_id
). Then, it uses dataProvider.getManyReference('book_details', { target: 'book_id', id: book.id })
to fetch the related details, and takes the first one.
<ReferenceOneField>
creates a RecordContext
with the reference record, so you can use any component relying on this context (<TextField>
, <SimpleShowLayout>
, etc.).
Tip: As with <ReferenceField>
, you can call <ReferenceOneField>
as many times as you need in the same component, react-admin will only make one call to dataProvider.getManyReference()
.
For the inverse relationships (the author linked to a biography), you can use a <ReferenceField>
.