From e3aecf1bc3f3c95763843c8d5b3bf776422b5f26 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Mon, 7 Jul 2025 13:54:38 +0200
Subject: [PATCH 01/41] add render props to ListBase component
---
docs/ListBase.md | 1 +
.../list/InfiniteListBase.stories.tsx | 39 ++++++++++++
.../src/controller/list/InfiniteListBase.tsx | 11 +++-
.../src/controller/list/ListBase.stories.tsx | 63 +++++++++++++++++++
.../ra-core/src/controller/list/ListBase.tsx | 17 ++++-
.../ra-ui-materialui/src/list/ListView.tsx | 2 +-
6 files changed, 128 insertions(+), 5 deletions(-)
diff --git a/docs/ListBase.md b/docs/ListBase.md
index 05770584c4e..ed6f92e3af7 100644
--- a/docs/ListBase.md
+++ b/docs/ListBase.md
@@ -69,6 +69,7 @@ The `` component accepts the same props as [`useListController`](./use
These are a subset of the props accepted by `` - only the props that change data fetching, and not the props related to the user interface.
In addition, `` renders its children components inside a `ListContext`. Check [the `` documentation](./List.md#children) for usage examples.
+Alternatively you can pass a render function to the props, in place of children. This function will receive the listContext as argument. Check [the `` documentation](./List.md#render) for usage examples.
## Security
diff --git a/packages/ra-core/src/controller/list/InfiniteListBase.stories.tsx b/packages/ra-core/src/controller/list/InfiniteListBase.stories.tsx
index a647d0d826c..2ba2a6443e5 100644
--- a/packages/ra-core/src/controller/list/InfiniteListBase.stories.tsx
+++ b/packages/ra-core/src/controller/list/InfiniteListBase.stories.tsx
@@ -246,6 +246,45 @@ DefaultTitle.argTypes = {
},
};
+export const WithRenderProps = () => (
+
+ {
+ const {
+ hasNextPage,
+ fetchNextPage,
+ isFetchingNextPage,
+ hasPreviousPage,
+ fetchPreviousPage,
+ isFetchingPreviousPage,
+ } = context;
+ return (
+
+ {hasPreviousPage && (
+ fetchPreviousPage()}
+ disabled={isFetchingPreviousPage}
+ >
+ Previous
+
+ )}
+ {hasNextPage && (
+ fetchNextPage()}
+ disabled={isFetchingNextPage}
+ >
+ Next
+
+ )}
+
+ );
+ }}
+ />
+
+);
+
const Title = () => {
const { defaultTitle } = useListContext();
const [locale, setLocale] = useLocaleState();
diff --git a/packages/ra-core/src/controller/list/InfiniteListBase.tsx b/packages/ra-core/src/controller/list/InfiniteListBase.tsx
index 6decf7ae8ab..f72803edfb4 100644
--- a/packages/ra-core/src/controller/list/InfiniteListBase.tsx
+++ b/packages/ra-core/src/controller/list/InfiniteListBase.tsx
@@ -3,6 +3,7 @@ import { ReactNode } from 'react';
import {
useInfiniteListController,
InfiniteListControllerProps,
+ InfiniteListControllerResult,
} from './useInfiniteListController';
import { OptionalResourceContextProvider } from '../../core';
import { RaRecord } from '../../types';
@@ -46,6 +47,7 @@ import { useIsAuthPending } from '../../auth';
*/
export const InfiniteListBase = ({
children,
+ render,
loading = null,
...props
}: InfiniteListBaseProps) => {
@@ -59,6 +61,12 @@ export const InfiniteListBase = ({
return loading;
}
+ if (!render && !children) {
+ throw new Error(
+ " requires either a 'render' prop or 'children' prop"
+ );
+ }
+
return (
// We pass props.resource here as we don't need to create a new ResourceContext if the props is not provided
@@ -83,6 +91,7 @@ export const InfiniteListBase = ({
export interface InfiniteListBaseProps
extends InfiniteListControllerProps {
- children: ReactNode;
loading?: ReactNode;
+ children?: ReactNode;
+ render?: (props: InfiniteListControllerResult) => ReactNode;
}
diff --git a/packages/ra-core/src/controller/list/ListBase.stories.tsx b/packages/ra-core/src/controller/list/ListBase.stories.tsx
index 4adf9b56375..b06e317c848 100644
--- a/packages/ra-core/src/controller/list/ListBase.stories.tsx
+++ b/packages/ra-core/src/controller/list/ListBase.stories.tsx
@@ -282,6 +282,69 @@ export const DefaultTitle = ({
);
+export const WithRenderProps = ({
+ dataProvider = defaultDataProvider,
+}: {
+ dataProvider?: DataProvider;
+}) => (
+
+ {
+ const {
+ data,
+ error,
+ isPending,
+ sort,
+ filterValues,
+ page,
+ perPage,
+ setPage,
+ total,
+ } = controllerProps;
+ const defaultValue = JSON.stringify({
+ page,
+ perPage,
+ sort,
+ filterValues,
+ });
+ if (isPending) {
+ return Loading...
;
+ }
+ if (error) {
+ return Error...
;
+ }
+
+ return (
+
+
setPage(page - 1)}
+ >
+ previous
+
+
+ Page {page} of {Math.ceil(total / perPage)}
+
+
= total / perPage}
+ onClick={() => setPage(page + 1)}
+ >
+ next
+
+
+ {data.map((record: any) => (
+ {record.title}
+ ))}
+
+
+ );
+ }}
+ >
+
+);
+
DefaultTitle.args = {
translations: 'default',
};
diff --git a/packages/ra-core/src/controller/list/ListBase.tsx b/packages/ra-core/src/controller/list/ListBase.tsx
index e5d14f4d1b4..684e15fdc6b 100644
--- a/packages/ra-core/src/controller/list/ListBase.tsx
+++ b/packages/ra-core/src/controller/list/ListBase.tsx
@@ -1,6 +1,10 @@
import * as React from 'react';
import { ReactNode } from 'react';
-import { useListController, ListControllerProps } from './useListController';
+import {
+ useListController,
+ ListControllerProps,
+ ListControllerResult,
+} from './useListController';
import { OptionalResourceContextProvider } from '../../core';
import { RaRecord } from '../../types';
import { ListContextProvider } from './ListContextProvider';
@@ -42,6 +46,7 @@ import { useIsAuthPending } from '../../auth';
*/
export const ListBase = ({
children,
+ render,
loading = null,
...props
}: ListBaseProps) => {
@@ -54,12 +59,17 @@ export const ListBase = ({
if (isAuthPending && !props.disableAuthentication) {
return loading;
}
+ if (!render && !children) {
+ throw new Error(
+ " requires either a 'render' prop or 'children' prop"
+ );
+ }
return (
// We pass props.resource here as we don't need to create a new ResourceContext if the props is not provided
- {children}
+ {render ? render(controllerProps) : children}
);
@@ -67,6 +77,7 @@ export const ListBase = ({
export interface ListBaseProps
extends ListControllerProps {
- children: ReactNode;
+ children?: ReactNode;
+ render?: (props: ListControllerResult) => ReactNode;
loading?: ReactNode;
}
diff --git a/packages/ra-ui-materialui/src/list/ListView.tsx b/packages/ra-ui-materialui/src/list/ListView.tsx
index e6cd323b79f..67fca2c1f02 100644
--- a/packages/ra-ui-materialui/src/list/ListView.tsx
+++ b/packages/ra-ui-materialui/src/list/ListView.tsx
@@ -191,7 +191,7 @@ export interface ListViewProps {
*
* );
*/
- children: ReactNode;
+ children?: ReactNode;
/**
* The component used to display the list. Defaults to .
From 14652d49f2d5a401d665c4e14a0e3b1bf268ccd8 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Mon, 7 Jul 2025 13:59:25 +0200
Subject: [PATCH 02/41] add render props to InfiniteListBase component
---
.../list/InfiniteListBase.stories.tsx | 61 ++++++++++++++-----
.../src/controller/list/InfiniteListBase.tsx | 2 +-
2 files changed, 46 insertions(+), 17 deletions(-)
diff --git a/packages/ra-core/src/controller/list/InfiniteListBase.stories.tsx b/packages/ra-core/src/controller/list/InfiniteListBase.stories.tsx
index 2ba2a6443e5..3c23bb6a8c8 100644
--- a/packages/ra-core/src/controller/list/InfiniteListBase.stories.tsx
+++ b/packages/ra-core/src/controller/list/InfiniteListBase.stories.tsx
@@ -259,25 +259,54 @@ export const WithRenderProps = () => (
hasPreviousPage,
fetchPreviousPage,
isFetchingPreviousPage,
+ setFilters,
+ isPending,
+ setSort,
+ sort,
+ filterValues,
+ data,
} = context;
+
+ if (isPending) {
+ return Loading...
;
+ }
+ const toggleSort = () => {
+ setSort({
+ field: sort.field === 'title' ? 'id' : 'title',
+ order: 'ASC',
+ });
+ };
+ const toggleFilter = () => {
+ setFilters(filterValues.q ? {} : { q: 'The ' });
+ };
+
return (
- {hasPreviousPage && (
-
fetchPreviousPage()}
- disabled={isFetchingPreviousPage}
- >
- Previous
-
- )}
- {hasNextPage && (
-
fetchNextPage()}
- disabled={isFetchingNextPage}
- >
- Next
-
- )}
+
+ {hasPreviousPage && (
+ fetchPreviousPage()}
+ disabled={isFetchingPreviousPage}
+ >
+ Previous
+
+ )}
+ {hasNextPage && (
+ fetchNextPage()}
+ disabled={isFetchingNextPage}
+ >
+ Next
+
+ )}
+
+
Toggle Sort
+
Toggle Filter
+
+ {data?.map((record: any) => (
+ {record.title}
+ ))}
+
);
}}
diff --git a/packages/ra-core/src/controller/list/InfiniteListBase.tsx b/packages/ra-core/src/controller/list/InfiniteListBase.tsx
index f72803edfb4..b9c7e511860 100644
--- a/packages/ra-core/src/controller/list/InfiniteListBase.tsx
+++ b/packages/ra-core/src/controller/list/InfiniteListBase.tsx
@@ -82,7 +82,7 @@ export const InfiniteListBase = ({
controllerProps.isFetchingPreviousPage,
}}
>
- {children}
+ {render ? render(controllerProps) : children}
From 4619f1655e6143e1a0e8a5540046072853d0486c Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Mon, 7 Jul 2025 14:49:58 +0200
Subject: [PATCH 03/41] add tests
---
.../src/controller/list/InfiniteListBase.spec.tsx | 7 +++++++
.../ra-core/src/controller/list/ListBase.spec.tsx | 13 +++++++++++++
2 files changed, 20 insertions(+)
diff --git a/packages/ra-core/src/controller/list/InfiniteListBase.spec.tsx b/packages/ra-core/src/controller/list/InfiniteListBase.spec.tsx
index d288bc4c0da..c471272b73e 100644
--- a/packages/ra-core/src/controller/list/InfiniteListBase.spec.tsx
+++ b/packages/ra-core/src/controller/list/InfiniteListBase.spec.tsx
@@ -5,6 +5,7 @@ import {
DefaultTitle,
NoAuthProvider,
WithAuthProviderNoAccessControl,
+ WithRenderProps,
} from './InfiniteListBase.stories';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { testDataProvider } from '../../dataProvider';
@@ -127,4 +128,10 @@ describe('InfiniteListBase', () => {
fireEvent.click(screen.getByText('FR'));
await screen.findByText('Liste des livres (fr)');
});
+
+ it('should allow render props', async () => {
+ render( );
+ await screen.findByText('War and Peace');
+ expect(screen.queryByText('Loading...')).toBeNull();
+ });
});
diff --git a/packages/ra-core/src/controller/list/ListBase.spec.tsx b/packages/ra-core/src/controller/list/ListBase.spec.tsx
index df80b38d36e..a4785fcd659 100644
--- a/packages/ra-core/src/controller/list/ListBase.spec.tsx
+++ b/packages/ra-core/src/controller/list/ListBase.spec.tsx
@@ -5,6 +5,7 @@ import {
DefaultTitle,
NoAuthProvider,
WithAuthProviderNoAccessControl,
+ WithRenderProps,
} from './ListBase.stories';
import { testDataProvider } from '../../dataProvider';
@@ -103,4 +104,16 @@ describe('ListBase', () => {
fireEvent.click(screen.getByText('FR'));
await screen.findByText('Liste des livres (fr)');
});
+
+ it('should allow to use render props', async () => {
+ const dataProvider = testDataProvider({
+ // @ts-ignore
+ getList: jest.fn(() =>
+ Promise.resolve({ data: [{ id: 1, title: 'Hello' }], total: 1 })
+ ),
+ });
+ render( );
+ expect(dataProvider.getList).toHaveBeenCalled();
+ await screen.findByText('Hello');
+ });
});
From 8ce945006586bd91f7fc4e37177c58f2ea40244f Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Mon, 7 Jul 2025 15:23:48 +0200
Subject: [PATCH 04/41] add render props to EditBase component
---
docs/EditBase.md | 1 +
.../src/controller/edit/EditBase.spec.tsx | 43 ++++++++++++++++---
.../src/controller/edit/EditBase.stories.tsx | 30 +++++++++++++
.../ra-core/src/controller/edit/EditBase.tsx | 18 ++++++--
4 files changed, 82 insertions(+), 10 deletions(-)
diff --git a/docs/EditBase.md b/docs/EditBase.md
index 0d820c6a258..ae58a4e2590 100644
--- a/docs/EditBase.md
+++ b/docs/EditBase.md
@@ -47,6 +47,7 @@ export const BookEdit = () => (
You can customize the `` component using the following props, documented in the `` component:
* `children`: the components that renders the form
+* `render`: alternative to children, a function that takes the EditController context and renders the form
* [`disableAuthentication`](./Edit.md#disableauthentication): disable the authentication check
* [`id`](./Edit.md#id): the id of the record to edit
* [`mutationMode`](./Edit.md#mutationmode): switch to optimistic or pessimistic mutations (undoable by default)
diff --git a/packages/ra-core/src/controller/edit/EditBase.spec.tsx b/packages/ra-core/src/controller/edit/EditBase.spec.tsx
index ae241a8af1e..b9cede99671 100644
--- a/packages/ra-core/src/controller/edit/EditBase.spec.tsx
+++ b/packages/ra-core/src/controller/edit/EditBase.spec.tsx
@@ -8,13 +8,14 @@ import {
DefaultTitle,
NoAuthProvider,
WithAuthProviderNoAccessControl,
+ WithRenderProps,
} from './EditBase.stories';
describe('EditBase', () => {
it('should give access to the save function', async () => {
const dataProvider = testDataProvider({
- // @ts-ignore
getOne: () =>
+ // @ts-ignore
Promise.resolve({ data: { id: 12, test: 'previous' } }),
update: jest.fn((_, { id, data, previousData }) =>
Promise.resolve({ data: { id, ...previousData, ...data } })
@@ -44,8 +45,8 @@ describe('EditBase', () => {
it('should allow to override the onSuccess function', async () => {
const dataProvider = testDataProvider({
- // @ts-ignore
getOne: () =>
+ // @ts-ignore
Promise.resolve({ data: { id: 12, test: 'previous' } }),
update: jest.fn((_, { id, data, previousData }) =>
Promise.resolve({ data: { id, ...previousData, ...data } })
@@ -84,8 +85,8 @@ describe('EditBase', () => {
it('should allow to override the onSuccess function at call time', async () => {
const dataProvider = testDataProvider({
- // @ts-ignore
getOne: () =>
+ // @ts-ignore
Promise.resolve({ data: { id: 12, test: 'previous' } }),
update: jest.fn((_, { id, data, previousData }) =>
Promise.resolve({ data: { id, ...previousData, ...data } })
@@ -128,8 +129,8 @@ describe('EditBase', () => {
it('should allow to override the onError function', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
const dataProvider = testDataProvider({
- // @ts-ignore
getOne: () =>
+ // @ts-ignore
Promise.resolve({ data: { id: 12, test: 'previous' } }),
// @ts-ignore
update: jest.fn(() => Promise.reject({ message: 'test' })),
@@ -162,8 +163,8 @@ describe('EditBase', () => {
it('should allow to override the onError function at call time', async () => {
const dataProvider = testDataProvider({
- // @ts-ignore
getOne: () =>
+ // @ts-ignore
Promise.resolve({ data: { id: 12, test: 'previous' } }),
// @ts-ignore
update: jest.fn(() => Promise.reject({ message: 'test' })),
@@ -199,8 +200,8 @@ describe('EditBase', () => {
it('should allow to override the transform function', async () => {
const dataProvider = testDataProvider({
- // @ts-ignore
getOne: () =>
+ // @ts-ignore
Promise.resolve({ data: { id: 12, test: 'previous' } }),
update: jest.fn((_, { id, data, previousData }) =>
Promise.resolve({ data: { id, ...previousData, ...data } })
@@ -239,8 +240,8 @@ describe('EditBase', () => {
it('should allow to override the transform function at call time', async () => {
const dataProvider = testDataProvider({
- // @ts-ignore
getOne: () =>
+ // @ts-ignore
Promise.resolve({ data: { id: 12, test: 'previous' } }),
update: jest.fn((_, { id, data, previousData }) =>
Promise.resolve({ data: { id, ...previousData, ...data } })
@@ -376,4 +377,32 @@ describe('EditBase', () => {
fireEvent.click(screen.getByText('FR'));
await screen.findByText("Modifier l'article Hello (fr)");
});
+
+ it('should allow renderProp', async () => {
+ const dataProvider = testDataProvider({
+ getOne: () =>
+ // @ts-ignore
+ Promise.resolve({ data: { id: 12, test: 'Hello' } }),
+ update: jest.fn((_, { id, data, previousData }) =>
+ Promise.resolve({ data: { id, ...previousData, ...data } })
+ ),
+ });
+ render(
+
+ );
+ await screen.findByText('12');
+ await screen.findByText('Hello');
+ fireEvent.click(screen.getByText('save'));
+
+ await waitFor(() => {
+ expect(dataProvider.update).toHaveBeenCalledWith('posts', {
+ id: 12,
+ data: { test: 'test' },
+ previousData: { id: 12, test: 'Hello' },
+ });
+ });
+ });
});
diff --git a/packages/ra-core/src/controller/edit/EditBase.stories.tsx b/packages/ra-core/src/controller/edit/EditBase.stories.tsx
index 37599384e09..10414473984 100644
--- a/packages/ra-core/src/controller/edit/EditBase.stories.tsx
+++ b/packages/ra-core/src/controller/edit/EditBase.stories.tsx
@@ -16,6 +16,7 @@ import {
mergeTranslations,
useEditContext,
useLocaleState,
+ MutationMode,
} from '../..';
export default {
@@ -149,6 +150,35 @@ export const AccessControl = ({
);
+export const WithRenderProps = ({
+ dataProvider = defaultDataProvider,
+ mutationMode = 'optimistic',
+}: {
+ dataProvider?: DataProvider;
+ mutationMode?: MutationMode;
+}) => (
+
+ {
+ const handleClick = () => {
+ if (!save) return;
+
+ save({ test: 'test' });
+ };
+ return (
+ <>
+ {record?.id}
+ {record?.test}
+ save
+ >
+ );
+ }}
+ />
+
+);
+
const defaultDataProvider = testDataProvider({
getOne: () =>
// @ts-ignore
diff --git a/packages/ra-core/src/controller/edit/EditBase.tsx b/packages/ra-core/src/controller/edit/EditBase.tsx
index 97cd99d717a..c1015e7e6cc 100644
--- a/packages/ra-core/src/controller/edit/EditBase.tsx
+++ b/packages/ra-core/src/controller/edit/EditBase.tsx
@@ -2,7 +2,11 @@ import * as React from 'react';
import { ReactNode } from 'react';
import { RaRecord } from '../../types';
-import { useEditController, EditControllerProps } from './useEditController';
+import {
+ useEditController,
+ EditControllerProps,
+ EditControllerResult,
+} from './useEditController';
import { EditContextProvider } from './EditContextProvider';
import { OptionalResourceContextProvider } from '../../core';
import { useIsAuthPending } from '../../auth';
@@ -38,6 +42,7 @@ import { useIsAuthPending } from '../../auth';
*/
export const EditBase = ({
children,
+ render,
loading = null,
...props
}: EditBaseProps) => {
@@ -52,11 +57,17 @@ export const EditBase = ({
return loading;
}
+ if (!render && !children) {
+ throw new Error(
+ " requires either a 'render' prop or 'children' prop"
+ );
+ }
+
return (
// We pass props.resource here as we don't need to create a new ResourceContext if the props is not provided
- {children}
+ {render ? render(controllerProps) : children}
);
@@ -66,6 +77,7 @@ export interface EditBaseProps<
RecordType extends RaRecord = RaRecord,
ErrorType = Error,
> extends EditControllerProps {
- children: ReactNode;
+ children?: ReactNode;
+ render?: (props: EditControllerResult) => ReactNode;
loading?: ReactNode;
}
From 6511b2e6c3b62791531567b679d84f7674775e71 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Mon, 7 Jul 2025 17:01:52 +0200
Subject: [PATCH 05/41] add render props to CreateBase component
---
docs/CreateBase.md | 1 +
.../src/controller/create/CreateBase.spec.tsx | 19 +++++++++++++++
.../controller/create/CreateBase.stories.tsx | 23 +++++++++++++++++++
.../src/controller/create/CreateBase.tsx | 13 +++++++++--
4 files changed, 54 insertions(+), 2 deletions(-)
diff --git a/docs/CreateBase.md b/docs/CreateBase.md
index 6edae3d4d9d..80d667f2bb5 100644
--- a/docs/CreateBase.md
+++ b/docs/CreateBase.md
@@ -46,6 +46,7 @@ export const BookCreate = () => (
You can customize the `` component using the following props, documented in the `` component:
* `children`: the components that renders the form
+* `render`: alternative to children, a function that takes the EditController context and renders the form
* [`disableAuthentication`](./Create.md#disableauthentication): disable the authentication check
* [`mutationMode`](./Create.md#mutationmode): Switch to optimistic or undoable mutations (pessimistic by default)
* [`mutationOptions`](./Create.md#mutationoptions): options for the `dataProvider.create()` call
diff --git a/packages/ra-core/src/controller/create/CreateBase.spec.tsx b/packages/ra-core/src/controller/create/CreateBase.spec.tsx
index 8c47b1c99be..09ac36cbaab 100644
--- a/packages/ra-core/src/controller/create/CreateBase.spec.tsx
+++ b/packages/ra-core/src/controller/create/CreateBase.spec.tsx
@@ -8,6 +8,7 @@ import {
DefaultTitle,
NoAuthProvider,
WithAuthProviderNoAccessControl,
+ WithRenderProp,
} from './CreateBase.stories';
describe('CreateBase', () => {
@@ -283,4 +284,22 @@ describe('CreateBase', () => {
fireEvent.click(screen.getByText('FR'));
await screen.findByText('Créer un article (fr)');
});
+
+ it('should allow render props', async () => {
+ const dataProvider = testDataProvider({
+ // @ts-ignore
+ create: jest.fn((_, { data }) =>
+ Promise.resolve({ data: { id: 1, ...data } })
+ ),
+ });
+
+ render( );
+ fireEvent.click(screen.getByText('save'));
+
+ await waitFor(() => {
+ expect(dataProvider.create).toHaveBeenCalledWith('posts', {
+ data: { test: 'test' },
+ });
+ });
+ });
});
diff --git a/packages/ra-core/src/controller/create/CreateBase.stories.tsx b/packages/ra-core/src/controller/create/CreateBase.stories.tsx
index bf44009544e..e0622af12e3 100644
--- a/packages/ra-core/src/controller/create/CreateBase.stories.tsx
+++ b/packages/ra-core/src/controller/create/CreateBase.stories.tsx
@@ -148,6 +148,29 @@ export const AccessControl = ({
);
+export const WithRenderProp = ({
+ dataProvider = defaultDataProvider,
+ callTimeOptions,
+ ...props
+}: {
+ dataProvider?: DataProvider;
+ callTimeOptions?: SaveHandlerCallbacks;
+} & Partial) => (
+
+ {
+ const handleClick = () => {
+ if (!save) return;
+ save({ test: 'test' }, callTimeOptions);
+ };
+
+ return save ;
+ }}
+ />
+
+);
+
const defaultDataProvider = testDataProvider({
// @ts-ignore
create: (_, { data }) => Promise.resolve({ data: { id: 1, ...data } }),
diff --git a/packages/ra-core/src/controller/create/CreateBase.tsx b/packages/ra-core/src/controller/create/CreateBase.tsx
index 69d6f9569ed..f9e8b1d06d0 100644
--- a/packages/ra-core/src/controller/create/CreateBase.tsx
+++ b/packages/ra-core/src/controller/create/CreateBase.tsx
@@ -3,6 +3,7 @@ import { ReactNode } from 'react';
import {
useCreateController,
CreateControllerProps,
+ CreateControllerResult,
} from './useCreateController';
import { CreateContextProvider } from './CreateContextProvider';
import { Identifier, RaRecord } from '../../types';
@@ -44,6 +45,7 @@ export const CreateBase = <
MutationOptionsError = Error,
>({
children,
+ render,
loading = null,
...props
}: CreateBaseProps) => {
@@ -62,11 +64,17 @@ export const CreateBase = <
return loading;
}
+ if (!render && !children) {
+ throw new Error(
+ ' requires either a `render` prop or `children` prop'
+ );
+ }
+
return (
// We pass props.resource here as we don't need to create a new ResourceContext if the props is not provided
- {children}
+ {render ? render(controllerProps) : children}
);
@@ -81,6 +89,7 @@ export interface CreateBaseProps<
MutationOptionsError,
ResultRecordType
> {
- children: ReactNode;
+ children?: ReactNode;
+ render?: (props: CreateControllerResult) => ReactNode;
loading?: ReactNode;
}
From bf3897d59364c6b267b911ce35db6455b5e797f5 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Mon, 7 Jul 2025 17:16:35 +0200
Subject: [PATCH 06/41] add render props to ShowBase component
---
docs/CreateBase.md | 2 +-
docs/ShowBase.md | 3 ++-
.../src/controller/show/ShowBase.spec.tsx | 13 +++++++++++++
.../src/controller/show/ShowBase.stories.tsx | 17 +++++++++++++++++
.../ra-core/src/controller/show/ShowBase.tsx | 18 +++++++++++++++---
5 files changed, 48 insertions(+), 5 deletions(-)
diff --git a/docs/CreateBase.md b/docs/CreateBase.md
index 80d667f2bb5..99060b0c9c3 100644
--- a/docs/CreateBase.md
+++ b/docs/CreateBase.md
@@ -46,7 +46,7 @@ export const BookCreate = () => (
You can customize the `` component using the following props, documented in the `` component:
* `children`: the components that renders the form
-* `render`: alternative to children, a function that takes the EditController context and renders the form
+* `render`: alternative to children, a function that takes the CreateController context and renders the form
* [`disableAuthentication`](./Create.md#disableauthentication): disable the authentication check
* [`mutationMode`](./Create.md#mutationmode): Switch to optimistic or undoable mutations (pessimistic by default)
* [`mutationOptions`](./Create.md#mutationoptions): options for the `dataProvider.create()` call
diff --git a/docs/ShowBase.md b/docs/ShowBase.md
index 61ecdbc58f3..514790dab9d 100644
--- a/docs/ShowBase.md
+++ b/docs/ShowBase.md
@@ -62,7 +62,8 @@ const App = () => (
| Prop | Required | Type | Default | Description
|------------------|----------|-------------------|---------|--------------------------------------------------------
-| `children` | Required | `ReactNode` | | The components rendering the record fields
+| `children` | Optional | `ReactNode` | | The components rendering the record fields
+| `children` | Optional | `(props: ShowControllerResult) => ReactNode` | | Alternative to children, a function that takes the ShowController context and renders the form
| `disable Authentication` | Optional | `boolean` | | Set to `true` to disable the authentication check
| `empty WhileLoading` | Optional | `boolean` | | Set to `true` to return `null` while the list is loading
| `id` | Optional | `string` | | The record identifier. If not provided, it will be deduced from the URL
diff --git a/packages/ra-core/src/controller/show/ShowBase.spec.tsx b/packages/ra-core/src/controller/show/ShowBase.spec.tsx
index 71f9db96fb0..7429cf5cfc9 100644
--- a/packages/ra-core/src/controller/show/ShowBase.spec.tsx
+++ b/packages/ra-core/src/controller/show/ShowBase.spec.tsx
@@ -8,6 +8,7 @@ import {
DefaultTitle,
NoAuthProvider,
WithAuthProviderNoAccessControl,
+ WithRenderProp,
} from './ShowBase.stories';
describe('ShowBase', () => {
@@ -105,4 +106,16 @@ describe('ShowBase', () => {
fireEvent.click(screen.getByText('FR'));
await screen.findByText("Détails de l'article Hello (fr)");
});
+
+ it('should support render prop', async () => {
+ const dataProvider = testDataProvider({
+ // @ts-ignore
+ getOne: jest.fn(() =>
+ Promise.resolve({ data: { id: 12, test: 'Hello' } })
+ ),
+ });
+ render( );
+ expect(dataProvider.getOne).toHaveBeenCalled();
+ await screen.findByText('Hello');
+ });
});
diff --git a/packages/ra-core/src/controller/show/ShowBase.stories.tsx b/packages/ra-core/src/controller/show/ShowBase.stories.tsx
index 68533e85c81..adc42bc74e2 100644
--- a/packages/ra-core/src/controller/show/ShowBase.stories.tsx
+++ b/packages/ra-core/src/controller/show/ShowBase.stories.tsx
@@ -145,6 +145,23 @@ export const AccessControl = ({
);
+export const WithRenderProp = ({
+ dataProvider = defaultDataProvider,
+ ...props
+}: {
+ dataProvider?: DataProvider;
+} & Partial) => (
+
+ {
+ return {record?.test}
;
+ }}
+ />
+
+);
+
const defaultDataProvider = testDataProvider({
getOne: () =>
// @ts-ignore
diff --git a/packages/ra-core/src/controller/show/ShowBase.tsx b/packages/ra-core/src/controller/show/ShowBase.tsx
index 7b221c1715a..264a07b7866 100644
--- a/packages/ra-core/src/controller/show/ShowBase.tsx
+++ b/packages/ra-core/src/controller/show/ShowBase.tsx
@@ -1,7 +1,11 @@
import * as React from 'react';
import { RaRecord } from '../../types';
-import { useShowController, ShowControllerProps } from './useShowController';
+import {
+ useShowController,
+ ShowControllerProps,
+ ShowControllerResult,
+} from './useShowController';
import { ShowContextProvider } from './ShowContextProvider';
import { OptionalResourceContextProvider } from '../../core';
import { useIsAuthPending } from '../../auth';
@@ -37,6 +41,7 @@ import { useIsAuthPending } from '../../auth';
*/
export const ShowBase = ({
children,
+ render,
loading = null,
...props
}: ShowBaseProps) => {
@@ -51,11 +56,17 @@ export const ShowBase = ({
return loading;
}
+ if (!render && !children) {
+ throw new Error(
+ ' requires either a `render` prop or `children` prop'
+ );
+ }
+
return (
// We pass props.resource here as we don't need to create a new ResourceContext if the props is not provided
- {children}
+ {render ? render(controllerProps) : children}
);
@@ -63,6 +74,7 @@ export const ShowBase = ({
export interface ShowBaseProps
extends ShowControllerProps {
- children: React.ReactNode;
+ children?: React.ReactNode;
+ render?: (props: ShowControllerResult) => React.ReactNode;
loading?: React.ReactNode;
}
From e03818e54cf4c6bd6d578ab7a68df88d86a50566 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Mon, 7 Jul 2025 18:01:47 +0200
Subject: [PATCH 07/41] add render props to ReferenceFIeldBase component
---
.../field/ReferenceFieldBase.spec.tsx | 71 ++++++++++++++++++-
.../field/ReferenceFieldBase.stories.tsx | 48 ++++++++++++-
.../controller/field/ReferenceFieldBase.tsx | 13 +++-
3 files changed, 124 insertions(+), 8 deletions(-)
diff --git a/packages/ra-core/src/controller/field/ReferenceFieldBase.spec.tsx b/packages/ra-core/src/controller/field/ReferenceFieldBase.spec.tsx
index 6bdb4d49541..2f201cb15ca 100644
--- a/packages/ra-core/src/controller/field/ReferenceFieldBase.spec.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceFieldBase.spec.tsx
@@ -5,7 +5,13 @@ import { CoreAdminContext } from '../../core/CoreAdminContext';
import { useResourceContext } from '../../core/useResourceContext';
import { testDataProvider } from '../../dataProvider';
import { ReferenceFieldBase } from './ReferenceFieldBase';
-import { Error, Loading, Meta } from './ReferenceFieldBase.stories';
+import {
+ Errored,
+ Loading,
+ Meta,
+ WithRenderProp,
+} from './ReferenceFieldBase.stories';
+import { WithRenderProps } from '../edit/EditBase.stories';
describe(' ', () => {
beforeAll(() => {
@@ -17,7 +23,7 @@ describe(' ', () => {
.mockImplementationOnce(() => {})
.mockImplementationOnce(() => {});
- render( );
+ render( );
await waitFor(() => {
expect(screen.queryByText('Error')).not.toBeNull();
});
@@ -40,8 +46,8 @@ describe(' ', () => {
return {resource}
;
};
const dataProvider = testDataProvider({
- // @ts-ignore
getList: () =>
+ // @ts-ignore
Promise.resolve({ data: [{ id: 1 }, { id: 2 }], total: 2 }),
});
render(
@@ -65,6 +71,7 @@ describe(' ', () => {
const dataProvider = testDataProvider({
getMany,
getOne: () =>
+ // @ts-ignore
Promise.resolve({
data: {
id: 1,
@@ -86,4 +93,62 @@ describe(' ', () => {
});
});
});
+
+ describe('with render prop', () => {
+ it('should display an error if error is defined', async () => {
+ jest.spyOn(console, 'error')
+ .mockImplementationOnce(() => {})
+ .mockImplementationOnce(() => {});
+
+ const dataProviderWithAuthorsError = {
+ getOne: () =>
+ Promise.resolve({
+ data: {
+ id: 1,
+ title: 'War and Peace',
+ author: 1,
+ summary:
+ "War and Peace broadly focuses on Napoleon's invasion of Russia, and the impact it had on Tsarist society. The book explores themes such as revolution, revolution and empire, the growth and decline of various states and the impact it had on their economies, culture, and society.",
+ year: 1869,
+ },
+ }),
+ getMany: _resource => Promise.reject(new Error('Error')),
+ } as any;
+
+ render(
+
+ );
+ await waitFor(() => {
+ expect(screen.queryByText('Error')).not.toBeNull();
+ });
+ });
+
+ it('should pass the loading state', async () => {
+ jest.spyOn(console, 'error')
+ .mockImplementationOnce(() => {})
+ .mockImplementationOnce(() => {});
+
+ const dataProviderWithAuthorsLoading = {
+ getOne: () =>
+ Promise.resolve({
+ data: {
+ id: 1,
+ title: 'War and Peace',
+ author: 1,
+ summary:
+ "War and Peace broadly focuses on Napoleon's invasion of Russia, and the impact it had on Tsarist society. The book explores themes such as revolution, revolution and empire, the growth and decline of various states and the impact it had on their economies, culture, and society.",
+ year: 1869,
+ },
+ }),
+ getMany: _resource => new Promise(() => {}),
+ } as any;
+
+ render(
+
+ );
+ await waitFor(() => {
+ expect(screen.queryByText('Loading...')).not.toBeNull();
+ });
+ });
+ });
});
diff --git a/packages/ra-core/src/controller/field/ReferenceFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceFieldBase.stories.tsx
index 5ef4bdafd4d..168f35c0909 100644
--- a/packages/ra-core/src/controller/field/ReferenceFieldBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceFieldBase.stories.tsx
@@ -94,10 +94,10 @@ const dataProviderWithAuthorsError = {
year: 1869,
},
}),
- getMany: _resource => Promise.reject('Error'),
+ getMany: _resource => Promise.reject(new Error('Error')),
} as any;
-export const Error = ({ dataProvider = dataProviderWithAuthorsError }) => (
+export const Errored = ({ dataProvider = dataProviderWithAuthorsError }) => (
);
+export const WithRenderProp = ({ dataProvider = dataProviderWithAuthors }) => (
+
+
+
+
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return (
+
+ {error.message}
+
+ );
+ }
+ return ;
+ }}
+ />
+
+ }
+ />
+
+
+);
+
const MyReferenceField = (props: { children: React.ReactNode }) => {
const context = useReferenceFieldContext();
diff --git a/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx
index 7b4a6a065b8..29d3ed17970 100644
--- a/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx
@@ -3,7 +3,10 @@ import { ReactNode } from 'react';
import { UseQueryOptions } from '@tanstack/react-query';
import { ReferenceFieldContextProvider } from './ReferenceFieldContext';
import { RaRecord } from '../../types';
-import { useReferenceFieldController } from './useReferenceFieldController';
+import {
+ useReferenceFieldController,
+ UseReferenceFieldControllerResult,
+} from './useReferenceFieldController';
import { ResourceContextProvider } from '../../core';
import { RecordContextProvider } from '../record';
import { useFieldValue } from '../../util';
@@ -44,8 +47,9 @@ export const ReferenceFieldBase = <
>(
props: ReferenceFieldBaseProps
) => {
- const { children, empty = null } = props;
+ const { children, render, empty = null } = props;
const id = useFieldValue(props);
+
const controllerProps =
useReferenceFieldController(props);
@@ -64,7 +68,7 @@ export const ReferenceFieldBase = <
- {children}
+ {render ? render(controllerProps) : children}
@@ -75,6 +79,9 @@ export interface ReferenceFieldBaseProps<
ReferenceRecordType extends RaRecord = RaRecord,
> {
children?: ReactNode;
+ render?: (
+ props: UseReferenceFieldControllerResult
+ ) => ReactNode;
className?: string;
empty?: ReactNode;
error?: ReactNode;
From bcfa72a0538dee757e2d33e4aac30041fa1dfcfb Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 8 Jul 2025 13:56:25 +0200
Subject: [PATCH 08/41] add render prop on ReferenceManyFieldBase
---
.../field/ReferenceFieldBase.spec.tsx | 22 +-
.../field/ReferenceManyFieldBase.spec.tsx | 178 +++++++
.../field/ReferenceManyFieldBase.stories.tsx | 443 ++++++++++++++++++
.../field/ReferenceManyFieldBase.tsx | 23 +-
4 files changed, 658 insertions(+), 8 deletions(-)
create mode 100644 packages/ra-core/src/controller/field/ReferenceManyFieldBase.spec.tsx
create mode 100644 packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
diff --git a/packages/ra-core/src/controller/field/ReferenceFieldBase.spec.tsx b/packages/ra-core/src/controller/field/ReferenceFieldBase.spec.tsx
index 2f201cb15ca..9ee4ae816b5 100644
--- a/packages/ra-core/src/controller/field/ReferenceFieldBase.spec.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceFieldBase.spec.tsx
@@ -6,12 +6,12 @@ import { useResourceContext } from '../../core/useResourceContext';
import { testDataProvider } from '../../dataProvider';
import { ReferenceFieldBase } from './ReferenceFieldBase';
import {
+ Basic,
Errored,
Loading,
Meta,
WithRenderProp,
} from './ReferenceFieldBase.stories';
-import { WithRenderProps } from '../edit/EditBase.stories';
describe(' ', () => {
beforeAll(() => {
@@ -25,7 +25,7 @@ describe(' ', () => {
render( );
await waitFor(() => {
- expect(screen.queryByText('Error')).not.toBeNull();
+ expect(screen.queryByText('Error: Error')).not.toBeNull();
});
});
@@ -46,8 +46,8 @@ describe(' ', () => {
return {resource}
;
};
const dataProvider = testDataProvider({
+ // @ts-ignore
getList: () =>
- // @ts-ignore
Promise.resolve({ data: [{ id: 1 }, { id: 2 }], total: 2 }),
});
render(
@@ -70,8 +70,8 @@ describe(' ', () => {
);
const dataProvider = testDataProvider({
getMany,
+ // @ts-ignore
getOne: () =>
- // @ts-ignore
Promise.resolve({
data: {
id: 1,
@@ -94,6 +94,13 @@ describe(' ', () => {
});
});
+ it('should render the data', async () => {
+ render( );
+ await waitFor(() => {
+ expect(screen.queryByText('Leo')).not.toBeNull();
+ });
+ });
+
describe('with render prop', () => {
it('should display an error if error is defined', async () => {
jest.spyOn(console, 'error')
@@ -150,5 +157,12 @@ describe(' ', () => {
expect(screen.queryByText('Loading...')).not.toBeNull();
});
});
+
+ it('should render the data', async () => {
+ render( );
+ await waitFor(() => {
+ expect(screen.queryByText('Leo')).not.toBeNull();
+ });
+ });
});
});
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.spec.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.spec.tsx
new file mode 100644
index 00000000000..76a5c0935cd
--- /dev/null
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.spec.tsx
@@ -0,0 +1,178 @@
+import * as React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import {
+ Basic,
+ Errored,
+ Loading,
+ WithPagination,
+ WithRenderProp,
+} from './ReferenceManyFieldBase.stories';
+
+import { ReferenceManyFieldBase } from './ReferenceManyFieldBase';
+import { useResourceContext } from '../../core/useResourceContext';
+import { testDataProvider } from '../../dataProvider/testDataProvider';
+import { CoreAdminContext } from '../../core/CoreAdminContext';
+
+describe('ReferenceManyFieldBase', () => {
+ it('should display an error if error is defined', async () => {
+ jest.spyOn(console, 'error')
+ .mockImplementationOnce(() => {})
+ .mockImplementationOnce(() => {});
+
+ render( );
+ await waitFor(() => {
+ expect(screen.queryByText('Error: Error')).not.toBeNull();
+ });
+ });
+
+ it('should pass the loading state', async () => {
+ jest.spyOn(console, 'error')
+ .mockImplementationOnce(() => {})
+ .mockImplementationOnce(() => {});
+
+ render( );
+ await waitFor(() => {
+ expect(screen.queryByText('Loading...')).not.toBeNull();
+ });
+ });
+ it('should pass the correct resource down to child component', async () => {
+ const MyComponent = () => {
+ const resource = useResourceContext();
+ return {resource}
;
+ };
+ const dataProvider = testDataProvider({
+ getList: () =>
+ // @ts-ignore
+ Promise.resolve({ data: [{ id: 1 }, { id: 2 }], total: 2 }),
+ });
+ render(
+
+
+
+
+
+ );
+ await waitFor(() => {
+ expect(screen.queryByText('posts')).not.toBeNull();
+ });
+ });
+
+ it('should render the data', async () => {
+ render( );
+ await waitFor(() => {
+ expect(screen.queryByText('War and Peace')).not.toBeNull();
+ expect(screen.queryByText('Anna Karenina')).not.toBeNull();
+ expect(screen.queryByText('The Kreutzer Sonata')).not.toBeNull();
+ });
+ });
+
+ it('should render pagination', async () => {
+ render( );
+ await waitFor(() => {
+ expect(screen.queryByText('1 - 2 of 3')).not.toBeNull();
+ expect(screen.queryByText('Next Page')).not.toBeNull();
+ expect(screen.queryByText('Previous Page')).not.toBeNull();
+ });
+ screen.getByText('Next Page').click();
+ await waitFor(() => {
+ expect(screen.queryByText('3 - 3 of 3')).not.toBeNull();
+ });
+ screen.getByText('Previous Page').click();
+
+ await waitFor(() => {
+ expect(screen.queryByText('1 - 2 of 3')).not.toBeNull();
+ });
+ });
+
+ describe('with render prop', () => {
+ it('should display an error if error is defined', async () => {
+ jest.spyOn(console, 'error')
+ .mockImplementationOnce(() => {})
+ .mockImplementationOnce(() => {});
+
+ const dataProviderWithAuthorsError = {
+ getOne: () =>
+ Promise.resolve({
+ data: {
+ id: 1,
+ title: 'War and Peace',
+ author: 1,
+ summary:
+ "War and Peace broadly focuses on Napoleon's invasion of Russia, and the impact it had on Tsarist society. The book explores themes such as revolution, revolution and empire, the growth and decline of various states and the impact it had on their economies, culture, and society.",
+ year: 1869,
+ },
+ }),
+ getMany: _resource => Promise.reject(new Error('Error')),
+ getManyReference: () => Promise.reject(new Error('Error')),
+ } as any;
+
+ render(
+
+ );
+ await waitFor(() => {
+ expect(screen.queryByText('Error')).not.toBeNull();
+ });
+ });
+
+ it('should pass the loading state', async () => {
+ jest.spyOn(console, 'error')
+ .mockImplementationOnce(() => {})
+ .mockImplementationOnce(() => {});
+
+ const dataProviderWithAuthorsLoading = {
+ getOne: () =>
+ Promise.resolve({
+ data: {
+ id: 1,
+ title: 'War and Peace',
+ author: 1,
+ summary:
+ "War and Peace broadly focuses on Napoleon's invasion of Russia, and the impact it had on Tsarist society. The book explores themes such as revolution, revolution and empire, the growth and decline of various states and the impact it had on their economies, culture, and society.",
+ year: 1869,
+ },
+ }),
+ getMany: _resource => new Promise(() => {}),
+ } as any;
+
+ render(
+
+ );
+ await waitFor(() => {
+ expect(screen.queryByText('Loading...')).not.toBeNull();
+ });
+ });
+
+ it('should render the data', async () => {
+ render( );
+ await waitFor(() => {
+ expect(screen.queryByText('War and Peace')).not.toBeNull();
+ expect(screen.queryByText('Anna Karenina')).not.toBeNull();
+ expect(
+ screen.queryByText('The Kreutzer Sonata')
+ ).not.toBeNull();
+ });
+ });
+ });
+
+ it('should render pagination using renderPagination prop', async () => {
+ render( );
+ await waitFor(() => {
+ expect(screen.queryByText('1 - 2 of 3')).not.toBeNull();
+ expect(screen.queryByText('Next Page')).not.toBeNull();
+ expect(screen.queryByText('Previous Page')).not.toBeNull();
+ });
+ screen.getByText('Next Page').click();
+ await waitFor(() => {
+ expect(screen.queryByText('3 - 3 of 3')).not.toBeNull();
+ });
+ screen.getByText('Previous Page').click();
+
+ await waitFor(() => {
+ expect(screen.queryByText('1 - 2 of 3')).not.toBeNull();
+ });
+ });
+});
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
new file mode 100644
index 00000000000..f30155421f0
--- /dev/null
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
@@ -0,0 +1,443 @@
+import * as React from 'react';
+import { QueryClient } from '@tanstack/react-query';
+import { CoreAdmin } from '../../core/CoreAdmin';
+import { Resource } from '../../core/Resource';
+import { ShowBase } from '../../controller/show/ShowBase';
+import { TestMemoryRouter } from '../../routing';
+import { ReferenceManyFieldBase } from './ReferenceManyFieldBase';
+import { useReferenceManyFieldController } from './useReferenceManyFieldController';
+import { useListContextWithProps } from '../list';
+
+export default {
+ title: 'ra-core/controller/field/ReferenceManyFieldBase',
+ excludeStories: ['dataProviderWithAuthors'],
+};
+
+const author = {
+ id: 1,
+ first_name: 'Leo',
+ last_name: 'Tolstoy',
+ language: 'Russian',
+};
+
+const books = [
+ {
+ id: 1,
+ title: 'War and Peace',
+ author: 1,
+ },
+ {
+ id: 2,
+ title: 'Anna Karenina',
+ author: 1,
+ },
+ {
+ id: 3,
+ title: 'The Kreutzer Sonata',
+ author: 1,
+ },
+ {
+ id: 4,
+ author: 2,
+ title: 'Hamlet',
+ },
+];
+
+export const dataProviderWithAuthors = {
+ getOne: () =>
+ Promise.resolve({
+ data: author,
+ }),
+ getMany: (_resource, params) =>
+ Promise.resolve({
+ data: books.filter(book => params.ids.includes(book.author)),
+ }),
+ getManyReference: (_resource, params) => {
+ const result = books.filter(book => book.author === params.id);
+
+ return Promise.resolve({
+ data: result.slice(
+ (params.pagination.page - 1) * params.pagination.perPage,
+ (params.pagination.page - 1) * params.pagination.perPage +
+ params.pagination.perPage
+ ),
+ total: result.length,
+ });
+ },
+} as any;
+
+export const Basic = ({ dataProvider = dataProviderWithAuthors }) => (
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+);
+
+const dataProviderWithAuthorsError = {
+ getOne: () =>
+ Promise.resolve({
+ data: {
+ id: 1,
+ title: 'War and Peace',
+ author: 1,
+ summary:
+ "War and Peace broadly focuses on Napoleon's invasion of Russia, and the impact it had on Tsarist society. The book explores themes such as revolution, revolution and empire, the growth and decline of various states and the impact it had on their economies, culture, and society.",
+ year: 1869,
+ },
+ }),
+ getMany: (_resource, params) =>
+ Promise.resolve({
+ data: books.filter(book => params.ids.includes(book.author)),
+ }),
+ getManyReference: _resource => Promise.reject(new Error('Error')),
+} as any;
+
+export const Errored = ({ dataProvider = dataProviderWithAuthorsError }) => (
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+);
+
+const dataProviderWithAuthorsLoading = {
+ getOne: () =>
+ Promise.resolve({
+ data: author,
+ }),
+
+ getMany: (_resource, params) =>
+ Promise.resolve({
+ data: books.filter(book => params.ids.includes(book.author)),
+ }),
+ getManyReference: _resource => new Promise(() => {}),
+} as any;
+
+export const Loading = ({ dataProvider = dataProviderWithAuthorsLoading }) => (
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+);
+
+export const WithPagination = ({ dataProvider = dataProviderWithAuthors }) => (
+
+
+
+
+ }
+ >
+
+
+
+
+
+ }
+ />
+
+
+);
+
+export const WithRenderProp = ({ dataProvider = dataProviderWithAuthors }) => (
+
+
+
+
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return (
+
+ {error.message}
+
+ );
+ }
+ return (
+
+ {data?.map((datum, index) => (
+
{datum.title}
+ ))}
+
+ );
+ }}
+ />
+
+ }
+ />
+
+
+);
+
+export const WithRenderPagination = ({
+ dataProvider = dataProviderWithAuthors,
+}) => (
+
+
+
+
+ {
+ const nextPage = () => {
+ setPage(page + 1);
+ };
+ const previousPage = () => {
+ setPage(page - 1);
+ };
+ return (
+
+
+ previous page
+
+ {(page - 1) * perPage + 1} -{' '}
+ {Math.min(page * perPage, total)} of{' '}
+ {total}
+ = total / perPage}
+ onClick={nextPage}
+ >
+ next page
+
+
+ );
+ }}
+ >
+
+
+
+
+
+ }
+ />
+
+
+);
+
+const MyReferenceManyField = ({
+ reference,
+ target,
+ source,
+ children,
+}: {
+ children: React.ReactNode;
+ reference: string;
+ target: string;
+ source: string;
+}) => {
+ const context = useReferenceManyFieldController({
+ reference,
+ target,
+ source,
+ });
+
+ if (context.isPending) {
+ return Loading...
;
+ }
+
+ if (context.error) {
+ return {context.error.toString()}
;
+ }
+ return children;
+};
+
+const List = ({ source }: { source: string }) => {
+ const listContext = useListContextWithProps();
+ return (
+
+ {listContext.data?.map((datum, index) => (
+
{datum[source]}
+ ))}
+
+ );
+};
+
+const Pagination = () => {
+ const { page, setPage, total, perPage } = useListContextWithProps();
+ const nextPage = () => {
+ setPage(page + 1);
+ };
+ const previousPage = () => {
+ setPage(page - 1);
+ };
+ return (
+
+
+ Previous Page
+
+
+ {`${(page - 1) * perPage + 1} - ${Math.min(page * perPage, total)} of ${total}`}
+
+ = total / perPage} onClick={nextPage}>
+ Next Page
+
+
+ );
+};
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
index d969805f7f3..87a6eadbeff 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
@@ -6,6 +6,7 @@ import {
type UseReferenceManyFieldControllerParams,
} from './useReferenceManyFieldController';
import type { RaRecord } from '../../types';
+import { ListControllerResult } from '../list';
/**
* Render related records to the current one.
@@ -61,11 +62,13 @@ export const ReferenceManyFieldBase = <
) => {
const {
children,
+ render,
debounce,
empty,
filter = defaultFilter,
page = 1,
pagination = null,
+ renderPagination = null,
perPage = 25,
record,
reference,
@@ -117,11 +120,19 @@ export const ReferenceManyFieldBase = <
return empty;
}
+ if (!render && !children) {
+ throw new Error(
+ " requires either a 'render' prop or 'children' prop"
+ );
+ }
+
return (
- {children}
- {pagination}
+ {render ? render(controllerProps) : children}
+ {renderPagination
+ ? renderPagination(controllerProps)
+ : pagination}
);
@@ -129,12 +140,16 @@ export const ReferenceManyFieldBase = <
export interface ReferenceManyFieldBaseProps<
RecordType extends Record = Record,
- ReferenceRecordType extends Record = Record,
+ ReferenceRecordType extends RaRecord = RaRecord,
> extends UseReferenceManyFieldControllerParams<
RecordType,
ReferenceRecordType
> {
- children: ReactNode;
+ children?: ReactNode;
+ render?: (props: ListControllerResult) => ReactNode;
+ renderPagination?: (
+ props: ListControllerResult
+ ) => ReactNode;
empty?: ReactNode;
pagination?: ReactNode;
}
From 04b308554f20f0976f5ad098f0e838ac05891244 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 8 Jul 2025 15:39:34 +0200
Subject: [PATCH 09/41] add render prop on ReferenceOneFieldBase
---
docs/ListBase.md | 2 +-
.../field/ReferenceManyFieldBase.spec.tsx | 2 +-
.../field/ReferenceManyFieldBase.stories.tsx | 19 +--
.../field/ReferenceOneFieldBase.spec.tsx | 64 ++++++++++
.../field/ReferenceOneFieldBase.stories.tsx | 119 ++++++++++++++++++
.../field/ReferenceOneFieldBase.tsx | 5 +-
.../src/field/ReferenceManyField.tsx | 2 +-
7 files changed, 201 insertions(+), 12 deletions(-)
create mode 100644 packages/ra-core/src/controller/field/ReferenceOneFieldBase.spec.tsx
create mode 100644 packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx
diff --git a/docs/ListBase.md b/docs/ListBase.md
index ed6f92e3af7..96d7cd3319e 100644
--- a/docs/ListBase.md
+++ b/docs/ListBase.md
@@ -69,7 +69,7 @@ The `` component accepts the same props as [`useListController`](./use
These are a subset of the props accepted by `` - only the props that change data fetching, and not the props related to the user interface.
In addition, `` renders its children components inside a `ListContext`. Check [the `` documentation](./List.md#children) for usage examples.
-Alternatively you can pass a render function to the props, in place of children. This function will receive the listContext as argument. Check [the `` documentation](./List.md#render) for usage examples.
+Alternatively you can pass a render function to the props, in place of children. This function will receive the listContext as argument.
## Security
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.spec.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.spec.tsx
index 76a5c0935cd..09cfc6aa146 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.spec.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.spec.tsx
@@ -41,8 +41,8 @@ describe('ReferenceManyFieldBase', () => {
return {resource}
;
};
const dataProvider = testDataProvider({
+ // @ts-ignore
getList: () =>
- // @ts-ignore
Promise.resolve({ data: [{ id: 1 }, { id: 2 }], total: 2 }),
});
render(
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
index f30155421f0..c37a3fec466 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
@@ -334,9 +334,9 @@ export const WithRenderPagination = ({
reference="books"
perPage={2}
renderPagination={({
- page,
+ page = 0,
setPage,
- total,
+ total = 0,
perPage,
}) => {
const nextPage = () => {
@@ -353,9 +353,7 @@ export const WithRenderPagination = ({
>
previous page
- {(page - 1) * perPage + 1} -{' '}
- {Math.min(page * perPage, total)} of{' '}
- {total}
+ {`${(page - 1) * perPage + 1} - ${Math.min(page * perPage, total)} of ${total}`}
= total / perPage}
onClick={nextPage}
@@ -420,12 +418,17 @@ const List = ({ source }: { source: string }) => {
};
const Pagination = () => {
- const { page, setPage, total, perPage } = useListContextWithProps();
+ const {
+ page = 1,
+ setPage,
+ total = 0,
+ perPage = 0,
+ } = useListContextWithProps();
const nextPage = () => {
- setPage(page + 1);
+ setPage?.(page + 1);
};
const previousPage = () => {
- setPage(page - 1);
+ setPage?.(page - 1);
};
return (
diff --git a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.spec.tsx b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.spec.tsx
new file mode 100644
index 00000000000..3a003c2439a
--- /dev/null
+++ b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.spec.tsx
@@ -0,0 +1,64 @@
+import * as React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import {
+ Basic,
+ Loading,
+ WithRenderProp,
+} from './ReferenceOneFieldBase.stories';
+
+describe('ReferenceOneFieldBase', () => {
+ it('should pass the loading state', async () => {
+ jest.spyOn(console, 'error')
+ .mockImplementationOnce(() => {})
+ .mockImplementationOnce(() => {});
+
+ render(
);
+ await waitFor(() => {
+ expect(screen.queryByText('Loading...')).not.toBeNull();
+ });
+ });
+
+ it('should render the data', async () => {
+ render(
);
+ await waitFor(() => {
+ expect(screen.queryByText('9780393966473')).not.toBeNull();
+ });
+ });
+
+ describe('with render prop', () => {
+ it('should pass the loading state', async () => {
+ jest.spyOn(console, 'error')
+ .mockImplementationOnce(() => {})
+ .mockImplementationOnce(() => {});
+
+ const dataProviderWithAuthorsLoading = {
+ getOne: () =>
+ Promise.resolve({
+ data: {
+ id: 1,
+ title: 'War and Peace',
+ author: 1,
+ summary:
+ "War and Peace broadly focuses on Napoleon's invasion of Russia, and the impact it had on Tsarist society. The book explores themes such as revolution, revolution and empire, the growth and decline of various states and the impact it had on their economies, culture, and society.",
+ year: 1869,
+ },
+ }),
+ getMany: _resource => new Promise(() => {}),
+ } as any;
+
+ render(
+
+ );
+ await waitFor(() => {
+ expect(screen.queryByText('Loading...')).not.toBeNull();
+ });
+ });
+
+ it('should render the data', async () => {
+ render(
);
+ await waitFor(() => {
+ expect(screen.queryByText('9780393966473')).not.toBeNull();
+ });
+ });
+ });
+});
diff --git a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx
new file mode 100644
index 00000000000..6831b37d555
--- /dev/null
+++ b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx
@@ -0,0 +1,119 @@
+import * as React from 'react';
+
+import {
+ CoreAdminContext,
+ RecordContextProvider,
+ ReferenceOneFieldBase,
+ ResourceContextProvider,
+ TestMemoryRouter,
+ useRecordContext,
+ useReferenceOneFieldController,
+} from '../..';
+
+export default { title: 'ra-ui-materialui/fields/ReferenceOneFieldBase' };
+
+const defaultDataProvider = {
+ getManyReference: () =>
+ Promise.resolve({
+ data: [{ id: 1, ISBN: '9780393966473', genre: 'novel' }],
+ total: 1,
+ }),
+} as any;
+
+const Wrapper = ({ children, dataProvider = defaultDataProvider }) => (
+
+
+
+
+ {children}
+
+
+
+
+);
+
+export const Basic = () => (
+
+
+
+
+
+
+
+);
+
+const dataProviderWithLoading = {
+ getManyReference: () => new Promise(() => {}),
+} as any;
+
+export const Loading = () => (
+
+
+
+
+
+
+
+);
+
+export const WithRenderProp = ({
+ dataProvider = defaultDataProvider,
+}: {
+ dataProvider?: any;
+}) => {
+ return (
+
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return (
+ {error.toString()}
+ );
+ }
+ return (
+
+ {referenceRecord ? referenceRecord.ISBN : ''}
+
+ );
+ }}
+ />
+
+ );
+};
+
+const MyReferenceOneField = ({
+ reference,
+ target,
+ children,
+}: {
+ children: React.ReactNode;
+ reference: string;
+ target: string;
+}) => {
+ const context = useReferenceOneFieldController({
+ reference,
+ target,
+ });
+
+ if (context.isPending) {
+ return
Loading...
;
+ }
+
+ if (context.error) {
+ return
{context.error.toString()}
;
+ }
+ return children;
+};
+
+const TextField = ({ source }) => {
+ const record = useRecordContext();
+ return
{record ? record[source] : ''} ;
+};
diff --git a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx
index c54f73aa5f1..03e9b640346 100644
--- a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx
@@ -10,6 +10,7 @@ import { useGetPathForRecord } from '../../routing';
import type { UseReferenceFieldControllerResult } from './useReferenceFieldController';
import type { RaRecord } from '../../types';
import type { LinkToType } from '../../routing';
+import { UseReferenceResult } from '../useReference';
/**
* Render the related record in a one-to-one relationship
@@ -29,6 +30,7 @@ export const ReferenceOneFieldBase = <
) => {
const {
children,
+ render,
record,
reference,
source = 'id',
@@ -79,7 +81,7 @@ export const ReferenceOneFieldBase = <
- {children}
+ {render ? render(controllerProps) : children}
@@ -94,6 +96,7 @@ export interface ReferenceOneFieldBaseProps<
ReferenceRecordType
> {
children?: ReactNode;
+ render?: (props: UseReferenceResult
) => ReactNode;
link?: LinkToType;
empty?: ReactNode;
resource?: string;
diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
index c044075c2ac..43904fa5f6d 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
@@ -80,6 +80,6 @@ export const ReferenceManyField = <
export interface ReferenceManyFieldProps<
RecordType extends Record = Record,
- ReferenceRecordType extends Record = Record,
+ ReferenceRecordType extends RaRecord = RaRecord,
> extends Omit, 'source'>,
ReferenceManyFieldBaseProps {}
From 4e0ebeab17b05c37bcc2a5cf84c40f5e90bd8699 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 8 Jul 2025 16:19:32 +0200
Subject: [PATCH 10/41] create ReferenceArrayFieldBase and use it in MUI
ReferenceArrayField
---
.../field/ReferenceArrayFieldBase.tsx | 124 ++++++++++++++++++
.../ra-core/src/controller/field/index.ts | 2 +
.../ra-core/src/controller/field/types.ts | 121 +++++++++++++++++
.../src/field/ReferenceArrayField.tsx | 38 +-----
packages/ra-ui-materialui/src/field/types.ts | 119 +----------------
5 files changed, 253 insertions(+), 151 deletions(-)
create mode 100644 packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
create mode 100644 packages/ra-core/src/controller/field/types.ts
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
new file mode 100644
index 00000000000..dc64f0738b0
--- /dev/null
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
@@ -0,0 +1,124 @@
+import * as React from 'react';
+import { type ReactElement, type ReactNode } from 'react';
+
+import type { UseQueryOptions } from '@tanstack/react-query';
+import { FilterPayload, RaRecord, SortPayload } from '../../types';
+import { useRecordContext } from '../record';
+import { useReferenceArrayFieldController } from './useReferenceArrayFieldController';
+import { ResourceContextProvider } from '../../core';
+import { ListContextProvider } from '../list';
+import { FieldProps } from './types';
+
+/**
+ * A container component that fetches records from another resource specified
+ * by an array of *ids* in current record.
+ *
+ * You must define the fields to be passed to the iterator component as children.
+ *
+ * @example Display all the products of the current order as datagrid
+ * // order = {
+ * // id: 123,
+ * // product_ids: [456, 457, 458],
+ * // }
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * @example Display all the categories of the current product as a list of chips
+ * // product = {
+ * // id: 456,
+ * // category_ids: [11, 22, 33],
+ * // }
+ *
+ *
+ *
+ *
+ *
+ *
+ * By default, restricts the displayed values to 1000. You can extend this limit
+ * by setting the `perPage` prop.
+ *
+ * @example
+ *
+ * ...
+ *
+ *
+ * By default, the field displays the results in the order in which they are referenced
+ * (i.e. in the order of the list of ids). You can change this order
+ * by setting the `sort` prop (an object with `field` and `order` properties).
+ *
+ * @example
+ *
+ * ...
+ *
+ *
+ * Also, you can filter the results to display only a subset of values. Use the
+ * `filter` prop for that.
+ *
+ * @example
+ *
+ * ...
+ *
+ */
+export const ReferenceArrayFieldBase = <
+ RecordType extends RaRecord = RaRecord,
+ ReferenceRecordType extends RaRecord = RaRecord,
+>(
+ props: ReferenceArrayFieldBaseProps
+) => {
+ const {
+ children,
+ filter,
+ page = 1,
+ perPage,
+ reference,
+ resource,
+ sort,
+ source,
+ queryOptions,
+ } = props;
+ const record = useRecordContext(props);
+ const controllerProps = useReferenceArrayFieldController<
+ RecordType,
+ ReferenceRecordType
+ >({
+ filter,
+ page,
+ perPage,
+ record,
+ reference,
+ resource,
+ sort,
+ source,
+ queryOptions,
+ });
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export interface ReferenceArrayFieldBaseProps<
+ RecordType extends RaRecord = RaRecord,
+ ReferenceRecordType extends RaRecord = RaRecord,
+> extends FieldProps {
+ children?: ReactNode;
+ filter?: FilterPayload;
+ page?: number;
+ pagination?: ReactElement;
+ perPage?: number;
+ reference: string;
+ sort?: SortPayload;
+ queryOptions?: Omit<
+ UseQueryOptions,
+ 'queryFn' | 'queryKey'
+ >;
+}
diff --git a/packages/ra-core/src/controller/field/index.ts b/packages/ra-core/src/controller/field/index.ts
index 873cce05bdb..c11005b7912 100644
--- a/packages/ra-core/src/controller/field/index.ts
+++ b/packages/ra-core/src/controller/field/index.ts
@@ -3,6 +3,8 @@ export * from './ReferenceFieldBase';
export * from './ReferenceFieldContext';
export * from './ReferenceManyCountBase';
export * from './ReferenceManyFieldBase';
+export * from './ReferenceArrayFieldBase';
+export * from './types';
export * from './useReferenceArrayFieldController';
export * from './useReferenceFieldController';
export * from './useReferenceManyFieldController';
diff --git a/packages/ra-core/src/controller/field/types.ts b/packages/ra-core/src/controller/field/types.ts
new file mode 100644
index 00000000000..e8509636a50
--- /dev/null
+++ b/packages/ra-core/src/controller/field/types.ts
@@ -0,0 +1,121 @@
+import { ReactElement } from 'react';
+import { ExtractRecordPaths, HintedString } from '../../types';
+
+type SortOrder = 'ASC' | 'DESC';
+
+export interface FieldProps<
+ RecordType extends Record = Record,
+> {
+ /**
+ * The field to use for sorting when users click this column head, if sortable.
+ *
+ * @see https://marmelab.com/react-admin/Fields.html#sortby
+ * @example
+ * const PostList = () => (
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * );
+ */
+ sortBy?: HintedString>;
+
+ /**
+ * The order used for sorting when users click this column head, if sortable.
+ *
+ * @see https://marmelab.com/react-admin/Fields.html#sortbyorder
+ * @example
+ * const PostList = () => (
+ *
+ *
+ *
+ *
+ *
+ *
+ * );
+ */
+ sortByOrder?: SortOrder;
+
+ /**
+ * Name of the property to display.
+ *
+ * @see https://marmelab.com/react-admin/Fields.html#source
+ * @example
+ * const CommentList = () => (
+ *
+ *
+ *
+ *
+ *
+ *
+ * );
+ */
+ source: ExtractRecordPaths;
+
+ /**
+ * Label to use as column header when using or .
+ * Defaults to the capitalized field name. Set to false to disable the label.
+ *
+ * @see https://marmelab.com/react-admin/Fields.html#label
+ * @example
+ * const PostList = () => (
+ *
+ *
+ *
+ *
+ *
+ *
+ * );
+ */
+ label?: string | ReactElement | boolean;
+
+ /**
+ * Set it to false to disable the click handler on the column header when used inside .
+ *
+ * @see https://marmelab.com/react-admin/Fields.html#sortable
+ * @example
+ * const PostList = () => (
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * );
+ */
+ sortable?: boolean;
+
+ /**
+ * The text to display when the field value is empty. Defaults to empty string.
+ *
+ * @see https://marmelab.com/react-admin/Fields.html#emptytext
+ * @example
+ * const PostList = () => (
+ *
+ *
+ *
+ *
+ *
+ *
+ * );
+ */
+ emptyText?: string;
+
+ /**
+ * The current record to use. Defaults to the `RecordContext` value.
+ *
+ * @see https://marmelab.com/react-admin/Fields.html#record
+ */
+ record?: RecordType;
+
+ /**
+ * The resource name. Defaults to the `ResourceContext` value.
+ */
+ resource?: string;
+}
diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
index 01e184df22f..5f4cc8fdc76 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
@@ -1,14 +1,11 @@
import * as React from 'react';
import { memo, type ReactElement, type ReactNode } from 'react';
import {
- ListContextProvider,
useListContext,
type ListControllerProps,
- useReferenceArrayFieldController,
type SortPayload,
type FilterPayload,
- ResourceContextProvider,
- useRecordContext,
+ ReferenceArrayFieldBase,
type RaRecord,
} from 'ra-core';
import {
@@ -90,37 +87,10 @@ export const ReferenceArrayField = <
props: inProps,
name: PREFIX,
});
- const {
- filter,
- page = 1,
- perPage,
- reference,
- resource,
- sort,
- source,
- queryOptions,
- } = props;
- const record = useRecordContext(props);
- const controllerProps = useReferenceArrayFieldController<
- RecordType,
- ReferenceRecordType
- >({
- filter,
- page,
- perPage,
- record,
- reference,
- resource,
- sort,
- source,
- queryOptions,
- });
return (
-
-
-
-
-
+
+
+
);
};
export interface ReferenceArrayFieldProps<
diff --git a/packages/ra-ui-materialui/src/field/types.ts b/packages/ra-ui-materialui/src/field/types.ts
index 70db854a0b2..9d023b3ce25 100644
--- a/packages/ra-ui-materialui/src/field/types.ts
+++ b/packages/ra-ui-materialui/src/field/types.ts
@@ -1,98 +1,11 @@
-import { ReactElement } from 'react';
import { TableCellProps } from '@mui/material/TableCell';
-import { ExtractRecordPaths, HintedString } from 'ra-core';
+import { FieldProps as FieldPropsCore } from 'ra-core';
type TextAlign = TableCellProps['align'];
-type SortOrder = 'ASC' | 'DESC';
export interface FieldProps<
RecordType extends Record = Record,
-> {
- /**
- * The field to use for sorting when users click this column head, if sortable.
- *
- * @see https://marmelab.com/react-admin/Fields.html#sortby
- * @example
- * const PostList = () => (
- *
- *
- *
- *
- *
- *
- *
- *
- * );
- */
- sortBy?: HintedString>;
-
- /**
- * The order used for sorting when users click this column head, if sortable.
- *
- * @see https://marmelab.com/react-admin/Fields.html#sortbyorder
- * @example
- * const PostList = () => (
- *
- *
- *
- *
- *
- *
- * );
- */
- sortByOrder?: SortOrder;
-
- /**
- * Name of the property to display.
- *
- * @see https://marmelab.com/react-admin/Fields.html#source
- * @example
- * const CommentList = () => (
- *
- *
- *
- *
- *
- *
- * );
- */
- source: ExtractRecordPaths;
-
- /**
- * Label to use as column header when using or .
- * Defaults to the capitalized field name. Set to false to disable the label.
- *
- * @see https://marmelab.com/react-admin/Fields.html#label
- * @example
- * const PostList = () => (
- *
- *
- *
- *
- *
- *
- * );
- */
- label?: string | ReactElement | boolean;
-
- /**
- * Set it to false to disable the click handler on the column header when used inside .
- *
- * @see https://marmelab.com/react-admin/Fields.html#sortable
- * @example
- * const PostList = () => (
- *
- *
- *
- *
- *
- *
- *
- *
- * );
- */
- sortable?: boolean;
-
+> extends FieldPropsCore {
/**
* A class name to apply to the root div element
*/
@@ -127,36 +40,8 @@ export interface FieldProps<
*/
textAlign?: TextAlign;
- /**
- * The text to display when the field value is empty. Defaults to empty string.
- *
- * @see https://marmelab.com/react-admin/Fields.html#emptytext
- * @example
- * const PostList = () => (
- *
- *
- *
- *
- *
- *
- * );
- */
- emptyText?: string;
-
/**
* @deprecated
*/
fullWidth?: boolean;
-
- /**
- * The current record to use. Defaults to the `RecordContext` value.
- *
- * @see https://marmelab.com/react-admin/Fields.html#record
- */
- record?: RecordType;
-
- /**
- * The resource name. Defaults to the `ResourceContext` value.
- */
- resource?: string;
}
From 730f55653d0ef5b2f2e341a4924c8e2ea1ea3b74 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 8 Jul 2025 17:51:54 +0200
Subject: [PATCH 11/41] add render prop on ReferenceArrayFieldBase
---
.../field/ReferenceArrayFieldBase.spec.tsx | 123 ++++++++
.../field/ReferenceArrayFieldBase.stories.tsx | 288 ++++++++++++++++++
.../field/ReferenceArrayFieldBase.tsx | 14 +-
.../src/field/ReferenceArrayField.tsx | 29 +-
4 files changed, 432 insertions(+), 22 deletions(-)
create mode 100644 packages/ra-core/src/controller/field/ReferenceArrayFieldBase.spec.tsx
create mode 100644 packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.spec.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.spec.tsx
new file mode 100644
index 00000000000..9ad47426858
--- /dev/null
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.spec.tsx
@@ -0,0 +1,123 @@
+import * as React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import {
+ Basic,
+ Errored,
+ Loading,
+ WithPagination,
+ WithRenderProp,
+} from './ReferenceArrayFieldBase.stories';
+
+import { ReferenceArrayFieldBase } from './ReferenceArrayFieldBase';
+import { useResourceContext } from '../../core/useResourceContext';
+import { testDataProvider } from '../../dataProvider/testDataProvider';
+import { CoreAdminContext } from '../../core/CoreAdminContext';
+
+describe('ReferenceArrayFieldBase', () => {
+ it('should display an error if error is defined', async () => {
+ jest.spyOn(console, 'error')
+ .mockImplementationOnce(() => {})
+ .mockImplementationOnce(() => {});
+
+ render( );
+ await waitFor(() => {
+ expect(screen.queryByText('Error: Error')).not.toBeNull();
+ });
+ });
+
+ it('should pass the loading state', async () => {
+ jest.spyOn(console, 'error')
+ .mockImplementationOnce(() => {})
+ .mockImplementationOnce(() => {});
+
+ render( );
+ await waitFor(() => {
+ expect(screen.queryByText('Loading...')).not.toBeNull();
+ });
+ });
+ it('should pass the correct resource down to child component', async () => {
+ const MyComponent = () => {
+ const resource = useResourceContext();
+ return {resource}
;
+ };
+ const dataProvider = testDataProvider({
+ // @ts-ignore
+ getList: () =>
+ Promise.resolve({ data: [{ id: 1 }, { id: 2 }], total: 2 }),
+ });
+ render(
+
+
+
+
+
+ );
+ await waitFor(() => {
+ expect(screen.queryByText('posts')).not.toBeNull();
+ });
+ });
+
+ it('should render the data', async () => {
+ render( );
+ await waitFor(() => {
+ expect(screen.queryByText('John Lennon')).not.toBeNull();
+ expect(screen.queryByText('Paul McCartney')).not.toBeNull();
+ expect(screen.queryByText('Ringo Star')).not.toBeNull();
+ expect(screen.queryByText('George Harrison')).not.toBeNull();
+ expect(screen.queryByText('Mick Jagger')).not.toBeNull();
+ expect(screen.queryByText('Keith Richards')).not.toBeNull();
+ expect(screen.queryByText('Ronnie Wood')).not.toBeNull();
+ expect(screen.queryByText('Charlie Watts')).not.toBeNull();
+ });
+ });
+
+ it('should render pagination', async () => {
+ render( );
+ await waitFor(() => {
+ expect(screen.queryByText('1 - 3 of 8')).not.toBeNull();
+ expect(screen.queryByText('Next Page')).not.toBeNull();
+ expect(screen.queryByText('Previous Page')).not.toBeNull();
+ });
+ screen.getByText('Next Page').click();
+ await waitFor(() => {
+ expect(screen.queryByText('4 - 6 of 8')).not.toBeNull();
+ });
+ screen.getByText('Previous Page').click();
+
+ await waitFor(() => {
+ expect(screen.queryByText('1 - 3 of 8')).not.toBeNull();
+ });
+ });
+
+ it('should support renderProp', async () => {
+ render( );
+ await waitFor(() => {
+ expect(screen.queryByText('John Lennon')).not.toBeNull();
+ expect(screen.queryByText('Paul McCartney')).not.toBeNull();
+ expect(screen.queryByText('Ringo Star')).not.toBeNull();
+ expect(screen.queryByText('George Harrison')).not.toBeNull();
+ expect(screen.queryByText('Mick Jagger')).not.toBeNull();
+ expect(screen.queryByText('Keith Richards')).not.toBeNull();
+ expect(screen.queryByText('Ronnie Wood')).not.toBeNull();
+ expect(screen.queryByText('Charlie Watts')).not.toBeNull();
+ });
+ });
+
+ it('should render pagination using renderPagination prop', async () => {
+ render( );
+ await waitFor(() => {
+ expect(screen.queryByText('1 - 3 of 8')).not.toBeNull();
+ expect(screen.queryByText('Next Page')).not.toBeNull();
+ expect(screen.queryByText('Previous Page')).not.toBeNull();
+ });
+ screen.getByText('Next Page').click();
+ await waitFor(() => {
+ expect(screen.queryByText('4 - 6 of 8')).not.toBeNull();
+ });
+ screen.getByText('Previous Page').click();
+
+ await waitFor(() => {
+ expect(screen.queryByText('1 - 3 of 8')).not.toBeNull();
+ });
+ });
+});
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx
new file mode 100644
index 00000000000..eabe88c7b9e
--- /dev/null
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx
@@ -0,0 +1,288 @@
+import * as React from 'react';
+import fakeRestProvider from 'ra-data-fakerest';
+
+import { ReferenceArrayFieldBase } from './ReferenceArrayFieldBase';
+import {
+ CoreAdmin,
+ DataProvider,
+ Resource,
+ ShowBase,
+ TestMemoryRouter,
+ useListContext,
+ useListContextWithProps,
+} from '../..';
+import { QueryClient } from '@tanstack/react-query';
+
+export default { title: 'ra-core/controller/field/ReferenceArrayFieldBase' };
+
+const fakeData = {
+ bands: [{ id: 1, name: 'The Beatles', members: [1, 2, 3, 4, 5, 6, 7, 8] }],
+ artists: [
+ { id: 1, name: 'John Lennon' },
+ { id: 2, name: 'Paul McCartney' },
+ { id: 3, name: 'Ringo Star' },
+ { id: 4, name: 'George Harrison' },
+ { id: 5, name: 'Mick Jagger' },
+ { id: 6, name: 'Keith Richards' },
+ { id: 7, name: 'Ronnie Wood' },
+ { id: 8, name: 'Charlie Watts' },
+ ],
+};
+const defaultDataProvider = fakeRestProvider(fakeData, false);
+
+export const Basic = ({
+ dataProvider = defaultDataProvider,
+}: {
+ dataProvider?: DataProvider;
+}) => (
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+);
+
+export const WithPagination = () => (
+
+
+
+
+ }
+ perPage={3}
+ >
+
+
+
+
+
+ }
+ />
+
+
+);
+
+const erroredDataProvider = {
+ ...defaultDataProvider,
+ getMany: _resource => Promise.reject(new Error('Error')),
+} as any;
+
+export const Errored = () => ;
+
+const foreverLoadingDataProvider = {
+ ...defaultDataProvider,
+ getMany: _resource => new Promise(() => {}),
+} as any;
+
+export const Loading = () => (
+
+);
+
+export const WithRenderProp = ({
+ dataProvider = defaultDataProvider,
+}: {
+ dataProvider?: DataProvider;
+}) => (
+
+
+
+
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return (
+
+ {error.toString()}
+
+ );
+ }
+
+ return (
+
+ {data?.map((datum, index) => (
+
{datum.name}
+ ))}
+
+ );
+ }}
+ />
+
+ }
+ />
+
+
+);
+
+export const WithRenderPaginationProp = ({
+ dataProvider = defaultDataProvider,
+}: {
+ dataProvider: DataProvider;
+}) => (
+
+
+
+
+ {
+ const nextPage = () => {
+ setPage?.(page + 1);
+ };
+ const previousPage = () => {
+ setPage?.(page - 1);
+ };
+ return (
+
+
+ Previous Page
+
+
+ {`${(page - 1) * perPage + 1} - ${Math.min(page * perPage, total)} of ${total}`}
+
+ = total / perPage}
+ onClick={nextPage}
+ >
+ Next Page
+
+
+ );
+ }}
+ >
+
+
+
+
+
+ }
+ />
+
+
+);
+
+const MyReferenceArrayField = (props: { children: React.ReactNode }) => {
+ const context = useListContext();
+
+ if (context.isPending) {
+ return Loading...
;
+ }
+
+ if (context.error) {
+ return {context.error.toString()}
;
+ }
+ return props.children;
+};
+
+const List = ({ source }: { source: string }) => {
+ const listContext = useListContextWithProps();
+ return (
+
+ {listContext.data?.map((datum, index) => (
+
{datum[source]}
+ ))}
+
+ );
+};
+
+const Pagination = () => {
+ const {
+ page = 1,
+ setPage,
+ total = 0,
+ perPage = 0,
+ } = useListContextWithProps();
+ const nextPage = () => {
+ setPage?.(page + 1);
+ };
+ const previousPage = () => {
+ setPage?.(page - 1);
+ };
+ return (
+
+
+ Previous Page
+
+
+ {`${(page - 1) * perPage + 1} - ${Math.min(page * perPage, total)} of ${total}`}
+
+ = total / perPage} onClick={nextPage}>
+ Next Page
+
+
+ );
+};
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
index dc64f0738b0..6306eb88230 100644
--- a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
@@ -6,7 +6,7 @@ import { FilterPayload, RaRecord, SortPayload } from '../../types';
import { useRecordContext } from '../record';
import { useReferenceArrayFieldController } from './useReferenceArrayFieldController';
import { ResourceContextProvider } from '../../core';
-import { ListContextProvider } from '../list';
+import { ListContextProvider, ListControllerResult } from '../list';
import { FieldProps } from './types';
/**
@@ -73,6 +73,9 @@ export const ReferenceArrayFieldBase = <
) => {
const {
children,
+ render,
+ pagination,
+ renderPagination,
filter,
page = 1,
perPage,
@@ -100,7 +103,10 @@ export const ReferenceArrayFieldBase = <
return (
- {children}
+ {render ? render(controllerProps) : children}
+ {renderPagination
+ ? renderPagination(controllerProps)
+ : pagination}
);
@@ -111,6 +117,10 @@ export interface ReferenceArrayFieldBaseProps<
ReferenceRecordType extends RaRecord = RaRecord,
> extends FieldProps {
children?: ReactNode;
+ render?: (props: ListControllerResult) => ReactElement;
+ renderPagination?: (
+ props: ListControllerResult
+ ) => ReactElement;
filter?: FilterPayload;
page?: number;
pagination?: ReactElement;
diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
index 5f4cc8fdc76..8ee8f2358db 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
@@ -1,12 +1,11 @@
import * as React from 'react';
-import { memo, type ReactElement, type ReactNode } from 'react';
+import { memo } from 'react';
import {
useListContext,
type ListControllerProps,
- type SortPayload,
- type FilterPayload,
ReferenceArrayFieldBase,
type RaRecord,
+ ReferenceArrayFieldBaseProps,
} from 'ra-core';
import {
type ComponentsOverrides,
@@ -15,7 +14,6 @@ import {
type Theme,
useThemeProps,
} from '@mui/material/styles';
-import type { UseQueryOptions } from '@tanstack/react-query';
import type { FieldProps } from './types';
import { LinearProgress } from '../layout';
@@ -80,35 +78,26 @@ import { SingleFieldList } from '../list/SingleFieldList';
export const ReferenceArrayField = <
RecordType extends RaRecord = RaRecord,
ReferenceRecordType extends RaRecord = RaRecord,
->(
- inProps: ReferenceArrayFieldProps
-) => {
+>({
+ pagination,
+ ...inProps
+}: ReferenceArrayFieldProps) => {
const props = useThemeProps({
props: inProps,
name: PREFIX,
});
return (
-
+
);
};
export interface ReferenceArrayFieldProps<
RecordType extends RaRecord = RaRecord,
ReferenceRecordType extends RaRecord = RaRecord,
-> extends FieldProps {
- children?: ReactNode;
- filter?: FilterPayload;
- page?: number;
- pagination?: ReactElement;
- perPage?: number;
- reference: string;
- sort?: SortPayload;
+> extends ReferenceArrayFieldBaseProps,
+ FieldProps {
sx?: SxProps;
- queryOptions?: Omit<
- UseQueryOptions,
- 'queryFn' | 'queryKey'
- >;
}
export interface ReferenceArrayFieldViewProps
From 3ae7f986498c13ac1c141f50afd2d098779aa4a1 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 8 Jul 2025 18:18:32 +0200
Subject: [PATCH 12/41] code review applied
---
docs/ShowBase.md | 2 +-
.../ra-core/src/controller/create/CreateBase.stories.tsx | 1 -
packages/ra-core/src/controller/list/ListBase.stories.tsx | 8 --------
packages/ra-ui-materialui/src/list/List.tsx | 2 +-
packages/ra-ui-materialui/src/list/ListView.tsx | 2 +-
5 files changed, 3 insertions(+), 12 deletions(-)
diff --git a/docs/ShowBase.md b/docs/ShowBase.md
index 514790dab9d..b4a84678bec 100644
--- a/docs/ShowBase.md
+++ b/docs/ShowBase.md
@@ -63,7 +63,7 @@ const App = () => (
| Prop | Required | Type | Default | Description
|------------------|----------|-------------------|---------|--------------------------------------------------------
| `children` | Optional | `ReactNode` | | The components rendering the record fields
-| `children` | Optional | `(props: ShowControllerResult) => ReactNode` | | Alternative to children, a function that takes the ShowController context and renders the form
+| `render` | Optional | `(props: ShowControllerResult) => ReactNode` | | Alternative to children, a function that takes the ShowController context and renders the form
| `disable Authentication` | Optional | `boolean` | | Set to `true` to disable the authentication check
| `empty WhileLoading` | Optional | `boolean` | | Set to `true` to return `null` while the list is loading
| `id` | Optional | `string` | | The record identifier. If not provided, it will be deduced from the URL
diff --git a/packages/ra-core/src/controller/create/CreateBase.stories.tsx b/packages/ra-core/src/controller/create/CreateBase.stories.tsx
index e0622af12e3..d7dd302b6a1 100644
--- a/packages/ra-core/src/controller/create/CreateBase.stories.tsx
+++ b/packages/ra-core/src/controller/create/CreateBase.stories.tsx
@@ -151,7 +151,6 @@ export const AccessControl = ({
export const WithRenderProp = ({
dataProvider = defaultDataProvider,
callTimeOptions,
- ...props
}: {
dataProvider?: DataProvider;
callTimeOptions?: SaveHandlerCallbacks;
diff --git a/packages/ra-core/src/controller/list/ListBase.stories.tsx b/packages/ra-core/src/controller/list/ListBase.stories.tsx
index b06e317c848..5ffe3f678e8 100644
--- a/packages/ra-core/src/controller/list/ListBase.stories.tsx
+++ b/packages/ra-core/src/controller/list/ListBase.stories.tsx
@@ -296,19 +296,11 @@ export const WithRenderProps = ({
data,
error,
isPending,
- sort,
- filterValues,
page,
perPage,
setPage,
total,
} = controllerProps;
- const defaultValue = JSON.stringify({
- page,
- perPage,
- sort,
- filterValues,
- });
if (isPending) {
return Loading...
;
}
diff --git a/packages/ra-ui-materialui/src/list/List.tsx b/packages/ra-ui-materialui/src/list/List.tsx
index 503e64272da..23b980e1c81 100644
--- a/packages/ra-ui-materialui/src/list/List.tsx
+++ b/packages/ra-ui-materialui/src/list/List.tsx
@@ -99,7 +99,7 @@ export const List = (
};
export interface ListProps
- extends ListBaseProps,
+ extends Omit, 'children'>,
ListViewProps {}
const defaultFilter = {};
diff --git a/packages/ra-ui-materialui/src/list/ListView.tsx b/packages/ra-ui-materialui/src/list/ListView.tsx
index 67fca2c1f02..e6cd323b79f 100644
--- a/packages/ra-ui-materialui/src/list/ListView.tsx
+++ b/packages/ra-ui-materialui/src/list/ListView.tsx
@@ -191,7 +191,7 @@ export interface ListViewProps {
*
* );
*/
- children?: ReactNode;
+ children: ReactNode;
/**
* The component used to display the list. Defaults to .
From 4d1c035fb69d003ecba4ede21cb6737cd77ac162 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Fri, 11 Jul 2025 11:32:39 +0200
Subject: [PATCH 13/41] add check on render and children on all referenceBase
field
---
.../src/controller/field/ReferenceArrayFieldBase.tsx | 7 +++++++
.../ra-core/src/controller/field/ReferenceFieldBase.tsx | 6 ++++++
.../ra-core/src/controller/field/ReferenceOneFieldBase.tsx | 6 ++++++
3 files changed, 19 insertions(+)
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
index 6306eb88230..5ce6358c400 100644
--- a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
@@ -100,6 +100,13 @@ export const ReferenceArrayFieldBase = <
source,
queryOptions,
});
+
+ if (!render && !children) {
+ throw new Error(
+ " requires either a 'render' prop or 'children' prop"
+ );
+ }
+
return (
diff --git a/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx
index 29d3ed17970..f65579eb521 100644
--- a/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx
@@ -53,6 +53,12 @@ export const ReferenceFieldBase = <
const controllerProps =
useReferenceFieldController(props);
+ if (!render && !children) {
+ throw new Error(
+ " requires either a 'render' prop or 'children' prop"
+ );
+ }
+
if (
(empty &&
// no foreign key value
diff --git a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx
index 03e9b640346..6ed6634d6fa 100644
--- a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx
@@ -77,6 +77,12 @@ export const ReferenceOneFieldBase = <
return empty;
}
+ if (!render && !children) {
+ throw new Error(
+ " requires either a 'render' prop or 'children' prop"
+ );
+ }
+
return (
From d3851688227ccd1727d80e6d00cdb9cc3692068f Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Fri, 11 Jul 2025 15:09:40 +0200
Subject: [PATCH 14/41] add doc for ReferenceFieldBase
---
docs/ReferenceFieldBase.md | 291 ++++++++++++++++++
.../controller/field/ReferenceFieldBase.tsx | 11 +-
2 files changed, 297 insertions(+), 5 deletions(-)
create mode 100644 docs/ReferenceFieldBase.md
diff --git a/docs/ReferenceFieldBase.md b/docs/ReferenceFieldBase.md
new file mode 100644
index 00000000000..09aaa15a202
--- /dev/null
+++ b/docs/ReferenceFieldBase.md
@@ -0,0 +1,291 @@
+---
+layout: default
+title: "The ReferenceFieldBase Component"
+storybook_path: ra-core-controller-field-referencefieldbase--basic
+---
+
+# ``
+
+`` is useful for displaying many-to-one and one-to-one relationships, e.g. the details of a user when rendering a post authored by that user.
+`` is a headless component, handling only the logic. This Allows to plug any UI library on top. For the version incorporating UI see [``](/ReferenceField.html)
+
+## Usage
+
+### With children
+
+For instance, let's consider a model where a `post` has one author from the `users` resource, referenced by a `user_id` field.
+
+```
+┌──────────────┐ ┌────────────────┐
+│ posts │ │ users │
+│--------------│ │----------------│
+│ id │ ┌───│ id │
+│ user_id │╾──┘ │ name │
+│ title │ │ date_of_birth │
+│ published_at │ └────────────────┘
+└──────────────┘
+```
+
+In that case, use `` to display the post author's as follows:
+
+```jsx
+import { Show, SimpleShowLayout, ReferenceField, TextField, DateField } from 'react-admin';
+
+export const PostShow = () => (
+
+
+
+
+
+
+
+
+
+
+);
+```
+
+`` fetches the data, puts it in a [`RecordContext`](./useRecordContext.md), and its up to its children to handle the rendering by accessing the ReferencingContext using the useReferenceFieldContext hook.
+
+This component fetches a referenced record (`users` in this example) using the `dataProvider.getMany()` method, and passes it to the ReferenceFieldContext.
+
+```tsx
+import { Show, SimpleShowLayout, ReferenceField, TextField, DateField } from 'react-admin';
+
+export const MyReferenceFieldView = () => {
+ const context = useReferenceFieldContext();
+
+ const value = useFieldValue({ source });
+ if (context.isPending) {
+ return Loading...
;
+ }
+
+ if (context.error) {
+ return {context.error.toString()}
;
+ }
+
+ return {value}
;
+};
+
+export const MyReferenceField = () => (
+
+
+
+);
+```
+
+It uses `dataProvider.getMany()` instead of `dataProvider.getOne()` [for performance reasons](#performance). When using several `` in the same page (e.g. in a ``), this allows to call the `dataProvider` once instead of once per row.
+
+### With render prop
+
+Alternatively you can pass a render prop instead of children to be able to inline the rendering. The render function will then receive the reference field context directly.
+
+```jsx
+export const MyReferenceField = () => (
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return (
+
+ {error.message}
+
+ );
+ }
+ return {value}
;
+ }} />
+);
+```
+
+## Props
+
+| Prop | Required | Type | Default | Description |
+| ----------- | -------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
+| `source` | Required | `string` | - | Name of the property to display |
+| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'posts' |
+| `children` | Optional | `ReactNode` | - | React component to render the referenced record, the component need to use useReferenceFieldContext to access the context. |
+| `render` | Optional | `(context) => ReactNode` | - | Function that takes the referenceFieldContext and render the referenced record. Will take priority on children props if both are set. |
+| `empty` | Optional | `ReactNode` | - | What to render when the field has no value or when the reference is missing |
+| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options |
+| `sortBy` | Optional | `string | Function` | `source` | Name of the field to use for sorting when used in a Datagrid |
+
+
+## `empty`
+
+`` can display a custom message when the referenced record is missing, thanks to the `empty` prop.
+
+```jsx
+
+```
+
+`` renders the `empty` element when:
+
+- the referenced record is missing (no record in the `users` table with the right `user_id`), or
+- the field is empty (no `user_id` in the record).
+
+You can pass either a React element or a string to the `empty` prop:
+
+```jsx
+Missing user} />
+
+```
+
+
+## `link`
+
+To change the link from the `` page to the `` page, set the `link` prop to "show".
+
+```jsx
+
+```
+
+You can also prevent `` from adding a link to children by setting `link` to `false`.
+
+```jsx
+// No link
+
+```
+
+You can also use a custom `link` function to get a custom path for the children. This function must accept `record` and `reference` as arguments.
+
+```jsx
+// Custom path
+ `/my/path/to/${reference}/${record.id}`}
+/>
+```
+
+## `queryOptions`
+
+Use the `queryOptions` prop to pass options to [the `dataProvider.getMany()` query](./useGetOne.md#aggregating-getone-calls) that fetches the referenced record.
+
+For instance, to pass [a custom `meta`](./Actions.md#meta-parameter):
+
+{% raw %}
+```jsx
+
+
+
+```
+{% endraw %}
+
+## `reference`
+
+The resource to fetch for the related record.
+
+For instance, if the `posts` resource has a `user_id` field, set the `reference` to `users` to fetch the user related to each post.
+
+```jsx
+
+```
+
+## `sortBy`
+
+By default, when used in a ``, and when the user clicks on the column header of a ``, react-admin sorts the list by the field `source`. To specify another field name to sort by, set the `sortBy` prop.
+
+```jsx
+
+```
+
+## Performance
+
+
+
+When used in a ``, `` fetches the referenced record only once for the entire table.
+
+
+
+For instance, with this code:
+
+```jsx
+import { List, DataTable, ReferenceField, EditButton } from 'react-admin';
+
+export const PostList = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+);
+```
+
+React-admin accumulates and deduplicates the ids of the referenced records to make *one* `dataProvider.getMany()` call for the entire list, instead of n `dataProvider.getOne()` calls. So for instance, if the API returns the following list of posts:
+
+```js
+[
+ {
+ id: 123,
+ title: 'Totally agree',
+ user_id: 789,
+ },
+ {
+ id: 124,
+ title: 'You are right my friend',
+ user_id: 789
+ },
+ {
+ id: 125,
+ title: 'Not sure about this one',
+ user_id: 735
+ }
+]
+```
+
+Then react-admin renders the `` with a loader for the ``, fetches the API for the related users in one call (`dataProvider.getMany('users', { ids: [789,735] }`), and re-renders the list once the data arrives. This accelerates the rendering and minimizes network load.
+
+## Prefetching
+
+When you know that a page will contain a ``, you can configure the main page query to prefetch the referenced records to avoid a flicker when the data arrives. To do so, pass a `meta.prefetch` parameter to the page query.
+
+For example, the following code prefetches the authors referenced by the posts:
+
+{% raw %}
+```jsx
+const PostList = () => (
+
+
+
+
+ {/** renders without an additional request */}
+
+
+
+
+);
+```
+{% endraw %}
+
+**Note**: For prefetching to function correctly, your data provider must support [Prefetching Relationships](./DataProviders.md#prefetching-relationships). Refer to your data provider's documentation to verify if this feature is supported.
+
+**Note**: Prefetching is a frontend performance feature, designed to avoid flickers and repaints. It doesn't always prevent `` to fetch the data. For instance, when coming to a show view from a list view, the main record is already in the cache, so the page renders immediately, and both the page controller and the `` controller fetch the data in parallel. The prefetched data from the page controller arrives after the first render of the ``, so the data provider fetches the related data anyway. But from a user perspective, the page displays immediately, including the ``. If you want to avoid the `` to fetch the data, you can use the React Query Client's `staleTime` option.
+
+## Access Control
+
+If your authProvider implements [the `canAccess` method](./AuthProviderWriting.md#canaccess), React-Admin will verify whether users have access to the Show and Edit views.
+
+For instance, given the following `ReferenceFieldBase`:
+
+```jsx
+
+```
+
+React-Admin will call `canAccess` with the following parameters:
+- If the `users` resource has a Show view: `{ action: "show", resource: 'posts', record: Object }`
+- If the `users` resource has an Edit view: `{ action: "edit", resource: 'posts', record: Object }`
+
+And the link property of the referenceField context will be set accordingly. It will be set to false if the access is denied.
\ No newline at end of file
diff --git a/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx
index f65579eb521..51ce01fb11d 100644
--- a/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx
@@ -18,25 +18,25 @@ import { useFieldValue } from '../../util';
* added as child.
*
* @example // using recordRepresentation
- *
+ *
*
* @example // using a Field component to represent the record
- *
+ *
*
*
*
* @example // By default, includes a link to the page of the related record
* // (`/users/:userId` in the previous example).
* // Set the `link` prop to "show" to link to the page instead.
- *
+ *
*
* @example // You can also prevent `` from adding link to children
* // by setting `link` to false.
- *
+ *
*
* @example // Alternatively, you can also pass a custom function to `link`.
* // It must take reference and record as arguments and return a string
- * "/path/to/${reference}/${record}"} />
+ * "/path/to/${reference}/${record}"} />
*
* @default
* In previous versions of React-Admin, the prop `linkType` was used. It is now deprecated and replaced with `link`. However
@@ -70,6 +70,7 @@ export const ReferenceFieldBase = <
) {
return empty;
}
+
return (
From a78f8385eafc8b76543bd8cb5af8b4d6bc819d0b Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Fri, 11 Jul 2025 15:52:46 +0200
Subject: [PATCH 15/41] add doc for ReferenceManyFieldBase
---
docs/ReferenceManyFieldBase.md | 448 ++++++++++++++++++
.../field/ReferenceManyFieldBase.stories.tsx | 60 +--
2 files changed, 458 insertions(+), 50 deletions(-)
create mode 100644 docs/ReferenceManyFieldBase.md
diff --git a/docs/ReferenceManyFieldBase.md b/docs/ReferenceManyFieldBase.md
new file mode 100644
index 00000000000..c276d9fac17
--- /dev/null
+++ b/docs/ReferenceManyFieldBase.md
@@ -0,0 +1,448 @@
+---
+layout: default
+title: "The ReferenceManyFieldBase Component"
+storybook_path: ra-ui-materialui-fields-referencemanyfieldbase--basic
+---
+
+# ``
+
+`` is useful for displaying a list of related records via a one-to-many relationship, when the foreign key is carried by the referenced resource.
+
+This component fetches a list of referenced records by a reverse lookup of the current `record.id` in the `target` field of another resource (using the `dataProvider.getManyReference()` REST method), and puts them in a [`ListContext`](./useListContext.md). This component is headless, and its children need to use the data from this context to render the desired ui.
+For a component handling the UI too use [the `` component](./ReferenceManyField.md) instead.
+
+**Tip**: If the relationship is materialized by an array of ids in the initial record, use [the `` component](./ReferenceArrayFieldBase.md) instead.
+
+
+## Usage
+
+### With children
+
+For instance, if an `author` has many `books`, and each book resource exposes an `author_id` field:
+
+```
+┌────────────────┐ ┌──────────────┐
+│ authors │ │ books │
+│----------------│ │--------------│
+│ id │───┐ │ id │
+│ first_name │ └──╼│ author_id │
+│ last_name │ │ title │
+│ date_of_birth │ │ published_at │
+└────────────────┘ └──────────────┘
+```
+
+`` can render the titles of all the books by a given author.
+
+```jsx
+import { Show, SimpleShowLayout, ReferenceManyFieldBase, DataTable, TextField, DateField } from 'react-admin';
+
+const CustomAuthorView = ({
+ source,
+ children,
+}: {
+ source: string;
+}) => {
+ const context = useListController();
+
+ if (context.isPending) {
+ return Loading...
;
+ }
+
+ if (context.error) {
+ return {context.error.toString()}
;
+ }
+ return (
+
+ {listContext.data?.map((datum, index) => (
+
{datum[source]}
+ ))}
+
+ );
+};
+
+const AuthorShow = () => (
+
+
+
+
+
+
+
+
+
+);
+```
+
+`` accepts a `reference` attribute, which specifies the resource to fetch for the related record. It also accepts a `source` attribute which defines the field containing the value to look for in the `target` field of the referenced resource. By default, this is the `id` of the resource (`authors.id` in the previous example).
+
+You can also use `` in a list, e.g. to display the authors of the comments related to each post in a list by matching `post.id` to `comment.post_id`:
+
+```jsx
+import { List, DataTable, ChipField, ReferenceManyFieldBase, SingleFieldList } from 'react-admin';
+
+export const PostList = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+```
+
+## Props
+
+| Prop | Required | Type | Default | Description |
+| -------------- | -------- | --------------------------------------------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------- |
+| `children` | Required if no render | `Element` | - | Element that render a list of records based on a `ListContext` |
+| `render` | Required if no children | `(listContext) => Element` | - | Function that receives a `ListContext` and render elements |
+| `debounce` | Optional | `number` | 500 | debounce time in ms for the `setFilters` callbacks |
+| `empty` | Optional | `ReactNode` | - | Element to display when there are no related records. |
+| `filter` | Optional | `Object` | - | Filters to use when fetching the related records, passed to `getManyReference()` |
+| `pagination` | Optional | `Element` | - | Pagination element to display pagination controls. empty by default (no pagination) |
+| `renderPagination` | Optional | `(listContext) => Element` | - | Pagination function that receives a `ListContext` and render pagination controls. |
+| `perPage` | Optional | `number` | 25 | Maximum number of referenced records to fetch |
+| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v3/docs/react/reference/useQuery) | `{}` | `react-query` options for the `getMany` query |
+| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'books' |
+| `sort` | Optional | `{ field, order }` | `{ field: 'id', order: 'DESC' }` | Sort order to use when fetching the related records, passed to `getManyReference()` |
+| `source` | Optional | `string` | `id` | Target field carrying the relationship on the source record (usually 'id') |
+| `storeKey` | Optional | `string` | - | The key to use to store the records selection state |
+| `target` | Required | `string` | - | Target field carrying the relationship on the referenced resource, e.g. 'user_id' |
+
+## `children`
+
+`` renders its children inside a [`ListContext`](./useListContext.md). This means you can use any component that uses a `ListContext`:
+
+- [``](./SingleFieldList.md)
+- [``](./DataTable.md)
+- [``](./Datagrid.md)
+- [``](./SimpleList.md)
+- [``](./EditableDatagrid.md)
+- [``](./Calendar.md)
+- Or a component of your own (check the [``](./WithListContext.md) and the [`useListContext`](./useListContext.md) chapters to learn how).
+
+For instance, use a `` to render the related records in a table:
+
+```jsx
+import { Show, SimpleShowLayout, TextField, ReferenceManyFieldBase, DataTable, DateField } from 'react-admin';
+
+export const AuthorShow = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+```
+
+## `render`
+
+Alternatively to children you can pass a render prop to ``. The render prop will receive the list context as its argument, allowing to inline the render logic.
+
+```jsx
+import { Show, SimpleShowLayout, ReferenceManyFieldBase, DataTable, TextField, DateField } from 'react-admin';
+
+const CustomAuthorView = ({
+ source,
+ children,
+}: {
+ source: string;
+}) => {
+ const context = useListController();
+
+ if (context.isPending) {
+ return Loading...
;
+ }
+
+ if (context.error) {
+ return {context.error.toString()}
;
+ }
+ return (
+
+ {listContext.data?.map((datum, index) => (
+
{datum[source]}
+ ))}
+
+ );
+};
+
+const AuthorShow = () => (
+
+
+
+
+ {
+
+ if (context.isPending) {
+ return Loading...
;
+ }
+
+ if (context.error) {
+ return {context.error.toString()}
;
+ }
+ return (
+
+ {listContext.data?.map((author, index) => (
+
{author.name}
+ ))}
+
+ );
+ }
+ }
+ />
+
+
+);
+```
+
+## `debounce`
+
+By default, `` does not refresh the data as soon as the user enters data in the filter form. Instead, it waits for half a second of user inactivity (via `lodash.debounce`) before calling the `dataProvider` on filter change. This is to prevent repeated (and useless) calls to the API.
+
+You can customize the debounce duration in milliseconds - or disable it completely - by passing a `debounce` prop to the `` component:
+
+```jsx
+// wait 1 seconds instead of 500 milliseconds before calling the dataProvider
+const PostCommentsField = () => (
+
+ ...
+
+);
+```
+
+## `empty`
+
+Use `empty` to customize the text displayed when the related record is empty.
+
+```jsx
+
+ ...
+
+```
+
+`empty` also accepts a translation key.
+
+```jsx
+
+ ...
+
+```
+
+`empty` also accepts a `ReactNode`.
+
+```jsx
+ }
+>
+ ...
+
+```
+
+## `filter`: Permanent Filter
+
+You can filter the query used to populate the possible values. Use the `filter` prop for that.
+
+{% raw %}
+
+```jsx
+
+ ...
+
+```
+
+{% endraw %}
+
+## Filtering The References
+
+
+
+ Your browser does not support the video tag.
+
+
+You can add filters to `` by adding [``](./FilterForm.md) and [``](./FilterButton.md):
+
+{% raw %}
+
+```jsx
+const filters = [ ];
+
+const AuthorEdit = () => (
+
+
+
+
+
+
+ ...
+
+
+
+
+);
+```
+
+{% endraw %}
+
+## `pagination`
+
+If you want to allow users to paginate the list, pass a `` element as the `pagination` prop:
+
+```jsx
+import { Pagination } from 'react-admin';
+
+ } reference="comments" target="post_id">
+ ...
+
+```
+
+## `perPage`
+
+By default, react-admin restricts the possible values to 25 and displays no pagination control. You can change the limit by setting the `perPage` prop:
+
+```jsx
+
+ ...
+
+```
+
+## `queryOptions`
+
+Use the `queryOptions` prop to pass options to [the `dataProvider.getMany()` query](./useGetOne.md#aggregating-getone-calls) that fetches the referenced record.
+
+For instance, to pass [a custom `meta`](./Actions.md#meta-parameter):
+
+{% raw %}
+```jsx
+
+```
+{% endraw %}
+
+## `reference`
+
+The name of the resource to fetch for the related records.
+
+For instance, if you want to display the `books` of a given `author`, the `reference` name should be `books`:
+
+```jsx
+
+
+
+
+
+
+```
+
+## `sort`
+
+By default, it orders the possible values by id desc. You can change this order by setting the `sort` prop (an object with `field` and `order` properties).
+
+{% raw %}
+```jsx
+
+ ...
+
+```
+{% endraw %}
+
+## `source`
+
+By default, `ReferenceManyFieldBase` uses the `id` field as target for the reference. If the foreign key points to another field of your record, you can select it with the `source` prop.
+
+```jsx
+
+ ...
+
+```
+
+## `storeKey`
+
+By default, react-admin stores the reference list selection state in localStorage so that users can come back to the list and find it in the same state as when they left it. React-admin uses the main resource, record id and reference resource as the identifier to store the selection state (under the key `${resource}.${record.id}.${reference}.selectedIds`).
+
+If you want to display multiple lists of the same reference and keep distinct selection states for each one, you must give each list a unique `storeKey` property.
+
+In the example below, both lists use the same reference ('books'), but their selection states are stored separately (under the store keys `'authors.1.books.selectedIds'` and `'custom.selectedIds'` respectively). This allows to use both components in the same page, each having its own state.
+
+{% raw %}
+```jsx
+
+
+
+
+
+
+
+
+
+
+
+
+```
+{% endraw %}
+
+## `target`
+
+Name of the field carrying the relationship on the referenced resource. For instance, if an `author` has many `books`, and each book resource exposes an `author_id` field, the `target` would be `author_id`.
+
+```jsx
+
+
+
+
+
+
+```
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
index c37a3fec466..1e16418a2fb 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
@@ -5,8 +5,7 @@ import { Resource } from '../../core/Resource';
import { ShowBase } from '../../controller/show/ShowBase';
import { TestMemoryRouter } from '../../routing';
import { ReferenceManyFieldBase } from './ReferenceManyFieldBase';
-import { useReferenceManyFieldController } from './useReferenceManyFieldController';
-import { useListContextWithProps } from '../list';
+import { useListContext, useListContextWithProps } from '../list';
export default {
title: 'ra-core/controller/field/ReferenceManyFieldBase',
@@ -90,11 +89,7 @@ export const Basic = ({ dataProvider = dataProviderWithAuthors }) => (
source="id"
reference="books"
>
-
+
@@ -148,11 +143,7 @@ export const Errored = ({ dataProvider = dataProviderWithAuthorsError }) => (
target="id"
source="author"
>
-
+
@@ -200,11 +191,7 @@ export const Loading = ({ dataProvider = dataProviderWithAuthorsLoading }) => (
target="id"
source="author"
>
-
+
@@ -241,11 +228,7 @@ export const WithPagination = ({ dataProvider = dataProviderWithAuthors }) => (
perPage={2}
pagination={ }
>
-
+
@@ -364,11 +347,7 @@ export const WithRenderPagination = ({
);
}}
>
-
+
@@ -379,22 +358,8 @@ export const WithRenderPagination = ({
);
-const MyReferenceManyField = ({
- reference,
- target,
- source,
- children,
-}: {
- children: React.ReactNode;
- reference: string;
- target: string;
- source: string;
-}) => {
- const context = useReferenceManyFieldController({
- reference,
- target,
- source,
- });
+const MyReferenceManyField = ({ children }: { children: React.ReactNode }) => {
+ const context = useListContext();
if (context.isPending) {
return Loading...
;
@@ -407,7 +372,7 @@ const MyReferenceManyField = ({
};
const List = ({ source }: { source: string }) => {
- const listContext = useListContextWithProps();
+ const listContext = useListContext();
return (
{listContext.data?.map((datum, index) => (
@@ -418,12 +383,7 @@ const List = ({ source }: { source: string }) => {
};
const Pagination = () => {
- const {
- page = 1,
- setPage,
- total = 0,
- perPage = 0,
- } = useListContextWithProps();
+ const { page = 1, setPage, total = 0, perPage = 0 } = useListContext();
const nextPage = () => {
setPage?.(page + 1);
};
From 665c58d0306d9c520bd947e09b2d22291480f1b3 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Fri, 11 Jul 2025 16:01:55 +0200
Subject: [PATCH 16/41] remove unneeded renderPagination prop from
ReferenceManyFieldBase
---
docs/ReferenceManyFieldBase.md | 6 +-
.../field/ReferenceManyFieldBase.spec.tsx | 24 ++----
.../field/ReferenceManyFieldBase.stories.tsx | 77 ++-----------------
.../field/ReferenceManyFieldBase.tsx | 8 +-
4 files changed, 20 insertions(+), 95 deletions(-)
diff --git a/docs/ReferenceManyFieldBase.md b/docs/ReferenceManyFieldBase.md
index c276d9fac17..3dd6ccba001 100644
--- a/docs/ReferenceManyFieldBase.md
+++ b/docs/ReferenceManyFieldBase.md
@@ -108,7 +108,6 @@ export const PostList = () => (
| `empty` | Optional | `ReactNode` | - | Element to display when there are no related records. |
| `filter` | Optional | `Object` | - | Filters to use when fetching the related records, passed to `getManyReference()` |
| `pagination` | Optional | `Element` | - | Pagination element to display pagination controls. empty by default (no pagination) |
-| `renderPagination` | Optional | `(listContext) => Element` | - | Pagination function that receives a `ListContext` and render pagination controls. |
| `perPage` | Optional | `number` | 25 | Maximum number of referenced records to fetch |
| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v3/docs/react/reference/useQuery) | `{}` | `react-query` options for the `getMany` query |
| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'books' |
@@ -153,7 +152,8 @@ export const AuthorShow = () => (
## `render`
-Alternatively to children you can pass a render prop to ``. The render prop will receive the list context as its argument, allowing to inline the render logic.
+Alternatively to children you can pass a render prop to ``. The render prop will receive the list context as its argument, allowing to inline the render logic for both the list and the pagination.
+When receiving a render prop the `` component will ignore the children and the pagination property.
```jsx
import { Show, SimpleShowLayout, ReferenceManyFieldBase, DataTable, TextField, DateField } from 'react-admin';
@@ -329,6 +329,8 @@ import { Pagination } from 'react-admin';
```
+***Note:*** The pagination prop will be ignored if a render prop is specified
+
## `perPage`
By default, react-admin restricts the possible values to 25 and displays no pagination control. You can change the limit by setting the `perPage` prop:
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.spec.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.spec.tsx
index 09cfc6aa146..783bd09992f 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.spec.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.spec.tsx
@@ -41,8 +41,8 @@ describe('ReferenceManyFieldBase', () => {
return {resource}
;
};
const dataProvider = testDataProvider({
- // @ts-ignore
getList: () =>
+ // @ts-ignore
Promise.resolve({ data: [{ id: 1 }, { id: 2 }], total: 2 }),
});
render(
@@ -156,23 +156,13 @@ describe('ReferenceManyFieldBase', () => {
).not.toBeNull();
});
});
- });
-
- it('should render pagination using renderPagination prop', async () => {
- render( );
- await waitFor(() => {
- expect(screen.queryByText('1 - 2 of 3')).not.toBeNull();
- expect(screen.queryByText('Next Page')).not.toBeNull();
- expect(screen.queryByText('Previous Page')).not.toBeNull();
- });
- screen.getByText('Next Page').click();
- await waitFor(() => {
- expect(screen.queryByText('3 - 3 of 3')).not.toBeNull();
- });
- screen.getByText('Previous Page').click();
- await waitFor(() => {
- expect(screen.queryByText('1 - 2 of 3')).not.toBeNull();
+ it('should not render pagination when receiving a render prop', async () => {
+ render(Custom Pagination
} />);
+ await waitFor(() => {
+ expect(screen.queryByText('War and Peace')).not.toBeNull();
+ });
+ expect(screen.queryByText('Custom Pagination')).toBeNull();
});
});
});
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
index 1e16418a2fb..8c332737830 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
@@ -239,7 +239,13 @@ export const WithPagination = ({ dataProvider = dataProviderWithAuthors }) => (
);
-export const WithRenderProp = ({ dataProvider = dataProviderWithAuthors }) => (
+export const WithRenderProp = ({
+ dataProvider = dataProviderWithAuthors,
+ pagination,
+}: {
+ dataProvider?: any;
+ pagination?: React.ReactNode;
+}) => (
(
reference="books"
target="author"
source="id"
+ pagination={pagination}
render={({ error, isPending, data }) => {
if (isPending) {
return Loading...
;
@@ -290,74 +297,6 @@ export const WithRenderProp = ({ dataProvider = dataProviderWithAuthors }) => (
);
-export const WithRenderPagination = ({
- dataProvider = dataProviderWithAuthors,
-}) => (
-
-
-
-
- {
- const nextPage = () => {
- setPage(page + 1);
- };
- const previousPage = () => {
- setPage(page - 1);
- };
- return (
-
-
- previous page
-
- {`${(page - 1) * perPage + 1} - ${Math.min(page * perPage, total)} of ${total}`}
- = total / perPage}
- onClick={nextPage}
- >
- next page
-
-
- );
- }}
- >
-
-
-
-
-
- }
- />
-
-
-);
-
const MyReferenceManyField = ({ children }: { children: React.ReactNode }) => {
const context = useListContext();
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
index 87a6eadbeff..8889dfce210 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
@@ -68,7 +68,6 @@ export const ReferenceManyFieldBase = <
filter = defaultFilter,
page = 1,
pagination = null,
- renderPagination = null,
perPage = 25,
record,
reference,
@@ -130,9 +129,7 @@ export const ReferenceManyFieldBase = <
{render ? render(controllerProps) : children}
- {renderPagination
- ? renderPagination(controllerProps)
- : pagination}
+ {!render && pagination}
);
@@ -147,9 +144,6 @@ export interface ReferenceManyFieldBaseProps<
> {
children?: ReactNode;
render?: (props: ListControllerResult) => ReactNode;
- renderPagination?: (
- props: ListControllerResult
- ) => ReactNode;
empty?: ReactNode;
pagination?: ReactNode;
}
From b39185d16f26ef9bee3618a9c68cbaed69d1d36f Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Fri, 11 Jul 2025 16:12:46 +0200
Subject: [PATCH 17/41] remove unneeded renderPagination from
ReferenceArrayFieldBase
---
.../field/ReferenceArrayFieldBase.spec.tsx | 20 ++---
.../field/ReferenceArrayFieldBase.stories.tsx | 74 +------------------
.../field/ReferenceArrayFieldBase.tsx | 8 +-
3 files changed, 9 insertions(+), 93 deletions(-)
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.spec.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.spec.tsx
index 9ad47426858..d9fbbe24225 100644
--- a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.spec.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.spec.tsx
@@ -41,8 +41,8 @@ describe('ReferenceArrayFieldBase', () => {
return {resource}
;
};
const dataProvider = testDataProvider({
- // @ts-ignore
getList: () =>
+ // @ts-ignore
Promise.resolve({ data: [{ id: 1 }, { id: 2 }], total: 2 }),
});
render(
@@ -103,21 +103,11 @@ describe('ReferenceArrayFieldBase', () => {
});
});
- it('should render pagination using renderPagination prop', async () => {
- render( );
- await waitFor(() => {
- expect(screen.queryByText('1 - 3 of 8')).not.toBeNull();
- expect(screen.queryByText('Next Page')).not.toBeNull();
- expect(screen.queryByText('Previous Page')).not.toBeNull();
- });
- screen.getByText('Next Page').click();
+ it('should not render pagination when given a render prop', async () => {
+ render(Custom Pagination} />);
await waitFor(() => {
- expect(screen.queryByText('4 - 6 of 8')).not.toBeNull();
- });
- screen.getByText('Previous Page').click();
-
- await waitFor(() => {
- expect(screen.queryByText('1 - 3 of 8')).not.toBeNull();
+ expect(screen.queryByText('John Lennon')).not.toBeNull();
});
+ expect(screen.queryByText('Custom Pagination')).toBeNull();
});
});
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx
index eabe88c7b9e..9887c052e53 100644
--- a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx
@@ -111,8 +111,10 @@ export const Loading = () => (
export const WithRenderProp = ({
dataProvider = defaultDataProvider,
+ pagination,
}: {
dataProvider?: DataProvider;
+ pagination?: React.ReactElement;
}) => (
{
if (isPending) {
return Loading...
;
@@ -164,77 +167,6 @@ export const WithRenderProp = ({
);
-export const WithRenderPaginationProp = ({
- dataProvider = defaultDataProvider,
-}: {
- dataProvider: DataProvider;
-}) => (
-
-
-
-
- {
- const nextPage = () => {
- setPage?.(page + 1);
- };
- const previousPage = () => {
- setPage?.(page - 1);
- };
- return (
-
-
- Previous Page
-
-
- {`${(page - 1) * perPage + 1} - ${Math.min(page * perPage, total)} of ${total}`}
-
- = total / perPage}
- onClick={nextPage}
- >
- Next Page
-
-
- );
- }}
- >
-
-
-
-
-
- }
- />
-
-
-);
-
const MyReferenceArrayField = (props: { children: React.ReactNode }) => {
const context = useListContext();
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
index 5ce6358c400..1023e354741 100644
--- a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
@@ -75,7 +75,6 @@ export const ReferenceArrayFieldBase = <
children,
render,
pagination,
- renderPagination,
filter,
page = 1,
perPage,
@@ -111,9 +110,7 @@ export const ReferenceArrayFieldBase = <
{render ? render(controllerProps) : children}
- {renderPagination
- ? renderPagination(controllerProps)
- : pagination}
+ {!render && pagination}
);
@@ -125,9 +122,6 @@ export interface ReferenceArrayFieldBaseProps<
> extends FieldProps {
children?: ReactNode;
render?: (props: ListControllerResult) => ReactElement;
- renderPagination?: (
- props: ListControllerResult
- ) => ReactElement;
filter?: FilterPayload;
page?: number;
pagination?: ReactElement;
From 70b260cc784091b02c511f9281e9958f12425893 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Fri, 11 Jul 2025 16:40:12 +0200
Subject: [PATCH 18/41] add doc on ReferenceArrayFieldBase
---
docs/ReferenceArrayFieldBase.md | 260 ++++++++++++++++++
docs/ReferenceManyFieldBase.md | 2 +-
.../field/ReferenceArrayFieldBase.stories.tsx | 10 +-
3 files changed, 263 insertions(+), 9 deletions(-)
create mode 100644 docs/ReferenceArrayFieldBase.md
diff --git a/docs/ReferenceArrayFieldBase.md b/docs/ReferenceArrayFieldBase.md
new file mode 100644
index 00000000000..06485316e11
--- /dev/null
+++ b/docs/ReferenceArrayFieldBase.md
@@ -0,0 +1,260 @@
+---
+layout: default
+title: "The ReferenceArrayFieldBase Component"
+storybook_path: ra-ui-materialui-fields-referencearrayfieldbase--basic
+---
+
+# ``
+
+Use `` to display a list of related records, via a one-to-many relationship materialized by an array of foreign keys.
+
+`` fetches a list of referenced records (using the `dataProvider.getMany()` method), and puts them in a [`ListContext`](./useListContext.md). This component is headless, and its children need to use the data from this context to render the desired ui.
+For a component handling the UI too use [the `` component](./ReferenceArrayField.md) instead.
+
+**Tip**: If the relationship is materialized by a foreign key on the referenced resource, use [the `` component](./ReferenceManyFieldBase.md) instead.
+
+## Usage
+
+For instance, let's consider a model where a `post` has many `tags`, materialized to a `tags_ids` field containing an array of ids:
+
+```
+┌──────────────┐ ┌────────┐
+│ posts │ │ tags │
+│--------------│ │--------│
+│ id │ ┌───│ id │
+│ title │ │ │ name │
+│ body │ │ └────────┘
+│ is_published │ │
+│ tag_ids │╾──┘
+└──────────────┘
+```
+
+A typical `post` record therefore looks like this:
+
+```json
+{
+ "id": 1,
+ "title": "Hello world",
+ "body": "...",
+ "is_published": true,
+ "tags_ids": [1, 2, 3]
+}
+```
+
+In that case, use `` to display the post tag names as Chips as follows:
+
+```jsx
+import { List, DataTable, ReferenceArrayFieldBase } from 'react-admin';
+
+const MyPostView = (props: { children: React.ReactNode }) => {
+ const context = useListContext();
+
+ if (context.isPending) {
+ return Loading...
;
+ }
+
+ if (context.error) {
+ return {context.error.toString()}
;
+ }
+ return (
+
+ {listContext.data?.map((tag, index) => (
+
{tag.name}
+ ))}
+
+ );
+};
+
+export const PostList = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+```
+
+`` expects a `reference` attribute, which specifies the resource to fetch for the related records. It also expects a `source` attribute, which defines the field containing the list of ids to look for in the referenced resource.
+
+`` fetches the `tag` resources related to each `post` resource by matching `post.tag_ids` to `tag.id`.
+
+You can change how the list of related records is rendered by passing a custom child reading the `ListContext` (e.g. a [``](./DataTable.md)). See the [`children`](#children) section for details.
+
+## Props
+
+| Prop | Required | Type | Default | Description |
+| -------------- | -------- | --------------------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------ |
+| `source` | Required | `string` | - | Name of the property to display |
+| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'tags' |
+| `children` | Required if no render | `Element` | | One or several elements that render a list of records based on a `ListContext` |
+| `render` | Required if no children | `Element` | | A function that takes a list context and render a list of records |
+| `filter` | Optional | `Object` | - | Filters to use when fetching the related records (the filtering is done client-side) |
+| `pagination` | Optional | `Element` | - | Pagination element to display pagination controls. empty by default (no pagination) |
+| `perPage` | Optional | `number` | 1000 | Maximum number of results to display |
+| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` options for the `getMany` query |
+| `sort` | Optional | `{ field, order }` | `{ field: 'id', order: 'DESC' }` | Sort order to use when displaying the related records (the sort is done client-side) |
+| `sortBy` | Optional | `string | Function` | `source` | When used in a `List`, name of the field to use for sorting when the user clicks on the column header. |
+
+## `children`
+
+You can pass any component of your own as child, to render the list of related records as you wish.
+You can access the list context using the `useListContext` hook.
+
+
+```jsx
+
+
+
+
+
+// With MyPostView like:
+const MyPostView = (props: { children: React.ReactNode }) => {
+ const context = useListContext();
+
+ if (context.isPending) {
+ return Loading...
;
+ }
+
+ if (context.error) {
+ return {context.error.toString()}
;
+ }
+ return (
+
+ {listContext.data?.map((tag, index) => (
+
{tag.name}
+ ))}
+
+ );
+};
+```
+
+## `render`
+
+Alternatively to children you can pass a render prop to ``. The render prop will receive the list context as its argument, allowing to inline the render logic for both the list and the pagination.
+When receiving a render prop the `` component will ignore the children and the pagination property.
+
+
+```jsx
+ {
+
+ if (context.isPending) {
+ return Loading...
;
+ }
+
+ if (context.error) {
+ return {context.error.toString()}
;
+ }
+ return (
+
+ {listContext.data?.map((tag, index) => (
+
{tag.name}
+ ))}
+
+ );
+ }}
+/>
+```
+
+## `filter`
+
+`` fetches all the related records, and displays them all, too. You can use the `filter` prop to filter the list of related records to display (this works by filtering the records client-side, after the fetch).
+
+For instance, to render only tags that are 'published', you can use the following code:
+
+{% raw %}
+```jsx
+
+```
+{% endraw %}
+
+## `pagination`
+
+`` fetches *all* the related fields, and puts them all in a `ListContext`. If a record has a large number of related records, you can limit the number of displayed records with the [`perPage`](#perpage) prop. Then, let users display remaining records by rendering pagination controls. For that purpose, pass a pagination element to the `pagination` prop.
+
+For instance, to limit the display of related records to 10, you can use the following code:
+
+```jsx
+import { Pagination, ReferenceArrayFieldBase } from 'react-admin';
+
+ }
+/>
+```
+
+***Note:*** The pagination prop will be ignored when the component receive a render prop
+
+## `perPage`
+
+`` fetches *all* the related fields, and puts them all in a `ListContext`. If a record has a large number of related records, it may be a good idea to limit the number of displayed records. The `perPage` prop allows to create a client-side pagination for the related records.
+
+For instance, to limit the display of related records to 10, you can use the following code:
+
+```jsx
+
+```
+
+If you want to let the user display the remaining records, you have to pass a [`pagination`](#pagination) element.
+
+## `queryOptions`
+
+Use the `queryOptions` prop to pass options to [the `dataProvider.getMany()` query](./useGetOne.md#aggregating-getone-calls) that fetches the referenced record.
+
+For instance, to pass [a custom `meta`](./Actions.md#meta-parameter):
+
+{% raw %}
+```jsx
+
+```
+{% endraw %}
+
+
+## `reference`
+
+The resource to fetch for the relateds record.
+
+For instance, if the `posts` resource has a `tag_ids` field, set the `reference` to `tags` to fetch the tags related to each post.
+
+```jsx
+
+```
+
+## `sort`
+
+By default, the related records are displayed in the order in which they appear in the `source`. For instance, if the current record is `{ id: 1234, title: 'Lorem Ipsum', tag_ids: [1, 23, 4] }`, a `` on the `tag_ids` field will display tags in the order 1, 23, 4.
+
+`` can force a different order (via a client-side sort after fetch) if you specify a `sort` prop.
+
+For instance, to sort tags by title in ascending order, you can use the following code:
+
+{% raw %}
+```jsx
+
+```
+{% endraw %}
diff --git a/docs/ReferenceManyFieldBase.md b/docs/ReferenceManyFieldBase.md
index 3dd6ccba001..ee9234be04a 100644
--- a/docs/ReferenceManyFieldBase.md
+++ b/docs/ReferenceManyFieldBase.md
@@ -102,7 +102,7 @@ export const PostList = () => (
| Prop | Required | Type | Default | Description |
| -------------- | -------- | --------------------------------------------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------- |
-| `children` | Required if no render | `Element` | - | Element that render a list of records based on a `ListContext` |
+| `children` | Required if no render | `Element` | - | One or several elements that render a list of records based on a `ListContext` |
| `render` | Required if no children | `(listContext) => Element` | - | Function that receives a `ListContext` and render elements |
| `debounce` | Optional | `number` | 500 | debounce time in ms for the `setFilters` callbacks |
| `empty` | Optional | `ReactNode` | - | Element to display when there are no related records. |
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx
index 9887c052e53..a3cc87c554e 100644
--- a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx
@@ -9,7 +9,6 @@ import {
ShowBase,
TestMemoryRouter,
useListContext,
- useListContextWithProps,
} from '../..';
import { QueryClient } from '@tanstack/react-query';
@@ -181,7 +180,7 @@ const MyReferenceArrayField = (props: { children: React.ReactNode }) => {
};
const List = ({ source }: { source: string }) => {
- const listContext = useListContextWithProps();
+ const listContext = useListContext();
return (
{listContext.data?.map((datum, index) => (
@@ -192,12 +191,7 @@ const List = ({ source }: { source: string }) => {
};
const Pagination = () => {
- const {
- page = 1,
- setPage,
- total = 0,
- perPage = 0,
- } = useListContextWithProps();
+ const { page = 1, setPage, total = 0, perPage = 0 } = useListContext();
const nextPage = () => {
setPage?.(page + 1);
};
From 58b7a55fabd4c019b3d73e687037e6964f817337 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Fri, 11 Jul 2025 17:48:46 +0200
Subject: [PATCH 19/41] add doc on ReferenceManyFieldBase
---
docs/ReferenceFieldBase.md | 4 +-
docs/ReferenceManyFieldBase.md | 1 -
docs/ReferenceOneFieldBase.md | 299 ++++++++++++++++++
.../field/ReferenceOneFieldBase.stories.tsx | 35 +-
4 files changed, 311 insertions(+), 28 deletions(-)
create mode 100644 docs/ReferenceOneFieldBase.md
diff --git a/docs/ReferenceFieldBase.md b/docs/ReferenceFieldBase.md
index 09aaa15a202..a835b48c2f4 100644
--- a/docs/ReferenceFieldBase.md
+++ b/docs/ReferenceFieldBase.md
@@ -7,7 +7,7 @@ storybook_path: ra-core-controller-field-referencefieldbase--basic
# ``
`` is useful for displaying many-to-one and one-to-one relationships, e.g. the details of a user when rendering a post authored by that user.
-`` is a headless component, handling only the logic. This Allows to plug any UI library on top. For the version incorporating UI see [``](/ReferenceField.html)
+`` is a headless component, handling only the logic. This Allows to use any UI library for the render. For a version incorporating UI see [``](/ReferenceField.html)
## Usage
@@ -37,7 +37,7 @@ export const PostShow = () => (
-
+
diff --git a/docs/ReferenceManyFieldBase.md b/docs/ReferenceManyFieldBase.md
index ee9234be04a..622c3140acc 100644
--- a/docs/ReferenceManyFieldBase.md
+++ b/docs/ReferenceManyFieldBase.md
@@ -126,7 +126,6 @@ export const PostList = () => (
- [``](./SimpleList.md)
- [``](./EditableDatagrid.md)
- [``](./Calendar.md)
-- Or a component of your own (check the [``](./WithListContext.md) and the [`useListContext`](./useListContext.md) chapters to learn how).
For instance, use a `` to render the related records in a table:
diff --git a/docs/ReferenceOneFieldBase.md b/docs/ReferenceOneFieldBase.md
new file mode 100644
index 00000000000..c032038fe71
--- /dev/null
+++ b/docs/ReferenceOneFieldBase.md
@@ -0,0 +1,299 @@
+---
+layout: default
+title: "The ReferenceOneFieldBase Component"
+storybook_path: ra-ui-materialui-fields-referenceonefieldbase--basic
+---
+
+# ``
+
+This field fetches a one-to-one relationship, e.g. the details of a book, when using a foreign key on the distant resource.
+
+```
+┌──────────────┐ ┌──────────────┐
+│ books │ │ book_details │
+│--------------│ │--------------│
+│ id │───┐ │ id │
+│ title │ └──╼│ book_id │
+│ published_at │ │ genre │
+└──────────────┘ │ ISBN │
+ └──────────────┘
+```
+
+`` behaves like ``: 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.
+
+`` is a headless component, handling only the logic. This Allows to use any UI library for the render. For a version incorporating UI see [``](/ReferenceOneField.html)
+
+For the inverse relationships (the book linked to a book_detail), you can use a [``](./ReferenceFieldBase.md).
+
+## Usage
+
+Here is how to render a field of the `book_details` resource inside a Show view for the `books` resource:
+
+```jsx
+
+const BookShow = () => (
+
+
+
+
+
+
+
+
+
+
+);
+
+// with MyBookView something like
+const MyBookView = ({ source }) => {
+ const context = useReferenceOneFieldContext({
+ reference,
+ target,
+ });
+
+ if (context.isPending) {
+ return Loading...
;
+ }
+
+ if (context.error) {
+ return {context.error.toString()}
;
+ }
+ return (
+
+
{record ? record.genre : ''}
+
{record ? record.ISBN : ''}
+
+ );
+}
+```
+
+**Tip**: As with ``, you can call `` as many times as you need in the same component, react-admin will only make one call to `dataProvider.getManyReference()` per reference.
+
+## Props
+
+| Prop | Required | Type | Default | Description |
+| -------------- | -------- | ------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------- |
+| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'book_details' |
+| `target` | Required | string | - | Target field carrying the relationship on the referenced resource, e.g. 'book_id' |
+| `children` | Required if no render | `Element` | - | React component to render the referenced record, the component need to use useReferenceOneFieldContext to access the context. |
+| `render` | Required if no children | `Element` | - | A function that takes the reference field context and return a React element |
+| `empty` | Optional | `ReactNode` | - | The text or element to display when the referenced record is empty |
+| `filter` | Optional | `Object` | `{}` | Used to filter referenced records |
+| `link` | Optional | `string | Function` | `edit` | Target of the link wrapping the rendered child. Set to `false` to disable the link. |
+| `queryOptions` | Optional | [`UseQueryOptions`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options |
+| `sort` | Optional | `{ field: String, order: 'ASC' or 'DESC' }` | `{ field: 'id', order: 'ASC' }` | Used to order referenced records |
+
+`` also accepts the [common field props](./Fields.md#common-field-props).
+
+## `children`
+
+You can pass any component of your own as children, to render the referenced record as you wish.
+You can access the list context using the `useReferenceOneFieldController` hook.
+
+```jsx
+const MyBookView = () => {
+ const context = useReferenceOneFieldContext({
+ reference,
+ target,
+ });
+
+ if (context.isPending) {
+ return Loading...
;
+ }
+
+ if (context.error) {
+ return {context.error.toString()}
;
+ }
+ return (
+
+
{record ? record.genre : ''}
+
{record ? record.ISBN : ''}
+
+ );
+}
+
+const BookShow = () => (
+
+
+
+);
+```
+
+## `render`
+
+Alternatively to children you can pass a render prop to ``. The render prop will receive the reference on field context as its argument, allowing to inline the render logic.
+When receiving a render prop the `` component will ignore the children property.
+
+```jsx
+const BookShow = () => (
+ {
+ if (context.isPending) {
+ return Loading...
;
+ }
+
+ if (context.error) {
+ return {context.error.toString()}
;
+ }
+ return (
+
+
{record ? record.genre : ''}
+
{record ? record.ISBN : ''}
+
+ );
+ }}
+ />
+);
+```
+
+## `empty`
+
+Use `empty` to customize the text displayed when the related record is empty.
+
+```jsx
+
+ ( )
+
+```
+
+`empty` also accepts a translation key.
+
+```jsx
+
+ ( )
+
+```
+
+`empty` also accepts a `ReactNode`.
+
+```jsx
+ }
+>
+ ( )
+
+```
+
+## `filter`
+
+You can also use `` in a one-to-many relationship. In that case, the first record will be displayed. The `filter` prop becomes super useful in that case, as it allows you to select the appropriate record to display.
+
+For instance, if a product has prices in many currencies, and you only want to render the price in euros, you can use:
+
+{% raw %}
+```jsx
+
+
+
+```
+{% endraw %}
+
+## `link`
+
+By default, `` will set pass a links to the edition page of the related record in the context.link. You can disable this behavior by setting the `link` prop to `false`.
+
+```jsx
+
+
+
+```
+
+You can also set the `link` prop to a string, which will be used as the link type. It can be either `edit`, `show`, a route path, or a function returning a route path based on the given record.
+
+{% raw %}
+```jsx
+ `/custom/${record.id}`}
+>
+
+
+```
+{% endraw %}
+
+## `queryOptions`
+
+`` uses `react-query` to fetch the related record. You can set [any of `useQuery` options](https://tanstack.com/query/v5/docs/react/reference/useQuery) via the `queryOptions` prop.
+
+For instance, if you want to disable the refetch on window focus for this query, you can use:
+
+{% raw %}
+```jsx
+
+
+
+```
+{% endraw %}
+
+## `reference`
+
+The name of the resource to fetch for the related records.
+
+For instance, if you want to display the details of a given book, the `reference` name should be `book_details`:
+
+```jsx
+
+
+
+```
+
+## `sort`
+
+You can also use `` in a one-to-many relationship. In that case, the first record will be displayed. This is where the `sort` prop comes in handy. It allows you to select the appropriate record to display.
+
+
+
+For instance, if you want to display the latest message in a discussion, you can use:
+
+{% raw %}
+```jsx
+
+
+
+```
+{% endraw %}
+
+## `target`
+
+The name of the field carrying the relationship on the referenced resource.
+
+For example, in the following schema, the relationship is carried by the `book_id` field:
+
+```
+┌──────────────┐ ┌──────────────┐
+│ books │ │ book_details │
+│--------------│ │--------------│
+│ id │───┐ │ id │
+│ title │ └──╼│ book_id │
+│ published_at │ │ genre │
+└──────────────┘ │ ISBN │
+ └──────────────┘
+```
+
+In that case, the `target` prop should be set to `book_id`:
+
+```jsx
+
+
+
+```
+
diff --git a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx
index 6831b37d555..b406814df19 100644
--- a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx
@@ -7,7 +7,7 @@ import {
ResourceContextProvider,
TestMemoryRouter,
useRecordContext,
- useReferenceOneFieldController,
+ useReferenceFieldContext,
} from '../..';
export default { title: 'ra-ui-materialui/fields/ReferenceOneFieldBase' };
@@ -37,9 +37,7 @@ const Wrapper = ({ children, dataProvider = defaultDataProvider }) => (
export const Basic = () => (
-
-
-
+
);
@@ -51,9 +49,7 @@ const dataProviderWithLoading = {
export const Loading = () => (
-
-
-
+
);
@@ -89,19 +85,8 @@ export const WithRenderProp = ({
);
};
-const MyReferenceOneField = ({
- reference,
- target,
- children,
-}: {
- children: React.ReactNode;
- reference: string;
- target: string;
-}) => {
- const context = useReferenceOneFieldController({
- reference,
- target,
- });
+const MyReferenceOneField = () => {
+ const context = useReferenceFieldContext();
if (context.isPending) {
return Loading...
;
@@ -110,10 +95,10 @@ const MyReferenceOneField = ({
if (context.error) {
return {context.error.toString()}
;
}
- return children;
-};
-const TextField = ({ source }) => {
- const record = useRecordContext();
- return {record ? record[source] : ''} ;
+ return (
+
+ {context.referenceRecord ? context.referenceRecord.ISBN : ''}
+
+ );
};
From 59bc05e367a32f1636ae5f7e2fa84560fd6e133d Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 15 Jul 2025 10:11:55 +0200
Subject: [PATCH 20/41] stop referring mui component in ReferenceFieldBase doc,
move section about render under props
---
docs/ReferenceFieldBase.md | 72 ++++++++++++-------
.../field/ReferenceManyFieldBase.stories.tsx | 2 +-
.../field/ReferenceOneFieldBase.stories.tsx | 1 -
3 files changed, 48 insertions(+), 27 deletions(-)
diff --git a/docs/ReferenceFieldBase.md b/docs/ReferenceFieldBase.md
index a835b48c2f4..bae8666aad7 100644
--- a/docs/ReferenceFieldBase.md
+++ b/docs/ReferenceFieldBase.md
@@ -11,8 +11,6 @@ storybook_path: ra-core-controller-field-referencefieldbase--basic
## Usage
-### With children
-
For instance, let's consider a model where a `post` has one author from the `users` resource, referenced by a `user_id` field.
```
@@ -32,16 +30,9 @@ In that case, use `` to display the post author's as follows
import { Show, SimpleShowLayout, ReferenceField, TextField, DateField } from 'react-admin';
export const PostShow = () => (
-
-
-
-
-
-
-
-
-
-
+
+
+
);
```
@@ -76,7 +67,50 @@ export const MyReferenceField = () => (
It uses `dataProvider.getMany()` instead of `dataProvider.getOne()` [for performance reasons](#performance). When using several `` in the same page (e.g. in a ``), this allows to call the `dataProvider` once instead of once per row.
-### With render prop
+
+## Props
+
+| Prop | Required | Type | Default | Description |
+| ----------- | -------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
+| `source` | Required | `string` | - | Name of the property to display |
+| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'posts' |
+| `children` | Optional | `ReactNode` | - | React component to render the referenced record, the component need to use useReferenceFieldContext to access the context. |
+| `render` | Optional | `(context) => ReactNode` | - | Function that takes the referenceFieldContext and render the referenced record. Will take priority on children props if both are set. |
+| `empty` | Optional | `ReactNode` | - | What to render when the field has no value or when the reference is missing |
+| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options |
+| `sortBy` | Optional | `string | Function` | `source` | Name of the field to use for sorting when used in a Datagrid |
+
+## `children`
+
+You can pass any component of your own as child, to render the related records as you wish.
+You can access the list context using the `useReferenceFieldContext` hook.
+
+```tsx
+import { Show, SimpleShowLayout, ReferenceField, TextField, DateField } from 'react-admin';
+
+export const MyReferenceFieldView = () => {
+ const context = useReferenceFieldContext();
+
+ const value = useFieldValue({ source });
+ if (context.isPending) {
+ return Loading...
;
+ }
+
+ if (context.error) {
+ return {context.error.toString()}
;
+ }
+
+ return {value}
;
+};
+
+export const MyReferenceField = () => (
+
+
+
+);
+```
+
+## `render`
Alternatively you can pass a render prop instead of children to be able to inline the rendering. The render function will then receive the reference field context directly.
@@ -99,18 +133,6 @@ export const MyReferenceField = () => (
);
```
-## Props
-
-| Prop | Required | Type | Default | Description |
-| ----------- | -------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
-| `source` | Required | `string` | - | Name of the property to display |
-| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'posts' |
-| `children` | Optional | `ReactNode` | - | React component to render the referenced record, the component need to use useReferenceFieldContext to access the context. |
-| `render` | Optional | `(context) => ReactNode` | - | Function that takes the referenceFieldContext and render the referenced record. Will take priority on children props if both are set. |
-| `empty` | Optional | `ReactNode` | - | What to render when the field has no value or when the reference is missing |
-| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options |
-| `sortBy` | Optional | `string | Function` | `source` | Name of the field to use for sorting when used in a Datagrid |
-
## `empty`
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
index 8c332737830..e7ebb8e4155 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
@@ -5,7 +5,7 @@ import { Resource } from '../../core/Resource';
import { ShowBase } from '../../controller/show/ShowBase';
import { TestMemoryRouter } from '../../routing';
import { ReferenceManyFieldBase } from './ReferenceManyFieldBase';
-import { useListContext, useListContextWithProps } from '../list';
+import { useListContext } from '../list';
export default {
title: 'ra-core/controller/field/ReferenceManyFieldBase',
diff --git a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx
index b406814df19..392597592df 100644
--- a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx
@@ -6,7 +6,6 @@ import {
ReferenceOneFieldBase,
ResourceContextProvider,
TestMemoryRouter,
- useRecordContext,
useReferenceFieldContext,
} from '../..';
From d7d1d0bcae421e5cd0230397d02f89140b901f32 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 15 Jul 2025 10:47:17 +0200
Subject: [PATCH 21/41] fix type
---
packages/ra-ui-materialui/src/list/InfiniteList.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/ra-ui-materialui/src/list/InfiniteList.tsx b/packages/ra-ui-materialui/src/list/InfiniteList.tsx
index e27ea5d683b..20beac31d58 100644
--- a/packages/ra-ui-materialui/src/list/InfiniteList.tsx
+++ b/packages/ra-ui-materialui/src/list/InfiniteList.tsx
@@ -108,7 +108,7 @@ const defaultFilter = {};
const defaultLoading = ;
export interface InfiniteListProps
- extends InfiniteListBaseProps,
+ extends Omit, 'children'>,
ListViewProps {}
const PREFIX = 'RaInfiniteList';
From 8b256f5f1121fe7122546954b7b25d29a9750dda Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 15 Jul 2025 11:23:22 +0200
Subject: [PATCH 22/41] rename ra-core FieldProps to FieldPropsBase
---
.../ra-core/src/controller/field/ReferenceArrayFieldBase.tsx | 4 ++--
packages/ra-core/src/controller/field/types.ts | 2 +-
packages/ra-ui-materialui/src/field/types.ts | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
index 1023e354741..b2a5ef23126 100644
--- a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
@@ -7,7 +7,7 @@ import { useRecordContext } from '../record';
import { useReferenceArrayFieldController } from './useReferenceArrayFieldController';
import { ResourceContextProvider } from '../../core';
import { ListContextProvider, ListControllerResult } from '../list';
-import { FieldProps } from './types';
+import { FieldPropsBase } from './types';
/**
* A container component that fetches records from another resource specified
@@ -119,7 +119,7 @@ export const ReferenceArrayFieldBase = <
export interface ReferenceArrayFieldBaseProps<
RecordType extends RaRecord = RaRecord,
ReferenceRecordType extends RaRecord = RaRecord,
-> extends FieldProps {
+> extends FieldPropsBase {
children?: ReactNode;
render?: (props: ListControllerResult) => ReactElement;
filter?: FilterPayload;
diff --git a/packages/ra-core/src/controller/field/types.ts b/packages/ra-core/src/controller/field/types.ts
index e8509636a50..0c42cc18efe 100644
--- a/packages/ra-core/src/controller/field/types.ts
+++ b/packages/ra-core/src/controller/field/types.ts
@@ -3,7 +3,7 @@ import { ExtractRecordPaths, HintedString } from '../../types';
type SortOrder = 'ASC' | 'DESC';
-export interface FieldProps<
+export interface FieldPropsBase<
RecordType extends Record = Record,
> {
/**
diff --git a/packages/ra-ui-materialui/src/field/types.ts b/packages/ra-ui-materialui/src/field/types.ts
index 9d023b3ce25..0031689ff95 100644
--- a/packages/ra-ui-materialui/src/field/types.ts
+++ b/packages/ra-ui-materialui/src/field/types.ts
@@ -1,11 +1,11 @@
import { TableCellProps } from '@mui/material/TableCell';
-import { FieldProps as FieldPropsCore } from 'ra-core';
+import { FieldPropsBase } from 'ra-core';
type TextAlign = TableCellProps['align'];
export interface FieldProps<
RecordType extends Record = Record,
-> extends FieldPropsCore {
+> extends FieldPropsBase {
/**
* A class name to apply to the root div element
*/
From 68637c04e22025a6b9b3df80734a25eab4b4bdc0 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 15 Jul 2025 12:12:04 +0200
Subject: [PATCH 23/41] fix render type in doc
---
docs/ReferenceArrayFieldBase.md | 2 +-
docs/ReferenceOneFieldBase.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/ReferenceArrayFieldBase.md b/docs/ReferenceArrayFieldBase.md
index 06485316e11..cb251367be9 100644
--- a/docs/ReferenceArrayFieldBase.md
+++ b/docs/ReferenceArrayFieldBase.md
@@ -96,7 +96,7 @@ You can change how the list of related records is rendered by passing a custom c
| `source` | Required | `string` | - | Name of the property to display |
| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'tags' |
| `children` | Required if no render | `Element` | | One or several elements that render a list of records based on a `ListContext` |
-| `render` | Required if no children | `Element` | | A function that takes a list context and render a list of records |
+| `render` | Required if no children | `(listContext) => Element` | | A function that takes a list context and render a list of records |
| `filter` | Optional | `Object` | - | Filters to use when fetching the related records (the filtering is done client-side) |
| `pagination` | Optional | `Element` | - | Pagination element to display pagination controls. empty by default (no pagination) |
| `perPage` | Optional | `number` | 1000 | Maximum number of results to display |
diff --git a/docs/ReferenceOneFieldBase.md b/docs/ReferenceOneFieldBase.md
index c032038fe71..a753bb71882 100644
--- a/docs/ReferenceOneFieldBase.md
+++ b/docs/ReferenceOneFieldBase.md
@@ -76,7 +76,7 @@ const MyBookView = ({ source }) => {
| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'book_details' |
| `target` | Required | string | - | Target field carrying the relationship on the referenced resource, e.g. 'book_id' |
| `children` | Required if no render | `Element` | - | React component to render the referenced record, the component need to use useReferenceOneFieldContext to access the context. |
-| `render` | Required if no children | `Element` | - | A function that takes the reference field context and return a React element |
+| `render` | Required if no children | `(referenceFieldContext) => Element` | - | A function that takes the reference field context and return a React element |
| `empty` | Optional | `ReactNode` | - | The text or element to display when the referenced record is empty |
| `filter` | Optional | `Object` | `{}` | Used to filter referenced records |
| `link` | Optional | `string | Function` | `edit` | Target of the link wrapping the rendered child. Set to `false` to disable the link. |
From 60eaf3748212807c0ff1386d14d40c3f6aa84469 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Thu, 17 Jul 2025 10:29:40 +0200
Subject: [PATCH 24/41] fix ra-core/controller/field/ReferenceOneFieldBase
story name
---
.../src/controller/field/ReferenceOneFieldBase.stories.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx
index 392597592df..f7fab891a07 100644
--- a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx
@@ -9,7 +9,7 @@ import {
useReferenceFieldContext,
} from '../..';
-export default { title: 'ra-ui-materialui/fields/ReferenceOneFieldBase' };
+export default { title: 'ra-core/controller/field/ReferenceOneFieldBase' };
const defaultDataProvider = {
getManyReference: () =>
From 8c08decd611c65051c603340abc08e171299e9c0 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Thu, 17 Jul 2025 11:40:48 +0200
Subject: [PATCH 25/41] code review
---
docs/CreateBase.md | 2 +-
docs/EditBase.md | 2 +-
docs/ListBase.md | 4 +-
docs/ReferenceArrayFieldBase.md | 55 ++++++++---------
docs/ReferenceFieldBase.md | 60 +++++++++----------
docs/ReferenceManyFieldBase.md | 57 ++++++------------
docs/ReferenceOneFieldBase.md | 49 +++++++--------
docs/ShowBase.md | 27 +++++++++
.../field/ReferenceArrayFieldBase.tsx | 4 +-
.../ra-core/src/controller/field/types.ts | 2 +-
packages/ra-ui-materialui/src/field/types.ts | 4 +-
11 files changed, 133 insertions(+), 133 deletions(-)
diff --git a/docs/CreateBase.md b/docs/CreateBase.md
index 99060b0c9c3..c7a4f393237 100644
--- a/docs/CreateBase.md
+++ b/docs/CreateBase.md
@@ -46,7 +46,7 @@ export const BookCreate = () => (
You can customize the `` component using the following props, documented in the `` component:
* `children`: the components that renders the form
-* `render`: alternative to children, a function that takes the CreateController context and renders the form
+* `render`: alternative to children, a function that takes the `CreateController` context and renders the form
* [`disableAuthentication`](./Create.md#disableauthentication): disable the authentication check
* [`mutationMode`](./Create.md#mutationmode): Switch to optimistic or undoable mutations (pessimistic by default)
* [`mutationOptions`](./Create.md#mutationoptions): options for the `dataProvider.create()` call
diff --git a/docs/EditBase.md b/docs/EditBase.md
index ae58a4e2590..a5671f6f1f1 100644
--- a/docs/EditBase.md
+++ b/docs/EditBase.md
@@ -47,7 +47,7 @@ export const BookEdit = () => (
You can customize the `` component using the following props, documented in the `` component:
* `children`: the components that renders the form
-* `render`: alternative to children, a function that takes the EditController context and renders the form
+* `render`: alternative to children, a function that takes the `EditController` context and renders the form
* [`disableAuthentication`](./Edit.md#disableauthentication): disable the authentication check
* [`id`](./Edit.md#id): the id of the record to edit
* [`mutationMode`](./Edit.md#mutationmode): switch to optimistic or pessimistic mutations (undoable by default)
diff --git a/docs/ListBase.md b/docs/ListBase.md
index 96d7cd3319e..2dfd4607a7b 100644
--- a/docs/ListBase.md
+++ b/docs/ListBase.md
@@ -66,10 +66,8 @@ The `` component accepts the same props as [`useListController`](./use
* [`resource`](./List.md#resource)
* [`sort`](./List.md#sort)
-These are a subset of the props accepted by `` - only the props that change data fetching, and not the props related to the user interface.
-
In addition, `` renders its children components inside a `ListContext`. Check [the `` documentation](./List.md#children) for usage examples.
-Alternatively you can pass a render function to the props, in place of children. This function will receive the listContext as argument.
+Alternatively, you can pass a `render` function prop instead of `children`. This function will receive the `ListContext` as argument.
## Security
diff --git a/docs/ReferenceArrayFieldBase.md b/docs/ReferenceArrayFieldBase.md
index cb251367be9..e4dc6f70cb3 100644
--- a/docs/ReferenceArrayFieldBase.md
+++ b/docs/ReferenceArrayFieldBase.md
@@ -1,7 +1,7 @@
---
layout: default
title: "The ReferenceArrayFieldBase Component"
-storybook_path: ra-ui-materialui-fields-referencearrayfieldbase--basic
+storybook_path: ra-core-fields-referencearrayfieldbase--basic
---
# ``
@@ -46,7 +46,7 @@ In that case, use `` to display the post tag names as C
```jsx
import { List, DataTable, ReferenceArrayFieldBase } from 'react-admin';
-const MyPostView = (props: { children: React.ReactNode }) => {
+const MyTagsView = (props: { children: React.ReactNode }) => {
const context = useListContext();
if (context.isPending) {
@@ -72,7 +72,7 @@ export const PostList = () => (
-
+
@@ -87,7 +87,7 @@ export const PostList = () => (
`` fetches the `tag` resources related to each `post` resource by matching `post.tag_ids` to `tag.id`.
-You can change how the list of related records is rendered by passing a custom child reading the `ListContext` (e.g. a [``](./DataTable.md)). See the [`children`](#children) section for details.
+You can change how the list of related records is rendered by passing a custom child reading the `ListContext` (e.g. a [``](./DataTable.md)) or a render function prop. See the [`children`](#children) and the [`render`](#render) sections for details.
## Props
@@ -95,8 +95,8 @@ You can change how the list of related records is rendered by passing a custom c
| -------------- | -------- | --------------------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `source` | Required | `string` | - | Name of the property to display |
| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'tags' |
-| `children` | Required if no render | `Element` | | One or several elements that render a list of records based on a `ListContext` |
-| `render` | Required if no children | `(listContext) => Element` | | A function that takes a list context and render a list of records |
+| `children` | Optional\* | `Element` | | One or several elements that render a list of records based on a `ListContext` |
+| `render` | Optional\* | `(ListContext) => Element` | | A function that takes a list context and renders a list of records |
| `filter` | Optional | `Object` | - | Filters to use when fetching the related records (the filtering is done client-side) |
| `pagination` | Optional | `Element` | - | Pagination element to display pagination controls. empty by default (no pagination) |
| `perPage` | Optional | `number` | 1000 | Maximum number of results to display |
@@ -104,32 +104,33 @@ You can change how the list of related records is rendered by passing a custom c
| `sort` | Optional | `{ field, order }` | `{ field: 'id', order: 'DESC' }` | Sort order to use when displaying the related records (the sort is done client-side) |
| `sortBy` | Optional | `string | Function` | `source` | When used in a `List`, name of the field to use for sorting when the user clicks on the column header. |
+\* Either one of children or render is required.
+
## `children`
You can pass any component of your own as child, to render the list of related records as you wish.
You can access the list context using the `useListContext` hook.
-
```jsx
-
+
-// With MyPostView like:
-const MyPostView = (props: { children: React.ReactNode }) => {
- const context = useListContext();
+// With MyTagList like:
+const MyTagList = (props: { children: React.ReactNode }) => {
+ const { isPending, error, data } = useListContext();
- if (context.isPending) {
+ if (isPending) {
return Loading...
;
}
- if (context.error) {
- return {context.error.toString()}
;
+ if (error) {
+ return {error.toString()}
;
}
return (
- {listContext.data?.map((tag, index) => (
+ {data?.map((tag, index) => (
{tag.name}
))}
@@ -139,27 +140,24 @@ const MyPostView = (props: { children: React.ReactNode }) => {
## `render`
-Alternatively to children you can pass a render prop to ``. The render prop will receive the list context as its argument, allowing to inline the render logic for both the list and the pagination.
-When receiving a render prop the `` component will ignore the children and the pagination property.
-
+Alternatively to children you can pass a `render` function prop to ``. The `render` prop will receive the `ListContext` as its argument, allowing to inline the `render` logic for both the list and the pagination.
+When receiving a `render` prop the `` component will ignore the children property.
```jsx
- {
-
- if (context.isPending) {
+ render={({ isPending, error, data }) => {
+ if (isPending) {
return Loading...
;
}
-
- if (context.error) {
- return {context.error.toString()}
;
+ if (error) {
+ return {error.toString()}
;
}
return (
- {listContext.data?.map((tag, index) => (
+ {data.map((tag, index) => (
{tag.name}
))}
@@ -176,7 +174,7 @@ For instance, to render only tags that are 'published', you can use the followin
{% raw %}
```jsx
-`
`` is useful for displaying many-to-one and one-to-one relationships, e.g. the details of a user when rendering a post authored by that user.
-`` is a headless component, handling only the logic. This Allows to use any UI library for the render. For a version incorporating UI see [``](/ReferenceField.html)
+`` is a headless component, handling only the logic. This allows to use any UI library for the render. For a version based on MUI see [``](/ReferenceField.html)
## Usage
@@ -29,7 +29,7 @@ In that case, use `` to display the post author's as follows
```jsx
import { Show, SimpleShowLayout, ReferenceField, TextField, DateField } from 'react-admin';
-export const PostShow = () => (
+export const UserList = () => (
@@ -38,12 +38,12 @@ export const PostShow = () => (
`` fetches the data, puts it in a [`RecordContext`](./useRecordContext.md), and its up to its children to handle the rendering by accessing the ReferencingContext using the useReferenceFieldContext hook.
-This component fetches a referenced record (`users` in this example) using the `dataProvider.getMany()` method, and passes it to the ReferenceFieldContext.
+This component fetches a referenced record (`users` in this example) using the `dataProvider.getMany()` method, and passes it to the `ReferenceFieldContext`.
```tsx
import { Show, SimpleShowLayout, ReferenceField, TextField, DateField } from 'react-admin';
-export const MyReferenceFieldView = () => {
+export const UserView = () => {
const context = useReferenceFieldContext();
const value = useFieldValue({ source });
@@ -60,22 +60,21 @@ export const MyReferenceFieldView = () => {
export const MyReferenceField = () => (
-
+
);
```
It uses `dataProvider.getMany()` instead of `dataProvider.getOne()` [for performance reasons](#performance). When using several `` in the same page (e.g. in a ``), this allows to call the `dataProvider` once instead of once per row.
-
## Props
| Prop | Required | Type | Default | Description |
| ----------- | -------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
| `source` | Required | `string` | - | Name of the property to display |
| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'posts' |
-| `children` | Optional | `ReactNode` | - | React component to render the referenced record, the component need to use useReferenceFieldContext to access the context. |
-| `render` | Optional | `(context) => ReactNode` | - | Function that takes the referenceFieldContext and render the referenced record. Will take priority on children props if both are set. |
+| `children` | Optional | `ReactNode` | - | React component to render the referenced record. |
+| `render` | Optional | `(context) => ReactNode` | - | Function that takes the referenceFieldContext and render the referenced record. |
| `empty` | Optional | `ReactNode` | - | What to render when the field has no value or when the reference is missing |
| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options |
| `sortBy` | Optional | `string | Function` | `source` | Name of the field to use for sorting when used in a Datagrid |
@@ -88,8 +87,8 @@ You can access the list context using the `useReferenceFieldContext` hook.
```tsx
import { Show, SimpleShowLayout, ReferenceField, TextField, DateField } from 'react-admin';
-export const MyReferenceFieldView = () => {
- const context = useReferenceFieldContext();
+export const UserView = () => {
+ const { isPending, error } = useReferenceFieldContext();
const value = useFieldValue({ source });
if (context.isPending) {
@@ -105,34 +104,39 @@ export const MyReferenceFieldView = () => {
export const MyReferenceField = () => (
-
+
);
```
## `render`
-Alternatively you can pass a render prop instead of children to be able to inline the rendering. The render function will then receive the reference field context directly.
+Alternatively, you can pass a `render` function prop instead of children. This function will receive the `ReferenceFieldContext` as argument.
```jsx
export const MyReferenceField = () => (
- {
- if (isPending) {
- return Loading...
;
- }
-
- if (error) {
- return (
-
- {error.message}
-
- );
- }
- return {value}
;
- }} />
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return (
+
+ {error.message}
+
+ );
+ }
+ return {value}
;
+ }}
+ />
);
```
+The `render` function prop will take priority on `children` props if both are set.
## `empty`
@@ -154,7 +158,6 @@ You can pass either a React element or a string to the `empty` prop:
```
-
## `link`
To change the link from the `` page to the `` page, set the `link` prop to "show".
@@ -180,7 +183,6 @@ You can also use a custom `link` function to get a custom path for the children.
link={(record, reference) => `/my/path/to/${reference}/${record.id}`}
/>
```
-
## `queryOptions`
Use the `queryOptions` prop to pass options to [the `dataProvider.getMany()` query](./useGetOne.md#aggregating-getone-calls) that fetches the referenced record.
@@ -208,7 +210,6 @@ For instance, if the `posts` resource has a `user_id` field, set the `reference`
```jsx
```
-
## `sortBy`
By default, when used in a ``, and when the user clicks on the column header of a ``, react-admin sorts the list by the field `source`. To specify another field name to sort by, set the `sortBy` prop.
@@ -216,7 +217,6 @@ By default, when used in a ``, and when the user clicks on the column
```jsx
```
-
## Performance
diff --git a/docs/ReferenceManyFieldBase.md b/docs/ReferenceManyFieldBase.md
index 622c3140acc..abba259ee45 100644
--- a/docs/ReferenceManyFieldBase.md
+++ b/docs/ReferenceManyFieldBase.md
@@ -13,7 +13,6 @@ For a component handling the UI too use [the `` component](.
**Tip**: If the relationship is materialized by an array of ids in the initial record, use [the `` component](./ReferenceArrayFieldBase.md) instead.
-
## Usage
### With children
@@ -36,7 +35,7 @@ For instance, if an `author` has many `books`, and each book resource exposes an
```jsx
import { Show, SimpleShowLayout, ReferenceManyFieldBase, DataTable, TextField, DateField } from 'react-admin';
-const CustomAuthorView = ({
+const BookList = ({
source,
children,
}: {
@@ -53,8 +52,8 @@ const CustomAuthorView = ({
}
return (
- {listContext.data?.map((datum, index) => (
-
{datum[source]}
+ {listContext.data?.map((book, index) => (
+ {book[source]}
))}
);
@@ -66,7 +65,7 @@ const AuthorShow = () => (
-
+
@@ -102,9 +101,9 @@ export const PostList = () => (
| Prop | Required | Type | Default | Description |
| -------------- | -------- | --------------------------------------------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------- |
-| `children` | Required if no render | `Element` | - | One or several elements that render a list of records based on a `ListContext` |
-| `render` | Required if no children | `(listContext) => Element` | - | Function that receives a `ListContext` and render elements |
-| `debounce` | Optional | `number` | 500 | debounce time in ms for the `setFilters` callbacks |
+| `children` | Optional | `Element` | - | One or several elements that render a list of records based on a `ListContext` |
+| `render` | Optional\* | `(ListContext) => Element` | - | Function that receives a `ListContext` and render elements |
+| `debounce` | Optional\* | `number` | 500 | debounce time in ms for the `setFilters` callbacks |
| `empty` | Optional | `ReactNode` | - | Element to display when there are no related records. |
| `filter` | Optional | `Object` | - | Filters to use when fetching the related records, passed to `getManyReference()` |
| `pagination` | Optional | `Element` | - | Pagination element to display pagination controls. empty by default (no pagination) |
@@ -116,6 +115,8 @@ export const PostList = () => (
| `storeKey` | Optional | `string` | - | The key to use to store the records selection state |
| `target` | Required | `string` | - | Target field carrying the relationship on the referenced resource, e.g. 'user_id' |
+\* Either one of children or render is required.
+
## `children`
`` renders its children inside a [`ListContext`](./useListContext.md). This means you can use any component that uses a `ListContext`:
@@ -151,57 +152,33 @@ export const AuthorShow = () => (
## `render`
-Alternatively to children you can pass a render prop to ``. The render prop will receive the list context as its argument, allowing to inline the render logic for both the list and the pagination.
-When receiving a render prop the `` component will ignore the children and the pagination property.
+Alternatively, you can pass a `render` function prop instead of children. The `render` prop will receive the `ListContext` as arguments, allowing to inline the render logic for both the list and the pagination.
+When receiving a `render` function prop the `` component will ignore the children property.
```jsx
import { Show, SimpleShowLayout, ReferenceManyFieldBase, DataTable, TextField, DateField } from 'react-admin';
-const CustomAuthorView = ({
- source,
- children,
-}: {
- source: string;
-}) => {
- const context = useListController();
-
- if (context.isPending) {
- return Loading...
;
- }
-
- if (context.error) {
- return {context.error.toString()}
;
- }
- return (
-
- {listContext.data?.map((datum, index) => (
-
{datum[source]}
- ))}
-
- );
-};
-
const AuthorShow = () => (
- {
+ ({ isPending, error, data }) => {
- if (context.isPending) {
+ if (isPending) {
return Loading...
;
}
- if (context.error) {
- return {context.error.toString()}
;
+ if (error) {
+ return {error.toString()}
;
}
return (
- {listContext.data?.map((author, index) => (
+ {data.map((author, index) => (
{author.name}
))}
diff --git a/docs/ReferenceOneFieldBase.md b/docs/ReferenceOneFieldBase.md
index a753bb71882..8087d25ecbc 100644
--- a/docs/ReferenceOneFieldBase.md
+++ b/docs/ReferenceOneFieldBase.md
@@ -21,7 +21,7 @@ This field fetches a one-to-one relationship, e.g. the details of a book, when u
`` behaves like ``: 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.
-`` is a headless component, handling only the logic. This Allows to use any UI library for the render. For a version incorporating UI see [``](/ReferenceOneField.html)
+`` is a headless component, handling only the logic. This allows to use any UI library for the render. For a version based on MUI see [``](/ReferenceOneField.html)
For the inverse relationships (the book linked to a book_detail), you can use a [``](./ReferenceFieldBase.md).
@@ -38,14 +38,14 @@ const BookShow = () => (
-
+
);
-// with MyBookView something like
-const MyBookView = ({ source }) => {
+// with BookDetails something like
+const BookDetails = ({ source }) => {
const context = useReferenceOneFieldContext({
reference,
target,
@@ -75,8 +75,8 @@ const MyBookView = ({ source }) => {
| -------------- | -------- | ------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------- |
| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'book_details' |
| `target` | Required | string | - | Target field carrying the relationship on the referenced resource, e.g. 'book_id' |
-| `children` | Required if no render | `Element` | - | React component to render the referenced record, the component need to use useReferenceOneFieldContext to access the context. |
-| `render` | Required if no children | `(referenceFieldContext) => Element` | - | A function that takes the reference field context and return a React element |
+| `children` | Optional\* | `Element` | - | React component to render the referenced record. |
+| `render` | Optional\* | `(referenceFieldContext) => Element` | - | A function that takes the reference field context and return a React element |
| `empty` | Optional | `ReactNode` | - | The text or element to display when the referenced record is empty |
| `filter` | Optional | `Object` | `{}` | Used to filter referenced records |
| `link` | Optional | `string | Function` | `edit` | Target of the link wrapping the rendered child. Set to `false` to disable the link. |
@@ -85,62 +85,64 @@ const MyBookView = ({ source }) => {
`` also accepts the [common field props](./Fields.md#common-field-props).
+\* Either one of children or render is required.
+
## `children`
You can pass any component of your own as children, to render the referenced record as you wish.
You can access the list context using the `useReferenceOneFieldController` hook.
```jsx
-const MyBookView = () => {
- const context = useReferenceOneFieldContext({
+const BookDetails = () => {
+ const { isPending, error, referenceRecord } = useReferenceOneFieldContext({
reference,
target,
});
- if (context.isPending) {
+ if (isPending) {
return Loading...
;
}
- if (context.error) {
- return {context.error.toString()}
;
+ if (error) {
+ return {error.toString()}
;
}
return (
-
{record ? record.genre : ''}
-
{record ? record.ISBN : ''}
+
{referenceRecord ? referenceRecord.genre : ''}
+
{referenceRecord ? referenceRecord.ISBN : ''}
);
}
const BookShow = () => (
-
+
);
```
## `render`
-Alternatively to children you can pass a render prop to ``. The render prop will receive the reference on field context as its argument, allowing to inline the render logic.
-When receiving a render prop the `` component will ignore the children property.
+Alternatively to children you can pass a `render` function prop to ``. The `render` function prop will receive the `ReferenceFieldContext` as its argument, allowing to inline the render logic.
+When receiving a `render` function prop the `` component will ignore the children property.
```jsx
const BookShow = () => (
{
- if (context.isPending) {
+ render={({ isPending, error, referenceRecord }) => {
+ if (isPending) {
return Loading...
;
}
- if (context.error) {
- return {context.error.toString()}
;
+ if (error) {
+ return {error.toString()}
;
}
return (
-
{record ? record.genre : ''}
-
{record ? record.ISBN : ''}
+
{referenceRecord ? referenceRecord.genre : ''}
+
{referenceRecord ? referenceRecord.ISBN : ''}
);
}}
@@ -211,7 +213,7 @@ You can also set the `link` prop to a string, which will be used as the link typ
{% raw %}
```jsx
- `/custom/${record.id}`}
@@ -296,4 +298,3 @@ In that case, the `target` prop should be set to `book_id`:
```
-
diff --git a/docs/ShowBase.md b/docs/ShowBase.md
index b4a84678bec..7e68a762962 100644
--- a/docs/ShowBase.md
+++ b/docs/ShowBase.md
@@ -107,6 +107,33 @@ const BookShow = () => (
```
{% endraw %}
+## `render`
+
+Alternatively, you can pass a `render` function prop instead of children. This function will receive the `ShowContext` as argument.
+
+{% raw %}
+```jsx
+import { ShowBase, TextField, DateField, ReferenceField, WithRecord } from 'react-admin';
+
+const BookShow = () => (
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return (
+
+ {error.message}
+
+ );
+ }
+ return {record.title}
;
+ }}/>
+);
+```
+{% endraw %}
+
## `disableAuthentication`
By default, the `` component will automatically redirect the user to the login page if the user is not authenticated. If you want to disable this behavior and allow anonymous access to a show page, set the `disableAuthentication` prop to `true`.
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
index b2a5ef23126..6043a64666d 100644
--- a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
@@ -7,7 +7,7 @@ import { useRecordContext } from '../record';
import { useReferenceArrayFieldController } from './useReferenceArrayFieldController';
import { ResourceContextProvider } from '../../core';
import { ListContextProvider, ListControllerResult } from '../list';
-import { FieldPropsBase } from './types';
+import { BaseFieldProps } from './types';
/**
* A container component that fetches records from another resource specified
@@ -119,7 +119,7 @@ export const ReferenceArrayFieldBase = <
export interface ReferenceArrayFieldBaseProps<
RecordType extends RaRecord = RaRecord,
ReferenceRecordType extends RaRecord = RaRecord,
-> extends FieldPropsBase {
+> extends BaseFieldProps {
children?: ReactNode;
render?: (props: ListControllerResult) => ReactElement;
filter?: FilterPayload;
diff --git a/packages/ra-core/src/controller/field/types.ts b/packages/ra-core/src/controller/field/types.ts
index 0c42cc18efe..f66fd2b2b4c 100644
--- a/packages/ra-core/src/controller/field/types.ts
+++ b/packages/ra-core/src/controller/field/types.ts
@@ -3,7 +3,7 @@ import { ExtractRecordPaths, HintedString } from '../../types';
type SortOrder = 'ASC' | 'DESC';
-export interface FieldPropsBase<
+export interface BaseFieldProps<
RecordType extends Record = Record,
> {
/**
diff --git a/packages/ra-ui-materialui/src/field/types.ts b/packages/ra-ui-materialui/src/field/types.ts
index 0031689ff95..efcbc5b2019 100644
--- a/packages/ra-ui-materialui/src/field/types.ts
+++ b/packages/ra-ui-materialui/src/field/types.ts
@@ -1,11 +1,11 @@
import { TableCellProps } from '@mui/material/TableCell';
-import { FieldPropsBase } from 'ra-core';
+import { BaseFieldProps } from 'ra-core';
type TextAlign = TableCellProps['align'];
export interface FieldProps<
RecordType extends Record = Record,
-> extends FieldPropsBase {
+> extends BaseFieldProps {
/**
* A class name to apply to the root div element
*/
From 8c95570953555c0d7daa8ae9982da2b119547488 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Thu, 17 Jul 2025 14:39:03 +0200
Subject: [PATCH 26/41] remove pagination prop from ReferenceArrayFieldBase and
ReferenceManyFieldBase
---
docs/ReferenceArrayFieldBase.md | 25 +-------
docs/ReferenceManyFieldBase.md | 17 +----
.../field/ReferenceArrayFieldBase.spec.tsx | 27 --------
.../field/ReferenceArrayFieldBase.stories.tsx | 51 ---------------
.../field/ReferenceArrayFieldBase.tsx | 3 -
.../field/ReferenceManyFieldBase.spec.tsx | 27 --------
.../field/ReferenceManyFieldBase.stories.tsx | 63 -------------------
.../field/ReferenceManyFieldBase.tsx | 3 -
8 files changed, 2 insertions(+), 214 deletions(-)
diff --git a/docs/ReferenceArrayFieldBase.md b/docs/ReferenceArrayFieldBase.md
index e4dc6f70cb3..44d0ea70d6b 100644
--- a/docs/ReferenceArrayFieldBase.md
+++ b/docs/ReferenceArrayFieldBase.md
@@ -98,7 +98,6 @@ You can change how the list of related records is rendered by passing a custom c
| `children` | Optional\* | `Element` | | One or several elements that render a list of records based on a `ListContext` |
| `render` | Optional\* | `(ListContext) => Element` | | A function that takes a list context and renders a list of records |
| `filter` | Optional | `Object` | - | Filters to use when fetching the related records (the filtering is done client-side) |
-| `pagination` | Optional | `Element` | - | Pagination element to display pagination controls. empty by default (no pagination) |
| `perPage` | Optional | `number` | 1000 | Maximum number of results to display |
| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` options for the `getMany` query |
| `sort` | Optional | `{ field, order }` | `{ field: 'id', order: 'DESC' }` | Sort order to use when displaying the related records (the sort is done client-side) |
@@ -140,7 +139,7 @@ const MyTagList = (props: { children: React.ReactNode }) => {
## `render`
-Alternatively to children you can pass a `render` function prop to ``. The `render` prop will receive the `ListContext` as its argument, allowing to inline the `render` logic for both the list and the pagination.
+Alternatively to children you can pass a `render` function prop to ``. The `render` prop will receive the `ListContext` as its argument, allowing to inline the `render` logic.
When receiving a `render` prop the `` component will ignore the children property.
```jsx
@@ -183,26 +182,6 @@ For instance, to render only tags that are 'published', you can use the followin
```
{% endraw %}
-## `pagination`
-
-`` fetches *all* the related fields, and puts them all in a `ListContext`. If a record has a large number of related records, you can limit the number of displayed records with the [`perPage`](#perpage) prop. Then, let users display remaining records by rendering pagination controls. For that purpose, pass a pagination element to the `pagination` prop.
-
-For instance, to limit the display of related records to 10, you can use the following code:
-
-```jsx
-import { Pagination, ReferenceArrayFieldBase } from 'react-admin';
-
- }
-/>
-```
-
-***Note:*** The pagination prop will be ignored when the component receive a render prop
-
## `perPage`
`` fetches *all* the related fields, and puts them all in a `ListContext`. If a record has a large number of related records, it may be a good idea to limit the number of displayed records. The `perPage` prop allows to create a client-side pagination for the related records.
@@ -213,8 +192,6 @@ For instance, to limit the display of related records to 10, you can use the fol
```
-If you want to let the user display the remaining records, you have to pass a [`pagination`](#pagination) element.
-
## `queryOptions`
Use the `queryOptions` prop to pass options to [the `dataProvider.getMany()` query](./useGetOne.md#aggregating-getone-calls) that fetches the referenced record.
diff --git a/docs/ReferenceManyFieldBase.md b/docs/ReferenceManyFieldBase.md
index abba259ee45..ebeaa731dd3 100644
--- a/docs/ReferenceManyFieldBase.md
+++ b/docs/ReferenceManyFieldBase.md
@@ -106,7 +106,6 @@ export const PostList = () => (
| `debounce` | Optional\* | `number` | 500 | debounce time in ms for the `setFilters` callbacks |
| `empty` | Optional | `ReactNode` | - | Element to display when there are no related records. |
| `filter` | Optional | `Object` | - | Filters to use when fetching the related records, passed to `getManyReference()` |
-| `pagination` | Optional | `Element` | - | Pagination element to display pagination controls. empty by default (no pagination) |
| `perPage` | Optional | `number` | 25 | Maximum number of referenced records to fetch |
| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v3/docs/react/reference/useQuery) | `{}` | `react-query` options for the `getMany` query |
| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'books' |
@@ -152,7 +151,7 @@ export const AuthorShow = () => (
## `render`
-Alternatively, you can pass a `render` function prop instead of children. The `render` prop will receive the `ListContext` as arguments, allowing to inline the render logic for both the list and the pagination.
+Alternatively, you can pass a `render` function prop instead of children. The `render` prop will receive the `ListContext` as arguments, allowing to inline the render logic.
When receiving a `render` function prop the `` component will ignore the children property.
```jsx
@@ -293,20 +292,6 @@ const AuthorEdit = () => (
{% endraw %}
-## `pagination`
-
-If you want to allow users to paginate the list, pass a `` element as the `pagination` prop:
-
-```jsx
-import { Pagination } from 'react-admin';
-
- } reference="comments" target="post_id">
- ...
-
-```
-
-***Note:*** The pagination prop will be ignored if a render prop is specified
-
## `perPage`
By default, react-admin restricts the possible values to 25 and displays no pagination control. You can change the limit by setting the `perPage` prop:
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.spec.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.spec.tsx
index d9fbbe24225..1b1ac1f65dc 100644
--- a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.spec.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.spec.tsx
@@ -4,7 +4,6 @@ import {
Basic,
Errored,
Loading,
- WithPagination,
WithRenderProp,
} from './ReferenceArrayFieldBase.stories';
@@ -71,24 +70,6 @@ describe('ReferenceArrayFieldBase', () => {
});
});
- it('should render pagination', async () => {
- render( );
- await waitFor(() => {
- expect(screen.queryByText('1 - 3 of 8')).not.toBeNull();
- expect(screen.queryByText('Next Page')).not.toBeNull();
- expect(screen.queryByText('Previous Page')).not.toBeNull();
- });
- screen.getByText('Next Page').click();
- await waitFor(() => {
- expect(screen.queryByText('4 - 6 of 8')).not.toBeNull();
- });
- screen.getByText('Previous Page').click();
-
- await waitFor(() => {
- expect(screen.queryByText('1 - 3 of 8')).not.toBeNull();
- });
- });
-
it('should support renderProp', async () => {
render( );
await waitFor(() => {
@@ -102,12 +83,4 @@ describe('ReferenceArrayFieldBase', () => {
expect(screen.queryByText('Charlie Watts')).not.toBeNull();
});
});
-
- it('should not render pagination when given a render prop', async () => {
- render(Custom Pagination} />);
- await waitFor(() => {
- expect(screen.queryByText('John Lennon')).not.toBeNull();
- });
- expect(screen.queryByText('Custom Pagination')).toBeNull();
- });
});
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx
index a3cc87c554e..debfa90798a 100644
--- a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx
@@ -67,31 +67,6 @@ export const Basic = ({
);
-export const WithPagination = () => (
-
-
-
-
- }
- perPage={3}
- >
-
-
-
-
-
- }
- />
-
-
-);
-
const erroredDataProvider = {
...defaultDataProvider,
getMany: _resource => Promise.reject(new Error('Error')),
@@ -110,10 +85,8 @@ export const Loading = () => (
export const WithRenderProp = ({
dataProvider = defaultDataProvider,
- pagination,
}: {
dataProvider?: DataProvider;
- pagination?: React.ReactElement;
}) => (
{
if (isPending) {
return Loading...
;
@@ -189,26 +161,3 @@ const List = ({ source }: { source: string }) => {
);
};
-
-const Pagination = () => {
- const { page = 1, setPage, total = 0, perPage = 0 } = useListContext();
- const nextPage = () => {
- setPage?.(page + 1);
- };
- const previousPage = () => {
- setPage?.(page - 1);
- };
- return (
-
-
- Previous Page
-
-
- {`${(page - 1) * perPage + 1} - ${Math.min(page * perPage, total)} of ${total}`}
-
- = total / perPage} onClick={nextPage}>
- Next Page
-
-
- );
-};
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
index 6043a64666d..b679589270d 100644
--- a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
@@ -74,7 +74,6 @@ export const ReferenceArrayFieldBase = <
const {
children,
render,
- pagination,
filter,
page = 1,
perPage,
@@ -110,7 +109,6 @@ export const ReferenceArrayFieldBase = <
{render ? render(controllerProps) : children}
- {!render && pagination}
);
@@ -124,7 +122,6 @@ export interface ReferenceArrayFieldBaseProps<
render?: (props: ListControllerResult) => ReactElement;
filter?: FilterPayload;
page?: number;
- pagination?: ReactElement;
perPage?: number;
reference: string;
sort?: SortPayload;
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.spec.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.spec.tsx
index 783bd09992f..a7c76e18d96 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.spec.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.spec.tsx
@@ -4,7 +4,6 @@ import {
Basic,
Errored,
Loading,
- WithPagination,
WithRenderProp,
} from './ReferenceManyFieldBase.stories';
@@ -70,24 +69,6 @@ describe('ReferenceManyFieldBase', () => {
});
});
- it('should render pagination', async () => {
- render( );
- await waitFor(() => {
- expect(screen.queryByText('1 - 2 of 3')).not.toBeNull();
- expect(screen.queryByText('Next Page')).not.toBeNull();
- expect(screen.queryByText('Previous Page')).not.toBeNull();
- });
- screen.getByText('Next Page').click();
- await waitFor(() => {
- expect(screen.queryByText('3 - 3 of 3')).not.toBeNull();
- });
- screen.getByText('Previous Page').click();
-
- await waitFor(() => {
- expect(screen.queryByText('1 - 2 of 3')).not.toBeNull();
- });
- });
-
describe('with render prop', () => {
it('should display an error if error is defined', async () => {
jest.spyOn(console, 'error')
@@ -156,13 +137,5 @@ describe('ReferenceManyFieldBase', () => {
).not.toBeNull();
});
});
-
- it('should not render pagination when receiving a render prop', async () => {
- render(Custom Pagination} />);
- await waitFor(() => {
- expect(screen.queryByText('War and Peace')).not.toBeNull();
- });
- expect(screen.queryByText('Custom Pagination')).toBeNull();
- });
});
});
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
index e7ebb8e4155..f8605f4b46f 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
@@ -202,49 +202,10 @@ export const Loading = ({ dataProvider = dataProviderWithAuthorsLoading }) => (
);
-export const WithPagination = ({ dataProvider = dataProviderWithAuthors }) => (
-
-
-
-
- }
- >
-
-
-
-
-
- }
- />
-
-
-);
-
export const WithRenderProp = ({
dataProvider = dataProviderWithAuthors,
- pagination,
}: {
dataProvider?: any;
- pagination?: React.ReactNode;
}) => (
{
if (isPending) {
return Loading...
;
@@ -320,26 +280,3 @@ const List = ({ source }: { source: string }) => {
);
};
-
-const Pagination = () => {
- const { page = 1, setPage, total = 0, perPage = 0 } = useListContext();
- const nextPage = () => {
- setPage?.(page + 1);
- };
- const previousPage = () => {
- setPage?.(page - 1);
- };
- return (
-
-
- Previous Page
-
-
- {`${(page - 1) * perPage + 1} - ${Math.min(page * perPage, total)} of ${total}`}
-
- = total / perPage} onClick={nextPage}>
- Next Page
-
-
- );
-};
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
index 8889dfce210..97cb869579e 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
@@ -67,7 +67,6 @@ export const ReferenceManyFieldBase = <
empty,
filter = defaultFilter,
page = 1,
- pagination = null,
perPage = 25,
record,
reference,
@@ -129,7 +128,6 @@ export const ReferenceManyFieldBase = <
{render ? render(controllerProps) : children}
- {!render && pagination}
);
@@ -145,7 +143,6 @@ export interface ReferenceManyFieldBaseProps<
children?: ReactNode;
render?: (props: ListControllerResult) => ReactNode;
empty?: ReactNode;
- pagination?: ReactNode;
}
const defaultFilter = {};
From 2f655d4ddc91c3676ff132fe86bfe29868061951 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Thu, 17 Jul 2025 15:19:37 +0200
Subject: [PATCH 27/41] use context instead of controllerContext
---
docs/ReferenceManyFieldBase.md | 2 +-
docs/ReferenceOneFieldBase.md | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/docs/ReferenceManyFieldBase.md b/docs/ReferenceManyFieldBase.md
index ebeaa731dd3..d9ab1ca4235 100644
--- a/docs/ReferenceManyFieldBase.md
+++ b/docs/ReferenceManyFieldBase.md
@@ -41,7 +41,7 @@ const BookList = ({
}: {
source: string;
}) => {
- const context = useListController();
+ const context = useListContext();
if (context.isPending) {
return Loading...
;
diff --git a/docs/ReferenceOneFieldBase.md b/docs/ReferenceOneFieldBase.md
index 8087d25ecbc..5fd62393e45 100644
--- a/docs/ReferenceOneFieldBase.md
+++ b/docs/ReferenceOneFieldBase.md
@@ -46,7 +46,7 @@ const BookShow = () => (
// with BookDetails something like
const BookDetails = ({ source }) => {
- const context = useReferenceOneFieldContext({
+ const context = useReferenceFieldContext({
reference,
target,
});
@@ -76,7 +76,7 @@ const BookDetails = ({ source }) => {
| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'book_details' |
| `target` | Required | string | - | Target field carrying the relationship on the referenced resource, e.g. 'book_id' |
| `children` | Optional\* | `Element` | - | React component to render the referenced record. |
-| `render` | Optional\* | `(referenceFieldContext) => Element` | - | A function that takes the reference field context and return a React element |
+| `render` | Optional\* | `(ReferenceFieldContext) => Element` | - | A function that takes the `ReferenceFieldContext` and return a React element |
| `empty` | Optional | `ReactNode` | - | The text or element to display when the referenced record is empty |
| `filter` | Optional | `Object` | `{}` | Used to filter referenced records |
| `link` | Optional | `string | Function` | `edit` | Target of the link wrapping the rendered child. Set to `false` to disable the link. |
@@ -90,11 +90,11 @@ const BookDetails = ({ source }) => {
## `children`
You can pass any component of your own as children, to render the referenced record as you wish.
-You can access the list context using the `useReferenceOneFieldController` hook.
+You can access the list context using the `useReferenceFieldContext` hook.
```jsx
const BookDetails = () => {
- const { isPending, error, referenceRecord } = useReferenceOneFieldContext({
+ const { isPending, error, referenceRecord } = useReferenceFieldContext({
reference,
target,
});
From 4a2d42575cd7d110aea6e6e24c97c10cf580beb0 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Thu, 17 Jul 2025 15:30:34 +0200
Subject: [PATCH 28/41] add missing pagination on mui reference many and array
---
packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx | 5 ++++-
packages/ra-ui-materialui/src/field/ReferenceManyField.tsx | 4 +++-
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
index 8ee8f2358db..6cc9b9d1d8d 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
@@ -98,11 +98,14 @@ export interface ReferenceArrayFieldProps<
> extends ReferenceArrayFieldBaseProps,
FieldProps {
sx?: SxProps;
+ pagination?: React.ReactElement;
}
export interface ReferenceArrayFieldViewProps
extends Omit,
- Omit {}
+ Omit {
+ pagination?: React.ReactElement;
+}
export const ReferenceArrayFieldView = (
props: ReferenceArrayFieldViewProps
diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
index 43904fa5f6d..7f98a3ce7fa 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
@@ -82,4 +82,6 @@ export interface ReferenceManyFieldProps<
RecordType extends Record = Record,
ReferenceRecordType extends RaRecord = RaRecord,
> extends Omit, 'source'>,
- ReferenceManyFieldBaseProps {}
+ ReferenceManyFieldBaseProps {
+ pagination?: React.ReactElement;
+}
From f3794b36bfc5c7e487e87021848525a90dd3652f Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Thu, 17 Jul 2025 17:01:52 +0200
Subject: [PATCH 29/41] remove mui components from ReferenceManyFieldBase doc.
Add example of ref in a list in stories
---
docs/ReferenceManyFieldBase.md | 224 ++++++++----------
.../field/ReferenceManyFieldBase.stories.tsx | 66 +++++-
2 files changed, 158 insertions(+), 132 deletions(-)
diff --git a/docs/ReferenceManyFieldBase.md b/docs/ReferenceManyFieldBase.md
index d9ab1ca4235..87e80571a47 100644
--- a/docs/ReferenceManyFieldBase.md
+++ b/docs/ReferenceManyFieldBase.md
@@ -33,7 +33,7 @@ For instance, if an `author` has many `books`, and each book resource exposes an
`` can render the titles of all the books by a given author.
```jsx
-import { Show, SimpleShowLayout, ReferenceManyFieldBase, DataTable, TextField, DateField } from 'react-admin';
+import { ShowBase, ReferenceManyFieldBase } from 'react-admin';
const BookList = ({
source,
@@ -60,15 +60,11 @@ const BookList = ({
};
const AuthorShow = () => (
-
-
-
-
-
-
-
-
-
+
+
+
+
+
);
```
@@ -77,23 +73,16 @@ const AuthorShow = () => (
You can also use `` in a list, e.g. to display the authors of the comments related to each post in a list by matching `post.id` to `comment.post_id`:
```jsx
-import { List, DataTable, ChipField, ReferenceManyFieldBase, SingleFieldList } from 'react-admin';
+import { ListBase, ListIterator, ReferenceManyFieldBase } from 'react-admin';
export const PostList = () => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
);
```
@@ -130,22 +119,19 @@ export const PostList = () => (
For instance, use a `` to render the related records in a table:
```jsx
-import { Show, SimpleShowLayout, TextField, ReferenceManyFieldBase, DataTable, DateField } from 'react-admin';
+import { ShowBase, ReferenceManyFieldBase, ListIterator } from 'react-admin';
export const AuthorShow = () => (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ (
+
+
{book.title}
+
{book.published_at}
+
+ )}/>
+
+
);
```
@@ -155,38 +141,37 @@ Alternatively, you can pass a `render` function prop instead of children. The `r
When receiving a `render` function prop the `` component will ignore the children property.
```jsx
-import { Show, SimpleShowLayout, ReferenceManyFieldBase, DataTable, TextField, DateField } from 'react-admin';
+import { ShowBase, ReferenceManyFieldBase } from 'react-admin';
const AuthorShow = () => (
-
-
-
-
- {
-
- if (isPending) {
- return Loading...
;
- }
-
- if (error) {
- return {error.toString()}
;
- }
- return (
-
- {data.map((author, index) => (
-
{author.name}
- ))}
-
- );
+
+ {
+
+ if (isPending) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return {error.toString()}
;
}
+ return (
+
+ {data.map((book, index) => (
+
+
{book.title}
+
{book.published_at}
+
+ ))}
+
+ );
}
- />
-
-
+ }
+ />
+
);
```
@@ -211,9 +196,9 @@ Use `empty` to customize the text displayed when the related record is empty.
```jsx
...
@@ -223,9 +208,9 @@ Use `empty` to customize the text displayed when the related record is empty.
```jsx
...
@@ -235,9 +220,9 @@ Use `empty` to customize the text displayed when the related record is empty.
```jsx
}
+ reference="books"
+ target="author_id"
+ empty={Create }
>
...
@@ -261,37 +246,6 @@ You can filter the query used to populate the possible values. Use the `filter`
{% endraw %}
-## Filtering The References
-
-
-
- Your browser does not support the video tag.
-
-
-You can add filters to `` by adding [``](./FilterForm.md) and [``](./FilterButton.md):
-
-{% raw %}
-
-```jsx
-const filters = [ ];
-
-const AuthorEdit = () => (
-
-
-
-
-
-
- ...
-
-
-
-
-);
-```
-
-{% endraw %}
-
## `perPage`
By default, react-admin restricts the possible values to 25 and displays no pagination control. You can change the limit by setting the `perPage` prop:
@@ -310,7 +264,9 @@ For instance, to pass [a custom `meta`](./Actions.md#meta-parameter):
{% raw %}
```jsx
-
+
+ ...
+
```
{% endraw %}
@@ -322,10 +278,12 @@ For instance, if you want to display the `books` of a given `author`, the `refer
```jsx
-
-
-
-
+ (
+
+
{book.title}
+
{book.published_at}
+
+ )} />
```
@@ -336,11 +294,11 @@ By default, it orders the possible values by id desc. You can change this order
{% raw %}
```jsx
- ...
+ ...
```
{% endraw %}
@@ -351,9 +309,9 @@ By default, `ReferenceManyFieldBase` uses the `id` field as target for the refer
```jsx
...
@@ -369,7 +327,7 @@ In the example below, both lists use the same reference ('books'), but their sel
{% raw %}
```jsx
-
+
-
-
-
+ (
+ {book.title}
+ )} />
-
-
-
+ (
+ {book.title}
+ )} />
-
+
```
{% endraw %}
@@ -403,9 +361,13 @@ Name of the field carrying the relationship on the referenced resource. For inst
```jsx
-
-
-
-
+ (
+
+
{book.title}
+
{book.published_at}
+
+ )}
+ />
```
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
index f8605f4b46f..d05cf4c9995 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
@@ -5,7 +5,9 @@ import { Resource } from '../../core/Resource';
import { ShowBase } from '../../controller/show/ShowBase';
import { TestMemoryRouter } from '../../routing';
import { ReferenceManyFieldBase } from './ReferenceManyFieldBase';
-import { useListContext } from '../list';
+import { ListBase, ListIterator, useListContext } from '../list';
+import { DataTableBase } from '../../dataTable';
+import fakeRestDataProvider from 'ra-data-fakerest';
export default {
title: 'ra-core/controller/field/ReferenceManyFieldBase',
@@ -100,6 +102,68 @@ export const Basic = ({ dataProvider = dataProviderWithAuthors }) => (
);
+const dataProviderWithAuthorList = fakeRestDataProvider(
+ {
+ authors: [
+ {
+ id: 1,
+ first_name: 'Leo',
+ last_name: 'Tolstoy',
+ language: 'Russian',
+ },
+ {
+ id: 2,
+ first_name: 'William',
+ last_name: 'Shakespear',
+ language: 'English',
+ },
+ ],
+ books,
+ },
+ process.env.NODE_ENV === 'development'
+);
+
+export const InAList = ({ dataProvider = dataProviderWithAuthorList }) => (
+
+
+
+ (
+
+
{author.last_name} Books
+
+
+
+
+
+
+ )}
+ >
+
+ }
+ />
+
+
+);
+
const dataProviderWithAuthorsError = {
getOne: () =>
Promise.resolve({
From 555d093f454c0d08f8e2b4b6bd44f07fc7efd266 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Thu, 17 Jul 2025 17:21:26 +0200
Subject: [PATCH 30/41] remove mui components from ReferenceFieldBase doc.
---
docs/ReferenceFieldBase.md | 97 +++++++++++++++++++++++---------------
1 file changed, 58 insertions(+), 39 deletions(-)
diff --git a/docs/ReferenceFieldBase.md b/docs/ReferenceFieldBase.md
index 28c31f66171..628227d69b7 100644
--- a/docs/ReferenceFieldBase.md
+++ b/docs/ReferenceFieldBase.md
@@ -54,7 +54,7 @@ export const UserView = () => {
if (context.error) {
return {context.error.toString()}
;
}
-
+
return {value}
;
};
@@ -85,7 +85,7 @@ You can pass any component of your own as child, to render the related records a
You can access the list context using the `useReferenceFieldContext` hook.
```tsx
-import { Show, SimpleShowLayout, ReferenceField, TextField, DateField } from 'react-admin';
+import { ReferenceFieldBase } from 'react-admin';
export const UserView = () => {
const { isPending, error } = useReferenceFieldContext();
@@ -111,14 +111,14 @@ export const MyReferenceField = () => (
## `render`
-Alternatively, you can pass a `render` function prop instead of children. This function will receive the `ReferenceFieldContext` as argument.
+Alternatively, you can pass a `render` function prop instead of children. This function will receive the `ReferenceFieldContext` as argument.
```jsx
export const MyReferenceField = () => (
{
+ render={({ error, isPending, referenceRecord }) => {
if (isPending) {
return Loading...
;
}
@@ -130,7 +130,7 @@ export const MyReferenceField = () => (
);
}
- return {value}
;
+ return {referenceRecord.name}
;
}}
/>
);
@@ -143,7 +143,9 @@ The `render` function prop will take priority on `children` props if both are se
`` can display a custom message when the referenced record is missing, thanks to the `empty` prop.
```jsx
-
+
+ ...
+
```
`` renders the `empty` element when:
@@ -154,8 +156,12 @@ The `render` function prop will take priority on `children` props if both are se
You can pass either a React element or a string to the `empty` prop:
```jsx
-Missing user} />
-
+Missing user} >
+ ...
+
+
+ ...
+
```
## `link`
@@ -163,14 +169,18 @@ You can pass either a React element or a string to the `empty` prop:
To change the link from the `` page to the `` page, set the `link` prop to "show".
```jsx
-
+
+ ...
+
```
You can also prevent `` from adding a link to children by setting `link` to `false`.
```jsx
// No link
-
+
+ ...
+
```
You can also use a custom `link` function to get a custom path for the children. This function must accept `record` and `reference` as arguments.
@@ -181,7 +191,9 @@ You can also use a custom `link` function to get a custom path for the children.
source="user_id"
reference="users"
link={(record, reference) => `/my/path/to/${reference}/${record.id}`}
-/>
+>
+ ...
+
```
## `queryOptions`
@@ -195,8 +207,9 @@ For instance, to pass [a custom `meta`](./Actions.md#meta-parameter):
source="user_id"
reference="users"
queryOptions={{ meta: { foo: 'bar' } }}
+ render={({ referenceRecord }) => referenceRecord.name}
>
-
+ ...
```
{% endraw %}
@@ -208,41 +221,44 @@ The resource to fetch for the related record.
For instance, if the `posts` resource has a `user_id` field, set the `reference` to `users` to fetch the user related to each post.
```jsx
-
+
+ ...
+
```
## `sortBy`
By default, when used in a ``, and when the user clicks on the column header of a ``, react-admin sorts the list by the field `source`. To specify another field name to sort by, set the `sortBy` prop.
```jsx
-
+
+ ...
+
```
## Performance
-When used in a ``, `` fetches the referenced record only once for the entire table.
-
-
+When used in a ``, `` fetches the referenced record only once for the entire table.
For instance, with this code:
```jsx
-import { List, DataTable, ReferenceField, EditButton } from 'react-admin';
+import { ListBase, ListIterator, ReferenceFieldBase } from 'react-admin';
export const PostList = () => (
-
-
-
-
-
-
-
-
-
-
-
-
+
+ (
+
+
#{referenceRecord?.id}
+
+
+
+
{referenceRecord.title}
+
+ )}
+ />
+
);
```
@@ -279,15 +295,18 @@ For example, the following code prefetches the authors referenced by the posts:
{% raw %}
```jsx
const PostList = () => (
-
-
-
-
- {/** renders without an additional request */}
-
-
-
-
+
+ (
+
+ )}
+ />
+
);
```
{% endraw %}
From 9dbb6047702193f193ec3493fed089824eb25315 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Thu, 17 Jul 2025 17:28:36 +0200
Subject: [PATCH 31/41] remove mui components from ReferenceArrayFieldBase doc.
---
docs/ReferenceArrayFieldBase.md | 23 ++++++++---------------
docs/ReferenceFieldBase.md | 16 +++++-----------
docs/ReferenceManyFieldBase.md | 12 ++++++------
3 files changed, 19 insertions(+), 32 deletions(-)
diff --git a/docs/ReferenceArrayFieldBase.md b/docs/ReferenceArrayFieldBase.md
index 44d0ea70d6b..789def03769 100644
--- a/docs/ReferenceArrayFieldBase.md
+++ b/docs/ReferenceArrayFieldBase.md
@@ -44,7 +44,7 @@ A typical `post` record therefore looks like this:
In that case, use `` to display the post tag names as Chips as follows:
```jsx
-import { List, DataTable, ReferenceArrayFieldBase } from 'react-admin';
+import { ListBase, ListIterator, ReferenceArrayFieldBase } from 'react-admin';
const MyTagsView = (props: { children: React.ReactNode }) => {
const context = useListContext();
@@ -66,20 +66,13 @@ const MyTagsView = (props: { children: React.ReactNode }) => {
};
export const PostList = () => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
);
```
diff --git a/docs/ReferenceFieldBase.md b/docs/ReferenceFieldBase.md
index 628227d69b7..f9127c14297 100644
--- a/docs/ReferenceFieldBase.md
+++ b/docs/ReferenceFieldBase.md
@@ -247,17 +247,11 @@ import { ListBase, ListIterator, ReferenceFieldBase } from 'react-admin';
export const PostList = () => (
- (
-
-
#{referenceRecord?.id}
-
-
-
-
{referenceRecord.title}
-
- )}
- />
+
+
+
+
+
);
```
diff --git a/docs/ReferenceManyFieldBase.md b/docs/ReferenceManyFieldBase.md
index 87e80571a47..2b79a2f14ae 100644
--- a/docs/ReferenceManyFieldBase.md
+++ b/docs/ReferenceManyFieldBase.md
@@ -278,12 +278,12 @@ For instance, if you want to display the `books` of a given `author`, the `refer
```jsx
- (
-
-
{book.title}
-
{book.published_at}
-
- )} />
+ (
+
+
{book.title}
+
{book.published_at}
+
+ )} />
```
From f8f8465ee550b873ba946932b52a8f7d9e87c843 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Thu, 17 Jul 2025 17:33:42 +0200
Subject: [PATCH 32/41] remove mui components from ReferenceOneFieldBase doc.
---
docs/ReferenceOneFieldBase.md | 35 +++++++++++++++--------------------
1 file changed, 15 insertions(+), 20 deletions(-)
diff --git a/docs/ReferenceOneFieldBase.md b/docs/ReferenceOneFieldBase.md
index 5fd62393e45..48100816694 100644
--- a/docs/ReferenceOneFieldBase.md
+++ b/docs/ReferenceOneFieldBase.md
@@ -32,16 +32,11 @@ Here is how to render a field of the `book_details` resource inside a Show view
```jsx
const BookShow = () => (
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
);
// with BookDetails something like
@@ -156,7 +151,7 @@ Use `empty` to customize the text displayed when the related record is empty.
```jsx
- ( )
+ ...
```
@@ -164,7 +159,7 @@ Use `empty` to customize the text displayed when the related record is empty.
```jsx
- ( )
+ ...
```
@@ -177,7 +172,7 @@ Use `empty` to customize the text displayed when the related record is empty.
target="book_id"
empty={ }
>
- ( )
+ ...
```
@@ -194,7 +189,7 @@ For instance, if a product has prices in many currencies, and you only want to r
target="product_id"
filter={{ currency: "EUR" }}
>
-
+ ...
```
{% endraw %}
@@ -205,7 +200,7 @@ By default, `` will set pass a links to the edition page
```jsx
-
+ ...
```
@@ -218,7 +213,7 @@ You can also set the `link` prop to a string, which will be used as the link typ
target="book_id"
link={record => `/custom/${record.id}`}
>
-
+ ...
```
{% endraw %}
@@ -237,7 +232,7 @@ For instance, if you want to disable the refetch on window focus for this query,
target="book_id"
queryOptions={{ refetchOnWindowFocus: false }}
>
-
+ ...
```
{% endraw %}
@@ -250,7 +245,7 @@ For instance, if you want to display the details of a given book, the `reference
```jsx
-
+ ...
```
@@ -269,7 +264,7 @@ For instance, if you want to display the latest message in a discussion, you can
target="discussion_id"
sort={{ field: "createdAt", order: "DESC" }}
>
-
+ ...
```
{% endraw %}
@@ -295,6 +290,6 @@ In that case, the `target` prop should be set to `book_id`:
```jsx
-
+ ...
```
From 8046b0e6931848904f382006c94cc66065565b9f Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Thu, 17 Jul 2025 18:25:24 +0200
Subject: [PATCH 33/41] move pagination on ReferenceManyField side
---
packages/ra-ui-materialui/src/field/ReferenceManyField.tsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
index 7f98a3ce7fa..621c2ab3177 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
@@ -74,7 +74,10 @@ export const ReferenceManyField = <
props.empty
)
}
- />
+ >
+ {props.children}
+ {!props.render && props.pagination}
+
);
};
From ab821565b1389e874a332f55a0d3cd31fa25950e Mon Sep 17 00:00:00 2001
From: fzaninotto
Date: Mon, 21 Jul 2025 15:18:44 +0200
Subject: [PATCH 34/41] Add optional loading, error and empty props to
ReferenceBase commponents
---
.../field/ReferenceArrayFieldBase.tsx | 34 +++++++++++++++++++
.../controller/field/ReferenceFieldBase.tsx | 9 ++++-
.../field/ReferenceManyFieldBase.tsx | 22 ++++++++----
.../field/ReferenceOneFieldBase.tsx | 24 +++++++++----
4 files changed, 75 insertions(+), 14 deletions(-)
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
index b679589270d..9b6253cad16 100644
--- a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
@@ -74,6 +74,9 @@ export const ReferenceArrayFieldBase = <
const {
children,
render,
+ error,
+ loading,
+ empty,
filter,
page = 1,
perPage,
@@ -105,6 +108,34 @@ export const ReferenceArrayFieldBase = <
);
}
+ if (controllerProps.isPending && loading) {
+ return loading;
+ }
+ if (controllerProps.error && error) {
+ return error;
+ }
+ if (
+ // there is an empty page component
+ empty &&
+ // there is no error
+ !controllerProps.error &&
+ // the list is not loading data for the first time
+ !controllerProps.isPending &&
+ // the API returned no data (using either normal or partial pagination)
+ (controllerProps.total === 0 ||
+ (controllerProps.total == null &&
+ // @ts-ignore FIXME total may be undefined when using partial pagination but the ListControllerResult type is wrong about it
+ controllerProps.hasPreviousPage === false &&
+ // @ts-ignore FIXME total may be undefined when using partial pagination but the ListControllerResult type is wrong about it
+ controllerProps.hasNextPage === false &&
+ // @ts-ignore FIXME total may be undefined when using partial pagination but the ListControllerResult type is wrong about it
+ controllerProps.data.length === 0)) &&
+ // the user didn't set any filters
+ !Object.keys(controllerProps.filterValues).length
+ ) {
+ return empty;
+ }
+
return (
@@ -120,6 +151,9 @@ export interface ReferenceArrayFieldBaseProps<
> extends BaseFieldProps {
children?: ReactNode;
render?: (props: ListControllerResult) => ReactElement;
+ error?: ReactNode;
+ loading?: ReactNode;
+ empty?: ReactNode;
filter?: FilterPayload;
page?: number;
perPage?: number;
diff --git a/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx
index 51ce01fb11d..93c6589743c 100644
--- a/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx
@@ -47,7 +47,7 @@ export const ReferenceFieldBase = <
>(
props: ReferenceFieldBaseProps
) => {
- const { children, render, empty = null } = props;
+ const { children, render, loading, error, empty = null } = props;
const id = useFieldValue(props);
const controllerProps =
@@ -59,6 +59,12 @@ export const ReferenceFieldBase = <
);
}
+ if (controllerProps.isPending && loading) {
+ return loading;
+ }
+ if (controllerProps.error && error) {
+ return error;
+ }
if (
(empty &&
// no foreign key value
@@ -92,6 +98,7 @@ export interface ReferenceFieldBaseProps<
className?: string;
empty?: ReactNode;
error?: ReactNode;
+ loading?: ReactNode;
queryOptions?: Partial<
UseQueryOptions & {
meta?: any;
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
index 97cb869579e..c21555993d1 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
@@ -65,6 +65,8 @@ export const ReferenceManyFieldBase = <
render,
debounce,
empty,
+ error,
+ loading,
filter = defaultFilter,
page = 1,
perPage = 25,
@@ -96,6 +98,18 @@ export const ReferenceManyFieldBase = <
queryOptions,
});
+ if (!render && !children) {
+ throw new Error(
+ " requires either a 'render' prop or 'children' prop"
+ );
+ }
+
+ if (controllerProps.isPending && loading) {
+ return loading;
+ }
+ if (controllerProps.error && error) {
+ return error;
+ }
if (
// there is an empty page component
empty &&
@@ -118,12 +132,6 @@ export const ReferenceManyFieldBase = <
return empty;
}
- if (!render && !children) {
- throw new Error(
- " requires either a 'render' prop or 'children' prop"
- );
- }
-
return (
@@ -143,6 +151,8 @@ export interface ReferenceManyFieldBaseProps<
children?: ReactNode;
render?: (props: ListControllerResult) => ReactNode;
empty?: ReactNode;
+ error?: ReactNode;
+ loading?: ReactNode;
}
const defaultFilter = {};
diff --git a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx
index 6ed6634d6fa..83087ab55bb 100644
--- a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx
@@ -36,6 +36,8 @@ export const ReferenceOneFieldBase = <
source = 'id',
target,
empty,
+ error,
+ loading,
sort,
filter,
link,
@@ -69,7 +71,19 @@ export const ReferenceOneFieldBase = <
[controllerProps, path]
);
+ if (!render && !children) {
+ throw new Error(
+ " requires either a 'render' prop or 'children' prop"
+ );
+ }
+
const recordFromContext = useRecordContext(props);
+ if (controllerProps.isPending && loading) {
+ return loading;
+ }
+ if (controllerProps.error && error) {
+ return error;
+ }
if (
!recordFromContext ||
(!controllerProps.isPending && controllerProps.referenceRecord == null)
@@ -77,12 +91,6 @@ export const ReferenceOneFieldBase = <
return empty;
}
- if (!render && !children) {
- throw new Error(
- " requires either a 'render' prop or 'children' prop"
- );
- }
-
return (
@@ -102,8 +110,10 @@ export interface ReferenceOneFieldBaseProps<
ReferenceRecordType
> {
children?: ReactNode;
+ loading?: ReactNode;
+ error?: ReactNode;
+ empty?: ReactNode;
render?: (props: UseReferenceResult) => ReactNode;
link?: LinkToType;
- empty?: ReactNode;
resource?: string;
}
From ef7057814bbc2365c3e706b5c27196d532eecba7 Mon Sep 17 00:00:00 2001
From: fzaninotto
Date: Mon, 21 Jul 2025 15:27:35 +0200
Subject: [PATCH 35/41] Improve ListBase doc
---
docs/ListBase.md | 65 ++++++++++++++++++++++++++++++++++--------------
1 file changed, 46 insertions(+), 19 deletions(-)
diff --git a/docs/ListBase.md b/docs/ListBase.md
index 2dfd4607a7b..b23d972f27e 100644
--- a/docs/ListBase.md
+++ b/docs/ListBase.md
@@ -6,7 +6,7 @@ storybook_path: ra-core-controller-list-listbase--no-auth-provider
# ``
-`` is a headless variant of [``](./List.md). It fetches a list of records from the data provider, puts it in a [`ListContext`](./useListContext.md), and renders its children. Use it to build a custom list layout.
+`` is a headless List page component. It fetches a list of records from the data provider, puts it in a [`ListContext`](./useListContext.md), and renders its children. Use it to build a custom list layout.
Contrary to [``](./List.md), it does not render the page layout, so no title, no actions, no ``, and no pagination.
@@ -14,47 +14,73 @@ Contrary to [``](./List.md), it does not render the page layout, so no tit
## Usage
-You can use `ListBase` to create your own custom reusable List component, like this one:
+You can use `ListBase` to create your own custom List page component, like this one:
```jsx
import {
+ DataTable,
ListBase,
- Title,
ListToolbar,
- Pagination,
DataTable,
+ Pagination,
+ Title,
} from 'react-admin';
import { Card } from '@mui/material';
-const MyList = ({ children, actions, filters, title, ...props }) => (
-
-
+const PostList = () => (
+
+
- {children}
+
+
+
+
+
);
+```
+Alternatively, you can pass a `render` function prop instead of `children`. This function will receive the `ListContext` as argument.
+
+```jsx
const PostList = () => (
-
-
- ...
-
-
+ (
+
+
+
+
+ {data?.map(record => (
+
+
+
+
+
+ ))}
+
+
+
+ )} />
);
```
-This custom List component has no aside component - it's up to you to add it in pure React.
-
## Props
-The `` component accepts the same props as [`useListController`](./useListController.md):
+The `` component accepts the following props:
+* `children`
* [`debounce`](./List.md#debounce)
* [`disableAuthentication`](./List.md#disableauthentication)
* [`disableSyncWithLocation`](./List.md#disablesyncwithlocation)
@@ -63,11 +89,12 @@ The `` component accepts the same props as [`useListController`](./use
* [`filterDefaultValues`](./List.md#filterdefaultvalues)
* [`perPage`](./List.md#perpage)
* [`queryOptions`](./List.md#queryoptions)
+* `render`
* [`resource`](./List.md#resource)
* [`sort`](./List.md#sort)
In addition, `` renders its children components inside a `ListContext`. Check [the `` documentation](./List.md#children) for usage examples.
-Alternatively, you can pass a `render` function prop instead of `children`. This function will receive the `ListContext` as argument.
+
## Security
From 10a52cf18afcacb4575126436f59da15cdccf356 Mon Sep 17 00:00:00 2001
From: fzaninotto
Date: Mon, 21 Jul 2025 15:41:56 +0200
Subject: [PATCH 36/41] Misc doc fixes
---
docs/ReferenceArrayFieldBase.md | 29 ++++++------
docs/ReferenceFieldBase.md | 84 +++++++++------------------------
2 files changed, 37 insertions(+), 76 deletions(-)
diff --git a/docs/ReferenceArrayFieldBase.md b/docs/ReferenceArrayFieldBase.md
index 789def03769..955ee8b2968 100644
--- a/docs/ReferenceArrayFieldBase.md
+++ b/docs/ReferenceArrayFieldBase.md
@@ -41,12 +41,22 @@ A typical `post` record therefore looks like this:
}
```
-In that case, use `` to display the post tag names as Chips as follows:
+In that case, use `` to display the post tag names as a list of chips, as follows:
```jsx
import { ListBase, ListIterator, ReferenceArrayFieldBase } from 'react-admin';
-const MyTagsView = (props: { children: React.ReactNode }) => {
+export const PostList = () => (
+
+
+
+
+
+
+
+);
+
+const TagList = (props: { children: React.ReactNode }) => {
const context = useListContext();
if (context.isPending) {
@@ -64,16 +74,6 @@ const MyTagsView = (props: { children: React.ReactNode }) => {
);
};
-
-export const PostList = () => (
-
-
-
-
-
-
-
-);
```
`` expects a `reference` attribute, which specifies the resource to fetch for the related records. It also expects a `source` attribute, which defines the field containing the list of ids to look for in the referenced resource.
@@ -106,11 +106,10 @@ You can access the list context using the `useListContext` hook.
```jsx
-
+
-// With MyTagList like:
-const MyTagList = (props: { children: React.ReactNode }) => {
+const TagList = (props: { children: React.ReactNode }) => {
const { isPending, error, data } = useListContext();
if (isPending) {
diff --git a/docs/ReferenceFieldBase.md b/docs/ReferenceFieldBase.md
index f9127c14297..d2c7695092e 100644
--- a/docs/ReferenceFieldBase.md
+++ b/docs/ReferenceFieldBase.md
@@ -24,29 +24,25 @@ For instance, let's consider a model where a `post` has one author from the `use
└──────────────┘
```
-In that case, use `` to display the post author's as follows:
+In that case, use `` to display the post's author as follows:
```jsx
-import { Show, SimpleShowLayout, ReferenceField, TextField, DateField } from 'react-admin';
-
-export const UserList = () => (
-
-
-
+import { Show, SimpleShowLayout, ReferenceField, TextField, RecordRepresentation } from 'react-admin';
+
+export const PostShow = () => (
+
+
+
+
+
+
+
+
);
-```
-
-`` fetches the data, puts it in a [`RecordContext`](./useRecordContext.md), and its up to its children to handle the rendering by accessing the ReferencingContext using the useReferenceFieldContext hook.
-
-This component fetches a referenced record (`users` in this example) using the `dataProvider.getMany()` method, and passes it to the `ReferenceFieldContext`.
-
-```tsx
-import { Show, SimpleShowLayout, ReferenceField, TextField, DateField } from 'react-admin';
export const UserView = () => {
const context = useReferenceFieldContext();
- const value = useFieldValue({ source });
if (context.isPending) {
return Loading...
;
}
@@ -55,16 +51,12 @@ export const UserView = () => {
return {context.error.toString()}
;
}
- return {value}
;
+ return ;
};
-
-export const MyReferenceField = () => (
-
-
-
-);
```
+`` fetches the data, puts it in a [`RecordContext`](./useRecordContext.md), and its up to its children to handle the rendering by accessing the `ReferencingContext` using the `useReferenceFieldContext` hook.
+
It uses `dataProvider.getMany()` instead of `dataProvider.getOne()` [for performance reasons](#performance). When using several `` in the same page (e.g. in a ``), this allows to call the `dataProvider` once instead of once per row.
## Props
@@ -74,7 +66,7 @@ It uses `dataProvider.getMany()` instead of `dataProvider.getOne()` [for perform
| `source` | Required | `string` | - | Name of the property to display |
| `reference` | Required | `string` | - | The name of the resource for the referenced records, e.g. 'posts' |
| `children` | Optional | `ReactNode` | - | React component to render the referenced record. |
-| `render` | Optional | `(context) => ReactNode` | - | Function that takes the referenceFieldContext and render the referenced record. |
+| `render` | Optional | `(context) => ReactNode` | - | Function that takes the referenceFieldContext and renders the referenced record. |
| `empty` | Optional | `ReactNode` | - | What to render when the field has no value or when the reference is missing |
| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options |
| `sortBy` | Optional | `string | Function` | `source` | Name of the field to use for sorting when used in a Datagrid |
@@ -88,18 +80,17 @@ You can access the list context using the `useReferenceFieldContext` hook.
import { ReferenceFieldBase } from 'react-admin';
export const UserView = () => {
- const { isPending, error } = useReferenceFieldContext();
+ const { error, isPending, referenceRecord } = useReferenceFieldContext();
- const value = useFieldValue({ source });
- if (context.isPending) {
+ if (isPending) {
return Loading...
;
}
- if (context.error) {
- return {context.error.toString()}
;
+ if (error) {
+ return {error.toString()}
;
}
- return {value}
;
+ return <>{referenceRecord.name}>;
};
export const MyReferenceField = () => (
@@ -164,37 +155,6 @@ You can pass either a React element or a string to the `empty` prop:
```
-## `link`
-
-To change the link from the `` page to the `` page, set the `link` prop to "show".
-
-```jsx
-
- ...
-
-```
-
-You can also prevent `` from adding a link to children by setting `link` to `false`.
-
-```jsx
-// No link
-
- ...
-
-```
-
-You can also use a custom `link` function to get a custom path for the children. This function must accept `record` and `reference` as arguments.
-
-```jsx
-// Custom path
- `/my/path/to/${reference}/${record.id}`}
->
- ...
-
-```
## `queryOptions`
Use the `queryOptions` prop to pass options to [the `dataProvider.getMany()` query](./useGetOne.md#aggregating-getone-calls) that fetches the referenced record.
@@ -225,6 +185,7 @@ For instance, if the `posts` resource has a `user_id` field, set the `reference`
...
```
+
## `sortBy`
By default, when used in a ``, and when the user clicks on the column header of a ``, react-admin sorts the list by the field `source`. To specify another field name to sort by, set the `sortBy` prop.
@@ -234,6 +195,7 @@ By default, when used in a ``, and when the user clicks on the column
...
```
+
## Performance
From a70196556f83832673a8e0838b5d7008dc2ce917 Mon Sep 17 00:00:00 2001
From: fzaninotto
Date: Mon, 21 Jul 2025 16:30:50 +0200
Subject: [PATCH 37/41] Fix types
---
.../ra-core/src/controller/field/types.ts | 90 +------------------
packages/ra-ui-materialui/src/field/types.ts | 89 +++++++++++++++++-
2 files changed, 89 insertions(+), 90 deletions(-)
diff --git a/packages/ra-core/src/controller/field/types.ts b/packages/ra-core/src/controller/field/types.ts
index f66fd2b2b4c..c18c4be5409 100644
--- a/packages/ra-core/src/controller/field/types.ts
+++ b/packages/ra-core/src/controller/field/types.ts
@@ -1,45 +1,8 @@
-import { ReactElement } from 'react';
-import { ExtractRecordPaths, HintedString } from '../../types';
-
-type SortOrder = 'ASC' | 'DESC';
+import { ExtractRecordPaths } from '../../types';
export interface BaseFieldProps<
RecordType extends Record = Record,
> {
- /**
- * The field to use for sorting when users click this column head, if sortable.
- *
- * @see https://marmelab.com/react-admin/Fields.html#sortby
- * @example
- * const PostList = () => (
- *
- *
- *
- *
- *
- *
- *
- *
- * );
- */
- sortBy?: HintedString>;
-
- /**
- * The order used for sorting when users click this column head, if sortable.
- *
- * @see https://marmelab.com/react-admin/Fields.html#sortbyorder
- * @example
- * const PostList = () => (
- *
- *
- *
- *
- *
- *
- * );
- */
- sortByOrder?: SortOrder;
-
/**
* Name of the property to display.
*
@@ -56,57 +19,6 @@ export interface BaseFieldProps<
*/
source: ExtractRecordPaths;
- /**
- * Label to use as column header when using or .
- * Defaults to the capitalized field name. Set to false to disable the label.
- *
- * @see https://marmelab.com/react-admin/Fields.html#label
- * @example
- * const PostList = () => (
- *
- *
- *
- *
- *
- *
- * );
- */
- label?: string | ReactElement | boolean;
-
- /**
- * Set it to false to disable the click handler on the column header when used inside .
- *
- * @see https://marmelab.com/react-admin/Fields.html#sortable
- * @example
- * const PostList = () => (
- *
- *
- *
- *
- *
- *
- *
- *
- * );
- */
- sortable?: boolean;
-
- /**
- * The text to display when the field value is empty. Defaults to empty string.
- *
- * @see https://marmelab.com/react-admin/Fields.html#emptytext
- * @example
- * const PostList = () => (
- *
- *
- *
- *
- *
- *
- * );
- */
- emptyText?: string;
-
/**
* The current record to use. Defaults to the `RecordContext` value.
*
diff --git a/packages/ra-ui-materialui/src/field/types.ts b/packages/ra-ui-materialui/src/field/types.ts
index efcbc5b2019..092f862a05a 100644
--- a/packages/ra-ui-materialui/src/field/types.ts
+++ b/packages/ra-ui-materialui/src/field/types.ts
@@ -1,7 +1,9 @@
+import { ReactElement } from 'react';
import { TableCellProps } from '@mui/material/TableCell';
-import { BaseFieldProps } from 'ra-core';
+import { BaseFieldProps, ExtractRecordPaths, HintedString } from 'ra-core';
type TextAlign = TableCellProps['align'];
+type SortOrder = 'ASC' | 'DESC';
export interface FieldProps<
RecordType extends Record = Record,
@@ -21,6 +23,91 @@ export interface FieldProps<
*/
headerClassName?: string;
+ /**
+ * Label to use as column header when using or .
+ * Defaults to the capitalized field name. Set to false to disable the label.
+ *
+ * @see https://marmelab.com/react-admin/Fields.html#label
+ * @example
+ * const PostList = () => (
+ *
+ *
+ *
+ *
+ *
+ *
+ * );
+ */
+ label?: string | ReactElement | boolean;
+
+ /**
+ * Set it to false to disable the click handler on the column header when used inside .
+ *
+ * @see https://marmelab.com/react-admin/Fields.html#sortable
+ * @example
+ * const PostList = () => (
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * );
+ */
+ sortable?: boolean;
+
+ /**
+ * The text to display when the field value is empty. Defaults to empty string.
+ *
+ * @see https://marmelab.com/react-admin/Fields.html#emptytext
+ * @example
+ * const PostList = () => (
+ *
+ *
+ *
+ *
+ *
+ *
+ * );
+ */
+ emptyText?: string;
+
+ /**
+ * The field to use for sorting when users click this column head, if sortable.
+ *
+ * @see https://marmelab.com/react-admin/Fields.html#sortby
+ * @example
+ * const PostList = () => (
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * );
+ */
+ sortBy?: HintedString>;
+
+ /**
+ * The order used for sorting when users click this column head, if sortable.
+ *
+ * @see https://marmelab.com/react-admin/Fields.html#sortbyorder
+ * @example
+ * const PostList = () => (
+ *
+ *
+ *
+ *
+ *
+ *
+ * );
+ */
+ sortByOrder?: SortOrder;
+
/**
* The text alignment for the cell content, when used inside .
*
From 979444c79f72e2d87cd55229739da64255dc05f6 Mon Sep 17 00:00:00 2001
From: fzaninotto
Date: Mon, 21 Jul 2025 16:32:32 +0200
Subject: [PATCH 38/41] Fix base stories and doc
---
docs/ReferenceArrayFieldBase.md | 12 ++--
docs/ReferenceManyFieldBase.md | 69 ++++++++-----------
docs/ReferenceOneFieldBase.md | 46 ++++++++-----
.../field/ReferenceArrayFieldBase.stories.tsx | 47 ++++++-------
.../field/ReferenceManyFieldBase.stories.tsx | 54 +++++----------
.../field/ReferenceOneFieldBase.stories.tsx | 39 +++++------
6 files changed, 118 insertions(+), 149 deletions(-)
diff --git a/docs/ReferenceArrayFieldBase.md b/docs/ReferenceArrayFieldBase.md
index 955ee8b2968..92c3a02f7f0 100644
--- a/docs/ReferenceArrayFieldBase.md
+++ b/docs/ReferenceArrayFieldBase.md
@@ -9,7 +9,8 @@ storybook_path: ra-core-fields-referencearrayfieldbase--basic
Use `` to display a list of related records, via a one-to-many relationship materialized by an array of foreign keys.
`` fetches a list of referenced records (using the `dataProvider.getMany()` method), and puts them in a [`ListContext`](./useListContext.md). This component is headless, and its children need to use the data from this context to render the desired ui.
-For a component handling the UI too use [the `` component](./ReferenceArrayField.md) instead.
+
+**Tip**: For a rendering a list of chips by default, use [the `` component](./ReferenceArrayField.md) instead.
**Tip**: If the relationship is materialized by a foreign key on the referenced resource, use [the `` component](./ReferenceManyFieldBase.md) instead.
@@ -100,11 +101,9 @@ You can change how the list of related records is rendered by passing a custom c
## `children`
-You can pass any component of your own as child, to render the list of related records as you wish.
-You can access the list context using the `useListContext` hook.
+You can pass any React component as child, to render the list of related records based on the `ListContext`.
```jsx
-
@@ -131,8 +130,7 @@ const TagList = (props: { children: React.ReactNode }) => {
## `render`
-Alternatively to children you can pass a `render` function prop to ``. The `render` prop will receive the `ListContext` as its argument, allowing to inline the `render` logic.
-When receiving a `render` prop the `` component will ignore the children property.
+Alternatively to `children`, you can pass a `render` function prop to ``. The `render` prop will receive the `ListContext` as its argument, allowing to inline the rendering logic.
```jsx
` component will ig
/>
```
+**Tip**: When receiving a `render` prop, the `` component will ignore the `children` property.
+
## `filter`
`` fetches all the related records, and displays them all, too. You can use the `filter` prop to filter the list of related records to display (this works by filtering the records client-side, after the fetch).
diff --git a/docs/ReferenceManyFieldBase.md b/docs/ReferenceManyFieldBase.md
index 2b79a2f14ae..646bf008f98 100644
--- a/docs/ReferenceManyFieldBase.md
+++ b/docs/ReferenceManyFieldBase.md
@@ -1,22 +1,21 @@
---
layout: default
title: "The ReferenceManyFieldBase Component"
-storybook_path: ra-ui-materialui-fields-referencemanyfieldbase--basic
+storybook_path: ra-core-controller-field-referencemanyfieldbase--basic
---
# ``
`` is useful for displaying a list of related records via a one-to-many relationship, when the foreign key is carried by the referenced resource.
-This component fetches a list of referenced records by a reverse lookup of the current `record.id` in the `target` field of another resource (using the `dataProvider.getManyReference()` REST method), and puts them in a [`ListContext`](./useListContext.md). This component is headless, and its children need to use the data from this context to render the desired ui.
-For a component handling the UI too use [the `` component](./ReferenceManyField.md) instead.
+This component fetches a list of referenced records by a reverse lookup of the current `record.id` in the `target` field of another resource (using the `dataProvider.getManyReference()` REST method), and puts them in a [`ListContext`](./useListContext.md).
+
+This component is headless. It relies on its `children` or a `render` prop to render the desired ui.
**Tip**: If the relationship is materialized by an array of ids in the initial record, use [the `` component](./ReferenceArrayFieldBase.md) instead.
## Usage
-### With children
-
For instance, if an `author` has many `books`, and each book resource exposes an `author_id` field:
```
@@ -35,6 +34,14 @@ For instance, if an `author` has many `books`, and each book resource exposes an
```jsx
import { ShowBase, ReferenceManyFieldBase } from 'react-admin';
+const AuthorShow = () => (
+
+
+
+
+
+);
+
const BookList = ({
source,
children,
@@ -58,14 +65,6 @@ const BookList = ({
);
};
-
-const AuthorShow = () => (
-
-
-
-
-
-);
```
`` accepts a `reference` attribute, which specifies the resource to fetch for the related record. It also accepts a `source` attribute which defines the field containing the value to look for in the `target` field of the referenced resource. By default, this is the `id` of the resource (`authors.id` in the previous example).
@@ -91,7 +90,7 @@ export const PostList = () => (
| Prop | Required | Type | Default | Description |
| -------------- | -------- | --------------------------------------------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------- |
| `children` | Optional | `Element` | - | One or several elements that render a list of records based on a `ListContext` |
-| `render` | Optional\* | `(ListContext) => Element` | - | Function that receives a `ListContext` and render elements |
+| `render` | Optional\* | `(ListContext) => Element` | - | Function that receives a `ListContext` and returns an element |
| `debounce` | Optional\* | `number` | 500 | debounce time in ms for the `setFilters` callbacks |
| `empty` | Optional | `ReactNode` | - | Element to display when there are no related records. |
| `filter` | Optional | `Object` | - | Filters to use when fetching the related records, passed to `getManyReference()` |
@@ -116,7 +115,7 @@ export const PostList = () => (
- [``](./EditableDatagrid.md)
- [``](./Calendar.md)
-For instance, use a `` to render the related records in a table:
+For instance, use a `` to render the related records:
```jsx
import { ShowBase, ReferenceManyFieldBase, ListIterator } from 'react-admin';
@@ -124,12 +123,13 @@ import { ShowBase, ReferenceManyFieldBase, ListIterator } from 'react-admin';
export const AuthorShow = () => (
- (
-
-
{book.title}
-
{book.published_at}
-
- )}/>
+
+ (
+
+ {book.title} , published on{' '}{book.published_at}
+
+ )}/>
+
);
@@ -159,14 +159,13 @@ const AuthorShow = () => (
return {error.toString()}
;
}
return (
-
+
{data.map((book, index) => (
-
-
{book.title}
-
{book.published_at}
-
+
+ {book.title} , published on{' '}{book.published_at}
+
))}
-
+
);
}
}
@@ -278,12 +277,7 @@ For instance, if you want to display the `books` of a given `author`, the `refer
```jsx
- (
-
-
{book.title}
-
{book.published_at}
-
- )} />
+ ...
```
@@ -361,13 +355,6 @@ Name of the field carrying the relationship on the referenced resource. For inst
```jsx
- (
-
-
{book.title}
-
{book.published_at}
-
- )}
- />
+ ...
```
diff --git a/docs/ReferenceOneFieldBase.md b/docs/ReferenceOneFieldBase.md
index 48100816694..0f119d04502 100644
--- a/docs/ReferenceOneFieldBase.md
+++ b/docs/ReferenceOneFieldBase.md
@@ -21,16 +21,17 @@ This field fetches a one-to-one relationship, e.g. the details of a book, when u
`` behaves like ``: 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.
-`` is a headless component, handling only the logic. This allows to use any UI library for the render. For a version based on MUI see [``](/ReferenceOneField.html)
+`` is a headless component, handling only the logic and relying on its `children` or `render` prop to render the UI.
-For the inverse relationships (the book linked to a book_detail), you can use a [``](./ReferenceFieldBase.md).
+**Tip**: For a version based on MUI, see [``](/ReferenceOneField.html)
+
+**Tip**: For the inverse relationships (the book linked to a book_detail), you can use a [``](./ReferenceFieldBase.md).
## Usage
Here is how to render a field of the `book_details` resource inside a Show view for the `books` resource:
```jsx
-
const BookShow = () => (
@@ -39,8 +40,7 @@ const BookShow = () => (
);
-// with BookDetails something like
-const BookDetails = ({ source }) => {
+const BookDetails = () => {
const context = useReferenceFieldContext({
reference,
target,
@@ -53,10 +53,13 @@ const BookDetails = ({ source }) => {
if (context.error) {
return {context.error.toString()}
;
}
+ if (!context.referenceRecord) {
+ return No details found
;
+ }
return (
-
{record ? record.genre : ''}
-
{record ? record.ISBN : ''}
+
{context.referenceRecord.genre}
+
{context.referenceRecord.ISBN}
);
}
@@ -88,6 +91,12 @@ You can pass any component of your own as children, to render the referenced rec
You can access the list context using the `useReferenceFieldContext` hook.
```jsx
+const BookShow = () => (
+
+
+
+);
+
const BookDetails = () => {
const { isPending, error, referenceRecord } = useReferenceFieldContext({
reference,
@@ -101,19 +110,16 @@ const BookDetails = () => {
if (error) {
return {error.toString()}
;
}
+ if (!referenceRecord) {
+ return No details found
;
+ }
return (
-
{referenceRecord ? referenceRecord.genre : ''}
-
{referenceRecord ? referenceRecord.ISBN : ''}
+
{referenceRecord.genre}
+
{referenceRecord.ISBN}
);
}
-
-const BookShow = () => (
-
-
-
-);
```
## `render`
@@ -134,10 +140,14 @@ const BookShow = () => (
if (error) {
return {error.toString()}
;
}
+
+ if (!referenceRecord) {
+ return No details found
;
+ }
return (
-
{referenceRecord ? referenceRecord.genre : ''}
-
{referenceRecord ? referenceRecord.ISBN : ''}
+
{referenceRecord.genre}
+
{referenceRecord.ISBN}
);
}}
@@ -196,7 +206,7 @@ For instance, if a product has prices in many currencies, and you only want to r
## `link`
-By default, `` will set pass a links to the edition page of the related record in the context.link. You can disable this behavior by setting the `link` prop to `false`.
+By default, `` populates the context with a `link` value that links to the edition page of the related record. You can disable this behavior by setting the `link` prop to `false`.
```jsx
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx
index debfa90798a..9e8685ffeca 100644
--- a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.stories.tsx
@@ -56,9 +56,7 @@ export const Basic = ({
source="members"
reference="artists"
>
-
-
-
+
}
@@ -67,6 +65,25 @@ export const Basic = ({
);
+const ArtistList = () => {
+ const { isPending, error, data } = useListContext();
+
+ if (isPending) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return {error.toString()}
;
+ }
+ return (
+
+ {data.map((datum, index) => (
+
{datum.name}
+ ))}
+
+ );
+};
+
const erroredDataProvider = {
...defaultDataProvider,
getMany: _resource => Promise.reject(new Error('Error')),
@@ -137,27 +154,3 @@ export const WithRenderProp = ({
);
-
-const MyReferenceArrayField = (props: { children: React.ReactNode }) => {
- const context = useListContext();
-
- if (context.isPending) {
- return Loading...
;
- }
-
- if (context.error) {
- return {context.error.toString()}
;
- }
- return props.children;
-};
-
-const List = ({ source }: { source: string }) => {
- const listContext = useListContext();
- return (
-
- {listContext.data?.map((datum, index) => (
-
{datum[source]}
- ))}
-
- );
-};
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
index d05cf4c9995..eab2b8eef8d 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.stories.tsx
@@ -45,25 +45,20 @@ const books = [
];
export const dataProviderWithAuthors = {
- getOne: () =>
- Promise.resolve({
- data: author,
- }),
- getMany: (_resource, params) =>
- Promise.resolve({
- data: books.filter(book => params.ids.includes(book.author)),
- }),
- getManyReference: (_resource, params) => {
+ getOne: async () => ({ data: author }),
+ getMany: async (_resource, params) => ({
+ data: books.filter(book => params.ids.includes(book.author)),
+ }),
+ getManyReference: async (_resource, params) => {
const result = books.filter(book => book.author === params.id);
-
- return Promise.resolve({
+ return {
data: result.slice(
(params.pagination.page - 1) * params.pagination.perPage,
(params.pagination.page - 1) * params.pagination.perPage +
params.pagination.perPage
),
total: result.length,
- });
+ };
},
} as any;
@@ -91,9 +86,7 @@ export const Basic = ({ dataProvider = dataProviderWithAuthors }) => (
source="id"
reference="books"
>
-
-
-
+
}
@@ -150,9 +143,7 @@ export const InAList = ({ dataProvider = dataProviderWithAuthorList }) => (
source="id"
reference="books"
>
-
-
-
+
)}
@@ -207,9 +198,7 @@ export const Errored = ({ dataProvider = dataProviderWithAuthorsError }) => (
target="id"
source="author"
>
-
-
-
+
}
@@ -255,9 +244,7 @@ export const Loading = ({ dataProvider = dataProviderWithAuthorsLoading }) => (
target="id"
source="author"
>
-
-
-
+
}
@@ -321,26 +308,19 @@ export const WithRenderProp = ({
);
-const MyReferenceManyField = ({ children }: { children: React.ReactNode }) => {
- const context = useListContext();
+const AuthorList = ({ source }) => {
+ const { isPending, error, data } = useListContext();
- if (context.isPending) {
+ if (isPending) {
return Loading...
;
}
- if (context.error) {
- return {context.error.toString()}
;
+ if (error) {
+ return {error.toString()}
;
}
- return children;
-};
-
-const List = ({ source }: { source: string }) => {
- const listContext = useListContext();
return (
- {listContext.data?.map((datum, index) => (
-
{datum[source]}
- ))}
+ {data?.map((datum, index) => {datum[source]} )}
);
};
diff --git a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx
index f7fab891a07..64dad507bac 100644
--- a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.stories.tsx
@@ -36,11 +36,28 @@ const Wrapper = ({ children, dataProvider = defaultDataProvider }) => (
export const Basic = () => (
-
+
);
+const BookDetails = () => {
+ const { isPending, error, referenceRecord } = useReferenceFieldContext();
+
+ if (isPending) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return {error.toString()}
;
+ }
+ if (!referenceRecord) {
+ return No details found
;
+ }
+
+ return {referenceRecord.ISBN} ;
+};
+
const dataProviderWithLoading = {
getManyReference: () => new Promise(() => {}),
} as any;
@@ -48,7 +65,7 @@ const dataProviderWithLoading = {
export const Loading = () => (
-
+
);
@@ -83,21 +100,3 @@ export const WithRenderProp = ({
);
};
-
-const MyReferenceOneField = () => {
- const context = useReferenceFieldContext();
-
- if (context.isPending) {
- return Loading...
;
- }
-
- if (context.error) {
- return {context.error.toString()}
;
- }
-
- return (
-
- {context.referenceRecord ? context.referenceRecord.ISBN : ''}
-
- );
-};
From c6911a05bd845e1568e21d8f99d16c155d939e0d Mon Sep 17 00:00:00 2001
From: fzaninotto
Date: Mon, 21 Jul 2025 16:55:33 +0200
Subject: [PATCH 39/41] Add context to render empty, error and loading
components
---
.../field/ReferenceArrayFieldBase.tsx | 20 ++++++++++++++++---
.../controller/field/ReferenceFieldBase.tsx | 20 ++++++++++++++++---
.../field/ReferenceManyFieldBase.tsx | 20 ++++++++++++++++---
.../field/ReferenceOneFieldBase.tsx | 20 ++++++++++++++++---
4 files changed, 68 insertions(+), 12 deletions(-)
diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
index 9b6253cad16..ba0ee3f1e19 100644
--- a/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldBase.tsx
@@ -109,10 +109,20 @@ export const ReferenceArrayFieldBase = <
}
if (controllerProps.isPending && loading) {
- return loading;
+ return (
+
+ {loading}
+
+ );
}
if (controllerProps.error && error) {
- return error;
+ return (
+
+
+ {error}
+
+
+ );
}
if (
// there is an empty page component
@@ -133,7 +143,11 @@ export const ReferenceArrayFieldBase = <
// the user didn't set any filters
!Object.keys(controllerProps.filterValues).length
) {
- return empty;
+ return (
+
+ {empty}
+
+ );
}
return (
diff --git a/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx
index 93c6589743c..8a43db5087c 100644
--- a/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceFieldBase.tsx
@@ -60,10 +60,20 @@ export const ReferenceFieldBase = <
}
if (controllerProps.isPending && loading) {
- return loading;
+ return (
+
+ {loading}
+
+ );
}
if (controllerProps.error && error) {
- return error;
+ return (
+
+
+ {error}
+
+
+ );
}
if (
(empty &&
@@ -74,7 +84,11 @@ export const ReferenceFieldBase = <
!controllerProps.isPending &&
!controllerProps.referenceRecord)
) {
- return empty;
+ return (
+
+ {empty}
+
+ );
}
return (
diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
index c21555993d1..fe5b1b9e021 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyFieldBase.tsx
@@ -105,10 +105,20 @@ export const ReferenceManyFieldBase = <
}
if (controllerProps.isPending && loading) {
- return loading;
+ return (
+
+ {loading}
+
+ );
}
if (controllerProps.error && error) {
- return error;
+ return (
+
+
+ {error}
+
+
+ );
}
if (
// there is an empty page component
@@ -129,7 +139,11 @@ export const ReferenceManyFieldBase = <
// the user didn't set any filters
!Object.keys(controllerProps.filterValues).length
) {
- return empty;
+ return (
+
+ {empty}
+
+ );
}
return (
diff --git a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx
index 83087ab55bb..ed056b20827 100644
--- a/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceOneFieldBase.tsx
@@ -79,16 +79,30 @@ export const ReferenceOneFieldBase = <
const recordFromContext = useRecordContext(props);
if (controllerProps.isPending && loading) {
- return loading;
+ return (
+
+ {loading}
+
+ );
}
if (controllerProps.error && error) {
- return error;
+ return (
+
+
+ {error}
+
+
+ );
}
if (
!recordFromContext ||
(!controllerProps.isPending && controllerProps.referenceRecord == null)
) {
- return empty;
+ return (
+
+ {empty}
+
+ );
}
return (
From 64d280690a867c41d0da651824fe954281e9f9c1 Mon Sep 17 00:00:00 2001
From: fzaninotto
Date: Mon, 21 Jul 2025 17:13:17 +0200
Subject: [PATCH 40/41] Fix cannot override ReferenceArrayField props via theme
anymore
---
.../src/field/ReferenceArrayField.tsx | 25 ++++++++++++-------
1 file changed, 16 insertions(+), 9 deletions(-)
diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
index 6cc9b9d1d8d..bbf45fed5a3 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
@@ -78,17 +78,23 @@ import { SingleFieldList } from '../list/SingleFieldList';
export const ReferenceArrayField = <
RecordType extends RaRecord = RaRecord,
ReferenceRecordType extends RaRecord = RaRecord,
->({
- pagination,
- ...inProps
-}: ReferenceArrayFieldProps) => {
+>(
+ inProps: ReferenceArrayFieldProps
+) => {
const props = useThemeProps({
props: inProps,
name: PREFIX,
});
+ const { pagination, children, className, sx, ...controllerProps } = props;
return (
-
-
+
+
+ {children}
+
);
};
@@ -101,10 +107,11 @@ export interface ReferenceArrayFieldProps<
pagination?: React.ReactElement;
}
-export interface ReferenceArrayFieldViewProps
- extends Omit,
- Omit {
+export interface ReferenceArrayFieldViewProps {
pagination?: React.ReactElement;
+ children?: React.ReactNode;
+ className?: string;
+ sx?: SxProps;
}
export const ReferenceArrayFieldView = (
From 0aba5d4feb88a62aa8676608eaf7e88abdd17b27 Mon Sep 17 00:00:00 2001
From: fzaninotto
Date: Mon, 21 Jul 2025 17:20:27 +0200
Subject: [PATCH 41/41] Improve readability
---
.../src/field/ReferenceManyField.tsx | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
index 621c2ab3177..52cca798f45 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.tsx
@@ -62,21 +62,22 @@ export const ReferenceManyField = <
props: ReferenceManyFieldProps
) => {
const translate = useTranslate();
+ const { children, pagination, empty, ...controllerProps } = props;
return (
- {...props}
+ {...controllerProps}
empty={
- typeof props.empty === 'string' ? (
+ typeof empty === 'string' ? (
- {translate(props.empty, { _: props.empty })}
+ {translate(empty, { _: empty })}
) : (
- props.empty
+ empty
)
}
>
- {props.children}
- {!props.render && props.pagination}
+ {children}
+ {pagination}
);
};