From f1504ef21524ec467a0bda4fe32e43d592b00a5b Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 15 Jul 2025 11:06:02 +0200
Subject: [PATCH 01/25] add render prop on List component
---
packages/ra-ui-materialui/src/list/List.tsx | 5 ++-
.../ra-ui-materialui/src/list/ListView.tsx | 38 ++++++++++++++++---
2 files changed, 36 insertions(+), 7 deletions(-)
diff --git a/packages/ra-ui-materialui/src/list/List.tsx b/packages/ra-ui-materialui/src/list/List.tsx
index 23b980e1c81..7e4c0966348 100644
--- a/packages/ra-ui-materialui/src/list/List.tsx
+++ b/packages/ra-ui-materialui/src/list/List.tsx
@@ -72,6 +72,7 @@ export const List = (
resource,
sort,
storeKey,
+ render,
...rest
} = useThemeProps({
props: props,
@@ -93,13 +94,13 @@ export const List = (
sort={sort}
storeKey={storeKey}
>
- {...rest} />
+ {...rest} render={render} />
);
};
export interface ListProps
- extends Omit, 'children'>,
+ extends Omit, 'children' | 'render'>,
ListViewProps {}
const defaultFilter = {};
diff --git a/packages/ra-ui-materialui/src/list/ListView.tsx b/packages/ra-ui-materialui/src/list/ListView.tsx
index e6cd323b79f..ee28b923a1a 100644
--- a/packages/ra-ui-materialui/src/list/ListView.tsx
+++ b/packages/ra-ui-materialui/src/list/ListView.tsx
@@ -8,7 +8,7 @@ import {
import type { ReactElement, ReactNode, ElementType } from 'react';
import Card from '@mui/material/Card';
import clsx from 'clsx';
-import { useListContext, type RaRecord } from 'ra-core';
+import { ListControllerResult, useListContext, type RaRecord } from 'ra-core';
import { Title } from '../layout/Title';
import { ListToolbar } from './ListToolbar';
@@ -36,8 +36,10 @@ export const ListView = (
component: Content = DefaultComponent,
title,
empty = defaultEmpty,
+ render,
...rest
} = props;
+ const listContext = useListContext();
const {
defaultTitle,
data,
@@ -48,9 +50,9 @@ export const ListView = (
total,
hasNextPage,
hasPreviousPage,
- } = useListContext();
+ } = listContext;
- if (!children || (!data && isPending && emptyWhileLoading)) {
+ if ((!children && !render) || (!data && isPending && emptyWhileLoading)) {
return null;
}
@@ -63,7 +65,9 @@ export const ListView = (
actions={actions}
/>
)}
- {children}
+
+ {render ? render(listContext) : children}
+
{!error && pagination !== false && pagination}
);
@@ -102,7 +106,7 @@ export const ListView = (
);
};
-export interface ListViewProps {
+export interface ListViewProps {
/**
* The actions to display in the toolbar. defaults to Filter + Create + Export.
*
@@ -193,6 +197,30 @@ export interface ListViewProps {
*/
children: ReactNode;
+ /**
+ * A function rendering the list of records. Take the list controller as argument.
+ *
+ * @see https://marmelab.com/react-admin/List.html#children
+ * @example
+ * import { List } from 'react-admin';
+ *
+ * export const BookList = () => (
+ *
+ * {(listContext) =>
+ * listContext.data.map(record => (
+ *
+ *
{record.id}
+ *
{record.title}
+ *
{record.published_at}
+ *
{record.nb_views}
+ *
+ * )
+ * }
+ *
+ * );
+ */
+ render?: (props: ListControllerResult) => ReactNode;
+
/**
* The component used to display the list. Defaults to .
*
From 8be740ed315067a0b78040dd9ad7db545bebaf9d Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 15 Jul 2025 12:05:10 +0200
Subject: [PATCH 02/25] add renderprops to List component
---
docs/List.md | 30 ++++++++++++++++++-
.../ra-ui-materialui/src/list/List.spec.tsx | 14 +++++++++
.../src/list/List.stories.tsx | 29 ++++++++++++++++++
packages/ra-ui-materialui/src/list/List.tsx | 1 +
.../ra-ui-materialui/src/list/ListView.tsx | 2 +-
5 files changed, 74 insertions(+), 2 deletions(-)
diff --git a/docs/List.md b/docs/List.md
index d680c9df6db..8f752bcc67f 100644
--- a/docs/List.md
+++ b/docs/List.md
@@ -55,7 +55,8 @@ You can find more advanced examples of `` usage in the [demos](./Demos.md)
| Prop | Required | Type | Default | Description |
|---------------------------|----------|----------------|----------------|----------------------------------------------------------------------------------------------|
-| `children` | Required | `ReactNode` | - | The components rendering the list of records. |
+| `children` | Required if no render | `ReactNode` | - | The components rendering the list of records. |
+| `render` | Required if no children | `(listContext) => ReactNode` | - | A function to render the list of records. Receive the list context as its argument |
| `actions` | Optional | `ReactElement` | - | The actions to display in the toolbar. |
| `aside` | Optional | `ReactElement` | - | The component to display on the side of the list. |
| `component` | Optional | `Component` | `Card` | The component to render as the root element. |
@@ -79,6 +80,33 @@ You can find more advanced examples of `` usage in the [demos](./Demos.md)
Additional props are passed down to the root component (a MUI `` by default).
+## `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 content.
+When receiving a render prop the `` component will ignore the children property.
+
+{% raw %}
+```tsx
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+ if (error) {
+ return Error: {error.message}
;
+ }
+ return (
+ record.year}
+ />
+ );
+ }}
+/>
+```
+{% endraw %}
+
## `actions`
By default, the `` view displays a toolbar on top of the list. It contains:
diff --git a/packages/ra-ui-materialui/src/list/List.spec.tsx b/packages/ra-ui-materialui/src/list/List.spec.tsx
index a4da84e28df..093de214ff0 100644
--- a/packages/ra-ui-materialui/src/list/List.spec.tsx
+++ b/packages/ra-ui-materialui/src/list/List.spec.tsx
@@ -23,6 +23,7 @@ import {
Default,
SelectAllLimit,
Themed,
+ WithRenderProp,
} from './List.stories';
const theme = createTheme(defaultTheme);
@@ -325,6 +326,19 @@ describe('
', () => {
});
});
+ it('should render a list page using render prop', async () => {
+ render();
+ expect(screen.getByText('Loading...')).toBeDefined();
+
+ await waitFor(() => {
+ screen.getByText('1-10 of 13');
+ });
+ screen.getByText('War and Peace (1869)');
+ screen.getByText(
+ 'A historical novel that intertwines the lives of Russian aristocrats with the events of the Napoleonic wars.'
+ );
+ });
+
describe('title', () => {
it('should display by default the title of the resource', async () => {
render();
diff --git a/packages/ra-ui-materialui/src/list/List.stories.tsx b/packages/ra-ui-materialui/src/list/List.stories.tsx
index 9650ef9bf08..a02fc97cffa 100644
--- a/packages/ra-ui-materialui/src/list/List.stories.tsx
+++ b/packages/ra-ui-materialui/src/list/List.stories.tsx
@@ -844,3 +844,32 @@ export const Themed = () => (
);
+
+export const WithRenderProp = () => (
+
+
+ (
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+ if (error) {
+ return Error: {error.message}
;
+ }
+ return (
+ record.year}
+ />
+ );
+ }}
+ />
+ )}
+ />
+
+
+);
diff --git a/packages/ra-ui-materialui/src/list/List.tsx b/packages/ra-ui-materialui/src/list/List.tsx
index 7e4c0966348..448f35e8329 100644
--- a/packages/ra-ui-materialui/src/list/List.tsx
+++ b/packages/ra-ui-materialui/src/list/List.tsx
@@ -20,6 +20,7 @@ import { Loading } from '../layout';
* - actions
* - aside: Side Component
* - children: List Layout
+ * - render: alternative to children Function to render the List Layout, receive the list context as argument
* - component
* - disableAuthentication
* - disableSyncWithLocation
diff --git a/packages/ra-ui-materialui/src/list/ListView.tsx b/packages/ra-ui-materialui/src/list/ListView.tsx
index ee28b923a1a..7003280cb76 100644
--- a/packages/ra-ui-materialui/src/list/ListView.tsx
+++ b/packages/ra-ui-materialui/src/list/ListView.tsx
@@ -195,7 +195,7 @@ export interface ListViewProps {
*
* );
*/
- children: ReactNode;
+ children?: ReactNode;
/**
* A function rendering the list of records. Take the list controller as argument.
From 35c57f4cf2f55ec522556107db8e5ad4db05f37a Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 15 Jul 2025 13:58:41 +0200
Subject: [PATCH 03/25] add renderProp on edit component
---
docs/Edit.md | 28 +++++++++++++++++++
.../ra-ui-materialui/src/detail/Edit.spec.tsx | 8 ++++++
.../src/detail/Edit.stories.tsx | 21 ++++++++++++++
packages/ra-ui-materialui/src/detail/Edit.tsx | 3 +-
.../ra-ui-materialui/src/detail/EditView.tsx | 26 +++++++++++++----
5 files changed, 80 insertions(+), 6 deletions(-)
diff --git a/docs/Edit.md b/docs/Edit.md
index 6fd1b4fbf31..8320635c6ca 100644
--- a/docs/Edit.md
+++ b/docs/Edit.md
@@ -66,6 +66,7 @@ You can customize the `` component using the following props:
* [`actions`](#actions): override the actions toolbar with a custom component
* [`aside`](#aside): component to render aside to the main content
* `children`: the components that renders the form
+* `render`: a function to renders the form, receive the editContext as argument.
* `className`: passed to the root component
* [`component`](#component): override the root component
* [`disableAuthentication`](#disableauthentication): disable the authentication check
@@ -80,6 +81,33 @@ You can customize the `` component using the following props:
* [`title`](#title): override the page title
* [`transform`](#transform): transform the form data before calling `dataProvider.update()`
+## `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 content.
+When receiving a render prop the `` component will ignore the children property.
+
+{% raw %}
+```tsx
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+ if (error) {
+ return Error: {error.message}
;
+ }
+ return (
+ record.year}
+ />
+ );
+ }}
+/>
+```
+{% endraw %}
+
## `actions`
You can replace the list of default actions by your own elements using the `actions` prop:
diff --git a/packages/ra-ui-materialui/src/detail/Edit.spec.tsx b/packages/ra-ui-materialui/src/detail/Edit.spec.tsx
index 58c7fbd5845..007a989f3eb 100644
--- a/packages/ra-ui-materialui/src/detail/Edit.spec.tsx
+++ b/packages/ra-ui-materialui/src/detail/Edit.spec.tsx
@@ -29,6 +29,7 @@ import {
NotificationTranslated,
EmptyWhileLoading,
Themed,
+ WithRenderProp,
} from './Edit.stories';
describe('', () => {
@@ -340,6 +341,13 @@ describe('', () => {
});
});
+ it('should allow tu use render prop instead of children', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.queryAllByText('War and Peace')).toHaveLength(1);
+ });
+ });
+
describe('onSuccess prop', () => {
it('should allow to override the default success side effects', async () => {
const dataProvider = {
diff --git a/packages/ra-ui-materialui/src/detail/Edit.stories.tsx b/packages/ra-ui-materialui/src/detail/Edit.stories.tsx
index 7b891b6d3b4..cf509758f8d 100644
--- a/packages/ra-ui-materialui/src/detail/Edit.stories.tsx
+++ b/packages/ra-ui-materialui/src/detail/Edit.stories.tsx
@@ -397,3 +397,24 @@ export const Themed = () => (
);
+
+export const WithRenderProp = () => (
+
+
+ (
+ {
+ return editContext.record ? (
+ {editContext.record.title}
+ ) : null;
+ }}
+ >
+
+
+ )}
+ />
+
+
+);
diff --git a/packages/ra-ui-materialui/src/detail/Edit.tsx b/packages/ra-ui-materialui/src/detail/Edit.tsx
index be155172461..6dd40f61b27 100644
--- a/packages/ra-ui-materialui/src/detail/Edit.tsx
+++ b/packages/ra-ui-materialui/src/detail/Edit.tsx
@@ -76,6 +76,7 @@ export const Edit = (
loading = defaultLoading,
...rest
} = props;
+
return (
resource={resource}
@@ -95,7 +96,7 @@ export const Edit = (
export interface EditProps
extends EditBaseProps,
- Omit {}
+ Omit {}
const defaultLoading = ;
diff --git a/packages/ra-ui-materialui/src/detail/EditView.tsx b/packages/ra-ui-materialui/src/detail/EditView.tsx
index 26cbbae0eec..f90d008bb26 100644
--- a/packages/ra-ui-materialui/src/detail/EditView.tsx
+++ b/packages/ra-ui-materialui/src/detail/EditView.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import type { ReactElement, ElementType } from 'react';
+import type { ReactElement, ElementType, ReactNode } from 'react';
import {
Card,
CardContent,
@@ -9,7 +9,11 @@ import {
type Theme,
} from '@mui/material';
import clsx from 'clsx';
-import { useEditContext, useResourceDefinition } from 'ra-core';
+import {
+ EditControllerResult,
+ useEditContext,
+ useResourceDefinition,
+} from 'ra-core';
import { EditActions } from './EditActions';
import { Title } from '../layout';
@@ -22,6 +26,7 @@ export const EditView = (props: EditViewProps) => {
actions,
aside,
children,
+ render,
className,
component: Content = Card,
emptyWhileLoading = false,
@@ -30,11 +35,13 @@ export const EditView = (props: EditViewProps) => {
} = props;
const { hasShow } = useResourceDefinition();
- const { resource, defaultTitle, record, isPending } = useEditContext();
+ const editContext = useEditContext();
+
+ const { resource, defaultTitle, record, isPending } = editContext;
const finalActions =
typeof actions === 'undefined' && hasShow ? defaultActions : actions;
- if (!children || (!record && isPending && emptyWhileLoading)) {
+ if ((!children && !render) || (!record && isPending && emptyWhileLoading)) {
return null;
}
@@ -54,7 +61,15 @@ export const EditView = (props: EditViewProps) => {
})}
>
- {record ? children : }
+ {record ? (
+ render ? (
+ render(editContext)
+ ) : (
+ children
+ )
+ ) : (
+
+ )}
{aside}
@@ -70,6 +85,7 @@ export interface EditViewProps
emptyWhileLoading?: boolean;
title?: string | ReactElement | false;
sx?: SxProps;
+ render?: (editContext: EditControllerResult) => ReactNode;
}
const PREFIX = 'RaEdit';
From add36d1b055a1432be8039b2f8c33d18dde0b18e Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 15 Jul 2025 14:02:25 +0200
Subject: [PATCH 04/25] fix type on InfiniteList
---
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 20beac31d58..6830d83df9f 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 Omit, 'children'>,
+ extends Omit, 'children' | 'render'>,
ListViewProps {}
const PREFIX = 'RaInfiniteList';
From 1b0807020a6ceca5eac8af9198191981c14ea346 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 15 Jul 2025 14:05:35 +0200
Subject: [PATCH 05/25] update Edit comment
---
packages/ra-ui-materialui/src/detail/Edit.tsx | 2 ++
1 file changed, 2 insertions(+)
diff --git a/packages/ra-ui-materialui/src/detail/Edit.tsx b/packages/ra-ui-materialui/src/detail/Edit.tsx
index 6dd40f61b27..2d71d031848 100644
--- a/packages/ra-ui-materialui/src/detail/Edit.tsx
+++ b/packages/ra-ui-materialui/src/detail/Edit.tsx
@@ -21,6 +21,8 @@ import { Loading } from '../layout';
*
* The component accepts the following props:
*
+ * - children: Component rendering the Form Layout
+ * - render: Alternative to children. A function to render the Form Layout. Receives the edit context as its argument.
* - actions
* - aside
* - component
From e0f44aeb56352bef328611df0bc8143223d40ae4 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 15 Jul 2025 14:45:05 +0200
Subject: [PATCH 06/25] add render prop to Create
---
docs/Create.md | 31 ++++++++++++++++
docs/Edit.md | 36 +++++++++++++------
docs/List.md | 2 +-
.../src/detail/Create.spec.tsx | 10 +++++-
.../src/detail/Create.stories.tsx | 19 ++++++++++
.../ra-ui-materialui/src/detail/Create.tsx | 17 +++++++--
.../src/detail/CreateView.tsx | 15 +++++---
packages/ra-ui-materialui/src/detail/Edit.tsx | 7 +++-
.../ra-ui-materialui/src/detail/EditView.tsx | 3 +-
packages/ra-ui-materialui/src/list/List.tsx | 6 ++++
10 files changed, 124 insertions(+), 22 deletions(-)
diff --git a/docs/Create.md b/docs/Create.md
index 26491acb05e..67428208f6b 100644
--- a/docs/Create.md
+++ b/docs/Create.md
@@ -58,6 +58,7 @@ You can customize the `` component using the following props:
* [`actions`](#actions): override the actions toolbar with a custom component
* [`aside`](#aside): component to render aside to the main content
* `children`: the components that renders the form
+* `render`: Alternative to children. A function that renders the form, receive the create context as its argument
* `className`: passed to the root component
* [`component`](#component): override the root component
* [`disableAuthentication`](#disableauthentication): disable the authentication check
@@ -70,6 +71,36 @@ You can customize the `` component using the following props:
* [`title`](#title): override the page title
* [`transform`](#transform): transform the form data before calling `dataProvider.create()`
+## `render`
+
+Alternatively to children you can pass a render prop to ``. The render prop will receive the create context as its argument, allowing to inline the render logic for the create form.
+When receiving a render prop the `` component will ignore the children property.
+
+{% raw %}
+```tsx
+ {
+ if (createContext.isPending) {
+ return Loading...
;
+ }
+ if (createContext.error) {
+ return Error: {error.message}
;
+ }
+
+ return (
+
+ {`Create new ${createContext.resource}`}
+
+
+
+
+
+
+
+ );
+}} />
+```
+{% endraw %}
+
## `actions`
You can replace the list of default actions by your own elements using the `actions` prop:
diff --git a/docs/Edit.md b/docs/Edit.md
index 8320635c6ca..912d02d2f9a 100644
--- a/docs/Edit.md
+++ b/docs/Edit.md
@@ -83,25 +83,39 @@ You can customize the `` component using the following props:
## `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 content.
-When receiving a render prop the `` component will ignore the children property.
+Alternatively to children you can pass a render prop to ``. The render prop will receive the edit context as its argument, allowing to inline the render logic for the edit form.
+When receiving a render prop the `` component will ignore the children property.
{% raw %}
```tsx
- {
- if (isPending) {
+ {
+ if (listContext.isPending) {
return Loading...
;
}
- if (error) {
+ if (listContext.error) {
return Error: {error.message}
;
}
return (
- record.year}
- />
+
+
{`Edit ${listController.resource} #${listController.record.id}`}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}}
/>
diff --git a/docs/List.md b/docs/List.md
index 8f752bcc67f..8daf9dab568 100644
--- a/docs/List.md
+++ b/docs/List.md
@@ -82,7 +82,7 @@ Additional props are passed down to the root component (a MUI `` by defaul
## `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 content.
+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 the list content.
When receiving a render prop the `` component will ignore the children property.
{% raw %}
diff --git a/packages/ra-ui-materialui/src/detail/Create.spec.tsx b/packages/ra-ui-materialui/src/detail/Create.spec.tsx
index abffa876427..e786c710185 100644
--- a/packages/ra-ui-materialui/src/detail/Create.spec.tsx
+++ b/packages/ra-ui-materialui/src/detail/Create.spec.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import expect from 'expect';
import { CoreAdminContext, testDataProvider } from 'ra-core';
-import { screen, render } from '@testing-library/react';
+import { screen, render, waitFor } from '@testing-library/react';
import { Create } from './Create';
import {
@@ -12,6 +12,7 @@ import {
NotificationDefault,
NotificationTranslated,
Themed,
+ WithRenderProp,
} from './Create.stories';
describe('', () => {
@@ -57,6 +58,13 @@ describe('', () => {
);
});
+ it('should support a render prop', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.queryByText('Create new books')).not.toBeNull();
+ });
+ });
+
describe('title', () => {
it('should display by default the title of the resource', async () => {
render();
diff --git a/packages/ra-ui-materialui/src/detail/Create.stories.tsx b/packages/ra-ui-materialui/src/detail/Create.stories.tsx
index 6285d68b774..bab3d292015 100644
--- a/packages/ra-ui-materialui/src/detail/Create.stories.tsx
+++ b/packages/ra-ui-materialui/src/detail/Create.stories.tsx
@@ -305,3 +305,22 @@ export const Themed = () => (
);
+
+export const WithRenderProp = () => (
+
+
+ (
+ {
+ return (
+ {`Create new ${createContext.resource}`}
+ );
+ }}
+ />
+ )}
+ />
+
+
+);
diff --git a/packages/ra-ui-materialui/src/detail/Create.tsx b/packages/ra-ui-materialui/src/detail/Create.tsx
index 34468e938f6..0275b462d82 100644
--- a/packages/ra-ui-materialui/src/detail/Create.tsx
+++ b/packages/ra-ui-materialui/src/detail/Create.tsx
@@ -22,6 +22,8 @@ import { Loading } from '../layout';
*
* The component accepts the following props:
*
+ * - children: Component rendering the Form Layout
+ * - render: Alternative to children. A function to render the Form Layout. Receives the create context as its argument.
* - actions
* - aside
* - component
@@ -66,7 +68,6 @@ export const Create = <
name: PREFIX,
});
- useCheckMinimumRequiredProps('Create', ['children'], props);
const {
resource,
record,
@@ -80,6 +81,13 @@ export const Create = <
loading = defaultLoading,
...rest
} = props;
+
+ if (!props.render && !props.children) {
+ throw new Error(
+ ' requires either a `render` prop or `children` prop'
+ );
+ }
+
return (
resource={resource}
@@ -102,8 +110,11 @@ export interface CreateProps<
RecordType extends Omit = any,
MutationOptionsError = Error,
ResultRecordType extends RaRecord = RecordType & { id: Identifier },
-> extends CreateBaseProps,
- Omit {}
+> extends Omit<
+ CreateBaseProps,
+ 'children' | 'render'
+ >,
+ CreateViewProps {}
const defaultLoading = ;
diff --git a/packages/ra-ui-materialui/src/detail/CreateView.tsx b/packages/ra-ui-materialui/src/detail/CreateView.tsx
index d0ca5b126b1..56ff926e2c7 100644
--- a/packages/ra-ui-materialui/src/detail/CreateView.tsx
+++ b/packages/ra-ui-materialui/src/detail/CreateView.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import type { ElementType, ReactElement } from 'react';
+import type { ElementType, ReactElement, ReactNode } from 'react';
import {
Card,
type ComponentsOverrides,
@@ -7,7 +7,7 @@ import {
type SxProps,
type Theme,
} from '@mui/material';
-import { useCreateContext } from 'ra-core';
+import { CreateControllerResult, useCreateContext } from 'ra-core';
import clsx from 'clsx';
import { Title } from '../layout';
@@ -18,13 +18,16 @@ export const CreateView = (props: CreateViewProps) => {
actions,
aside,
children,
+ render,
className,
component: Content = Card,
title,
...rest
} = props;
- const { resource, defaultTitle } = useCreateContext();
+ const createContext = useCreateContext();
+
+ const { resource, defaultTitle } = createContext;
return (
@@ -41,7 +44,9 @@ export const CreateView = (props: CreateViewProps) => {
[CreateClasses.noActions]: !actions,
})}
>
- {children}
+
+ {render ? render(createContext) : children}
+
{aside}
@@ -55,6 +60,8 @@ export interface CreateViewProps
component?: ElementType;
sx?: SxProps;
title?: string | ReactElement | false;
+ render?: (createContext: CreateControllerResult) => ReactNode;
+ children?: ReactNode;
}
const PREFIX = 'RaCreate';
diff --git a/packages/ra-ui-materialui/src/detail/Edit.tsx b/packages/ra-ui-materialui/src/detail/Edit.tsx
index 2d71d031848..70a7f587b9f 100644
--- a/packages/ra-ui-materialui/src/detail/Edit.tsx
+++ b/packages/ra-ui-materialui/src/detail/Edit.tsx
@@ -65,7 +65,6 @@ export const Edit = (
name: PREFIX,
});
- useCheckMinimumRequiredProps('Edit', ['children'], props);
const {
resource,
id,
@@ -79,6 +78,12 @@ export const Edit = (
...rest
} = props;
+ if (!props.render && !props.children) {
+ throw new Error(
+ ' requires either a `render` prop or `children` prop'
+ );
+ }
+
return (
resource={resource}
diff --git a/packages/ra-ui-materialui/src/detail/EditView.tsx b/packages/ra-ui-materialui/src/detail/EditView.tsx
index f90d008bb26..929bc31eaae 100644
--- a/packages/ra-ui-materialui/src/detail/EditView.tsx
+++ b/packages/ra-ui-materialui/src/detail/EditView.tsx
@@ -41,7 +41,8 @@ export const EditView = (props: EditViewProps) => {
const finalActions =
typeof actions === 'undefined' && hasShow ? defaultActions : actions;
- if ((!children && !render) || (!record && isPending && emptyWhileLoading)) {
+
+ if (!record && isPending && emptyWhileLoading) {
return null;
}
diff --git a/packages/ra-ui-materialui/src/list/List.tsx b/packages/ra-ui-materialui/src/list/List.tsx
index 448f35e8329..e42d07dfed0 100644
--- a/packages/ra-ui-materialui/src/list/List.tsx
+++ b/packages/ra-ui-materialui/src/list/List.tsx
@@ -80,6 +80,12 @@ export const List = (
name: PREFIX,
});
+ if (!props.render && !props.children) {
+ throw new Error(
+ ' requires either a `render` prop or `children` prop'
+ );
+ }
+
return (
debounce={debounce}
From d45d592240dab96924cc8d4901071d2596f35760 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 15 Jul 2025 14:52:09 +0200
Subject: [PATCH 07/25] add render prop on Show
---
.../src/detail/Show.stories.tsx | 19 +++++++++++++++++++
packages/ra-ui-materialui/src/detail/Show.tsx | 8 +++++++-
.../ra-ui-materialui/src/detail/ShowView.tsx | 13 ++++++++++---
3 files changed, 36 insertions(+), 4 deletions(-)
diff --git a/packages/ra-ui-materialui/src/detail/Show.stories.tsx b/packages/ra-ui-materialui/src/detail/Show.stories.tsx
index 46a545f3a74..3fdee242f49 100644
--- a/packages/ra-ui-materialui/src/detail/Show.stories.tsx
+++ b/packages/ra-ui-materialui/src/detail/Show.stories.tsx
@@ -280,3 +280,22 @@ export const Themed = () => (
);
+
+export const WithRenderProp = () => (
+
+
+ (
+
+ showContext.record ? (
+ {showContext.record.title}
+ ) : null
+ }
+ />
+ )}
+ />
+
+
+);
diff --git a/packages/ra-ui-materialui/src/detail/Show.tsx b/packages/ra-ui-materialui/src/detail/Show.tsx
index 850bdfe0d2c..6d1755454fc 100644
--- a/packages/ra-ui-materialui/src/detail/Show.tsx
+++ b/packages/ra-ui-materialui/src/detail/Show.tsx
@@ -75,6 +75,12 @@ export const Show = (
...rest
} = props;
+ if (!props.render && !props.children) {
+ throw new Error(
+ ' requires either a `render` prop or `children` prop'
+ );
+ }
+
return (
id={id}
@@ -90,7 +96,7 @@ export const Show = (
export interface ShowProps
extends ShowBaseProps,
- Omit {}
+ Omit {}
const defaultLoading = ;
diff --git a/packages/ra-ui-materialui/src/detail/ShowView.tsx b/packages/ra-ui-materialui/src/detail/ShowView.tsx
index 844f6a1e7cc..6f9d1c3ec60 100644
--- a/packages/ra-ui-materialui/src/detail/ShowView.tsx
+++ b/packages/ra-ui-materialui/src/detail/ShowView.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import type { ReactElement, ElementType } from 'react';
+import type { ReactElement, ElementType, ReactNode } from 'react';
import {
Card,
type ComponentsOverrides,
@@ -8,7 +8,11 @@ import {
type Theme,
} from '@mui/material';
import clsx from 'clsx';
-import { useShowContext, useResourceDefinition } from 'ra-core';
+import {
+ useShowContext,
+ useResourceDefinition,
+ ShowControllerResult,
+} from 'ra-core';
import { ShowActions } from './ShowActions';
import { Title } from '../layout';
import { ShowProps } from './Show';
@@ -20,6 +24,7 @@ export const ShowView = (props: ShowViewProps) => {
actions,
aside,
children,
+ render,
className,
component: Content = Card,
emptyWhileLoading = false,
@@ -27,7 +32,8 @@ export const ShowView = (props: ShowViewProps) => {
...rest
} = props;
- const { resource, defaultTitle, record } = useShowContext();
+ const showContext = useShowContext();
+ const { resource, defaultTitle, record } = showContext;
const { hasEdit } = useResourceDefinition();
const finalActions =
@@ -66,6 +72,7 @@ export interface ShowViewProps
emptyWhileLoading?: boolean;
title?: string | ReactElement | false;
sx?: SxProps;
+ render?: (showContext: ShowControllerResult) => ReactNode;
}
const PREFIX = 'RaShow';
From a94303fe5778c51472bb4f68e5a86ff46f6a05e9 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 15 Jul 2025 15:13:04 +0200
Subject: [PATCH 08/25] add render prop on Show
---
docs/Show.md | 3 ++-
packages/ra-ui-materialui/src/detail/Edit.spec.tsx | 2 +-
packages/ra-ui-materialui/src/detail/Show.spec.tsx | 10 +++++++++-
packages/ra-ui-materialui/src/detail/Show.tsx | 2 ++
packages/ra-ui-materialui/src/detail/ShowView.tsx | 6 ++++--
5 files changed, 18 insertions(+), 5 deletions(-)
diff --git a/docs/Show.md b/docs/Show.md
index 580359356a9..850ec020be0 100644
--- a/docs/Show.md
+++ b/docs/Show.md
@@ -61,7 +61,8 @@ That's enough to display the post show view above.
| Prop | Required | Type | Default | Description
|------------------|----------|-------------------|---------|--------------------------------------------------------
-| `children` | Required | `ReactNode` | | The components rendering the record fields
+| `children` | Required if no render | `ReactNode` | | The components rendering the record fields
+| `render` | Required if no children | `(showContext) => ReactNode` | | A function rendering the record fields, receive the show context as its argument
| `actions` | Optional | `ReactElement` | | The actions to display in the toolbar
| `aside` | Optional | `ReactElement` | | The component to display on the side of the list
| `className` | Optional | `string` | | passed to the root component
diff --git a/packages/ra-ui-materialui/src/detail/Edit.spec.tsx b/packages/ra-ui-materialui/src/detail/Edit.spec.tsx
index 007a989f3eb..ddfcaf09563 100644
--- a/packages/ra-ui-materialui/src/detail/Edit.spec.tsx
+++ b/packages/ra-ui-materialui/src/detail/Edit.spec.tsx
@@ -341,7 +341,7 @@ describe('', () => {
});
});
- it('should allow tu use render prop instead of children', async () => {
+ it('should allow to use render prop instead of children', async () => {
render();
await waitFor(() => {
expect(screen.queryAllByText('War and Peace')).toHaveLength(1);
diff --git a/packages/ra-ui-materialui/src/detail/Show.spec.tsx b/packages/ra-ui-materialui/src/detail/Show.spec.tsx
index 64c3a0a475f..88db2e52036 100644
--- a/packages/ra-ui-materialui/src/detail/Show.spec.tsx
+++ b/packages/ra-ui-materialui/src/detail/Show.spec.tsx
@@ -25,6 +25,7 @@ import {
TitleFalse,
TitleElement,
Themed,
+ WithRenderProp,
} from './Show.stories';
import { Show } from './Show';
@@ -132,11 +133,18 @@ describe('', () => {
it('should be customized by a theme', async () => {
render();
- expect(screen.queryByTestId('themed-view').classList).toContain(
+ expect(screen.queryByTestId('themed-view')?.classList).toContain(
'custom-class'
);
});
+ it('should allow to use render prop instead of children', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.queryByText('War and Peace')).not.toBeNull();
+ });
+ });
+
describe('title', () => {
it('should display by default the title of the resource', async () => {
render();
diff --git a/packages/ra-ui-materialui/src/detail/Show.tsx b/packages/ra-ui-materialui/src/detail/Show.tsx
index 6d1755454fc..9a97860242c 100644
--- a/packages/ra-ui-materialui/src/detail/Show.tsx
+++ b/packages/ra-ui-materialui/src/detail/Show.tsx
@@ -45,7 +45,9 @@ import { Loading } from '../layout';
* );
* export default App;
*
+ * @typedef {(showContext: Object) => ReactNode} RenderProp
* @param {ShowProps} inProps
+ * @param {RenderProp} inProps.render A function rendering the page content, receive the show context as its argument.
* @param {ReactElement|false} inProps.actions An element to display above the page content, or false to disable actions.
* @param {string} inProps.className A className to apply to the page content.
* @param {ElementType} inProps.component The component to use as root component (div by default).
diff --git a/packages/ra-ui-materialui/src/detail/ShowView.tsx b/packages/ra-ui-materialui/src/detail/ShowView.tsx
index 6f9d1c3ec60..5028186e685 100644
--- a/packages/ra-ui-materialui/src/detail/ShowView.tsx
+++ b/packages/ra-ui-materialui/src/detail/ShowView.tsx
@@ -39,7 +39,7 @@ export const ShowView = (props: ShowViewProps) => {
const finalActions =
typeof actions === 'undefined' && hasEdit ? defaultActions : actions;
- if (!children || (!record && emptyWhileLoading)) {
+ if (!record && emptyWhileLoading) {
return null;
}
return (
@@ -57,7 +57,9 @@ export const ShowView = (props: ShowViewProps) => {
[ShowClasses.noActions]: !finalActions,
})}
>
- {children}
+
+ {render ? render(showContext) : children}
+
{aside}
From d6d0b10461fe0452474de53c8d9e52008549b2db Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 15 Jul 2025 15:41:26 +0200
Subject: [PATCH 09/25] add render prop on InfiniteList
---
docs/InfiniteList.md | 32 +++++++++++++-
.../src/list/InfiniteList.spec.tsx | 28 +++++++++++-
.../src/list/InfiniteList.stories.tsx | 44 +++++++++++++++++++
.../src/list/InfiniteList.tsx | 6 +++
4 files changed, 106 insertions(+), 4 deletions(-)
diff --git a/docs/InfiniteList.md b/docs/InfiniteList.md
index be70505ea75..671245769b3 100644
--- a/docs/InfiniteList.md
+++ b/docs/InfiniteList.md
@@ -58,9 +58,10 @@ The props are the same as [the `` component](./List.md):
| Prop | Required | Type | Default | Description |
|----------------------------|----------|----------------|-------------------------|----------------------------------------------------------------------------------------------|
-| `children` | Required | `ReactNode` | - | The component to use to render the list of records. |
+| `children` | Required if no render | `ReactNode` | - | The component to use to render the list of records. |
+| `render` | Required if no children | `ReactNode` | - | A function that render the list of records, receives the list context as argument. |
| `actions` | Optional | `ReactElement` | - | The actions to display in the toolbar. |
-| `aside` | Optional | `ReactElement` | - | The component to display on the side of the list. |
+| `aside` | Optional | `(listContext) => ReactElement` | - | The component to display on the side of the list. |
| `component` | Optional | `Component` | `Card` | The component to render as the root element. |
| `debounce` | Optional | `number` | `500` | The debounce delay in milliseconds to apply when users change the sort or filter parameters. |
| `disable Authentication` | Optional | `boolean` | `false` | Set to `true` to disable the authentication check. |
@@ -84,6 +85,33 @@ Check the [`` component](./List.md) for details about each prop.
Additional props are passed down to the root component (a MUI `` by default).
+## `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 the list content.
+When receiving a render prop the `` component will ignore the children property.
+
+{% raw %}
+```tsx
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+ if (error) {
+ return Error: {error.message}
;
+ }
+ return (
+ record.year}
+ />
+ );
+ }}
+/>
+```
+{% endraw %}
+
## `pagination`
You can replace the default "load on scroll" pagination (triggered by a component named ``) by a custom pagination component. To get the pagination state and callbacks, you'll need to read the `InfinitePaginationContext`.
diff --git a/packages/ra-ui-materialui/src/list/InfiniteList.spec.tsx b/packages/ra-ui-materialui/src/list/InfiniteList.spec.tsx
index acece3dd609..fcd310d2d80 100644
--- a/packages/ra-ui-materialui/src/list/InfiniteList.spec.tsx
+++ b/packages/ra-ui-materialui/src/list/InfiniteList.spec.tsx
@@ -1,14 +1,38 @@
import * as React from 'react';
import expect from 'expect';
-import { render, screen } from '@testing-library/react';
+import { render, screen, waitFor } from '@testing-library/react';
-import { Themed } from './InfiniteList.stories';
+import { Themed, WithRenderProp } from './InfiniteList.stories';
describe('', () => {
+ let originalIntersectionObserver;
+ beforeAll(() => {
+ originalIntersectionObserver = window.IntersectionObserver;
+ const intersectionObserverMock = () => ({
+ observe: () => null,
+ unobserve: () => null,
+ });
+ window.IntersectionObserver = jest
+ .fn()
+ .mockImplementation(intersectionObserverMock);
+ });
+ afterAll(() => {
+ window.IntersectionObserver = originalIntersectionObserver;
+ });
+
it('should be customized by a theme', async () => {
render();
expect(screen.queryByTestId('themed-list').classList).toContain(
'custom-class'
);
});
+ it('should render a list page using render prop', async () => {
+ render();
+ expect(screen.getByText('Loading...')).toBeDefined();
+
+ await waitFor(() => {
+ screen.getByText('War and Peace');
+ screen.getByText('Leo Tolstoy');
+ });
+ });
});
diff --git a/packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx b/packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx
index 4b638d80e0e..7aeba9b2dd1 100644
--- a/packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx
+++ b/packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx
@@ -478,3 +478,47 @@ export const Themed = () => (
/>
);
+
+export const WithRenderProp = () => (
+
+ dataProvider
+ .getList(resource, params)
+ .then(({ data, total }) => ({
+ data,
+ pageInfo: {
+ hasNextPage:
+ total! >
+ params.pagination.page *
+ params.pagination.perPage,
+ hasPreviousPage: params.pagination.page > 1,
+ },
+ })),
+ }}
+ >
+ (
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+ if (error) {
+ return Error: {error.message}
;
+ }
+ return (
+ record.year}
+ />
+ );
+ }}
+ />
+ )}
+ />
+
+);
diff --git a/packages/ra-ui-materialui/src/list/InfiniteList.tsx b/packages/ra-ui-materialui/src/list/InfiniteList.tsx
index 6830d83df9f..54248d269e7 100644
--- a/packages/ra-ui-materialui/src/list/InfiniteList.tsx
+++ b/packages/ra-ui-materialui/src/list/InfiniteList.tsx
@@ -83,6 +83,12 @@ export const InfiniteList = (
name: PREFIX,
});
+ if (!props.render && !props.children) {
+ throw new Error(
+ ' requires either a `render` prop or `children` prop'
+ );
+ }
+
return (
debounce={debounce}
From 36a20c6d626f9c366824acf2382259293778f28c Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 15 Jul 2025 17:27:01 +0200
Subject: [PATCH 10/25] add render prop on ReferenceField
---
docs/ReferenceField.md | 24 +++++++++++
.../src/field/ReferenceField.spec.tsx | 40 +++++++++++++++++++
.../src/field/ReferenceField.stories.tsx | 20 ++++++++++
.../src/field/ReferenceField.tsx | 33 +++++++++++----
4 files changed, 110 insertions(+), 7 deletions(-)
diff --git a/docs/ReferenceField.md b/docs/ReferenceField.md
index 256c1174919..6f2818fa431 100644
--- a/docs/ReferenceField.md
+++ b/docs/ReferenceField.md
@@ -75,6 +75,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` | - | One or more Field elements used to render the referenced record |
+| `render` | Optional | (referenceFieldContext) => `ReactNode` | - | A function used to render the referenced record, receive the reference field context as its argument |
| `empty` | Optional | `ReactNode` | - | What to render when the field has no value or when the reference is missing |
| `label` | Optional | `string | Function` | `resources. [resource]. fields.[source]` | Label to use for the field when rendered in layout components |
| `link` | Optional | `string | Function` | `edit` | Target of the link wrapping the rendered child. Set to `false` to disable the link. |
@@ -83,6 +84,29 @@ It uses `dataProvider.getMany()` instead of `dataProvider.getOne()` [for perform
`` also accepts the [common field props](./Fields.md#common-field-props).
+## `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.
+
+```jsx
+export const MyReferenceField = () => (
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return (
+
+ {error.message}
+
+ );
+ }
+ return {referenceRecord.name}
;
+ }} />
+);
+```
+
## `empty`
`` can display a custom message when the referenced record is missing, thanks to the `empty` prop.
diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx
index 00e483d2c88..59a60eeee5f 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceField.spec.tsx
@@ -506,6 +506,46 @@ describe('', () => {
await screen.findByText('boo');
});
+ it('should render its child using render prop when given', async () => {
+ const dataProvider = testDataProvider({
+ getMany: jest.fn().mockResolvedValue({
+ data: [{ id: 123, title: 'foo' }],
+ }),
+ });
+ render(
+
+
+
+
+
+ referenceRecord?.title || 'No title'
+ }
+ />
+
+
+
+
+ );
+ await new Promise(resolve => setTimeout(resolve, 10));
+ expect(screen.queryByRole('progressbar')).toBeNull();
+ expect(screen.getByText('foo')).not.toBeNull();
+ expect(screen.queryAllByRole('link')).toHaveLength(1);
+ expect(screen.queryByRole('link')?.getAttribute('href')).toBe(
+ '#/posts/123'
+ );
+ });
+
describe('link', () => {
it('should render a link to specified link type', async () => {
render();
diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.stories.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.stories.tsx
index 8d013fe7b4f..36196f3df14 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceField.stories.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceField.stories.tsx
@@ -951,3 +951,23 @@ export const Themed = () => {
);
};
+
+export const WithRenderProp = () => (
+
+ {
+ console.log({ error, isPending, referenceRecord });
+ if (isPending) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return {error.message}
;
+ }
+ return referenceRecord.ISBN;
+ }}
+ >
+
+);
diff --git a/packages/ra-ui-materialui/src/field/ReferenceField.tsx b/packages/ra-ui-materialui/src/field/ReferenceField.tsx
index 2afe09b9e8d..1f256a694f0 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceField.tsx
@@ -16,6 +16,7 @@ import {
type RaRecord,
ReferenceFieldBase,
useReferenceFieldContext,
+ UseReferenceFieldControllerResult,
} from 'ra-core';
import type { UseQueryOptions } from '@tanstack/react-query';
import clsx from 'clsx';
@@ -40,6 +41,14 @@ import { visuallyHidden } from '@mui/utils';
*
*
*
+ * @example // using a render prop to render the record
+ * (
+ * {context.referenceRecord?.name}
+ * )
+ * }>
+ *
+ *
* @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.
@@ -60,9 +69,11 @@ import { visuallyHidden } from '@mui/utils';
export const ReferenceField = <
RecordType extends Record = Record,
ReferenceRecordType extends RaRecord = RaRecord,
->(
- inProps: ReferenceFieldProps
-) => {
+>({
+ children,
+ render,
+ ...inProps
+}: ReferenceFieldProps) => {
const props = useThemeProps({
props: inProps,
name: PREFIX,
@@ -89,6 +100,8 @@ export const ReferenceField = <
>
{...props}
+ render={render}
+ children={children}
/>
);
@@ -99,6 +112,9 @@ export interface ReferenceFieldProps<
ReferenceRecordType extends RaRecord = RaRecord,
> extends FieldProps {
children?: ReactNode;
+ render?: (
+ context: UseReferenceFieldControllerResult
+ ) => ReactNode;
/**
* @deprecated Use the empty prop instead
*/
@@ -123,13 +139,13 @@ export const ReferenceFieldView = <
>(
props: ReferenceFieldViewProps
) => {
- const { children, className, emptyText, reference, sx, ...rest } =
+ const { children, render, className, emptyText, reference, sx, ...rest } =
useThemeProps({
props: props,
name: PREFIX,
});
- const { error, link, isLoading, referenceRecord } =
- useReferenceFieldContext();
+ const referenceFieldContext = useReferenceFieldContext();
+ const { error, link, isLoading, referenceRecord } = referenceFieldContext;
const getRecordRepresentation = useGetRecordRepresentation(reference);
@@ -150,7 +166,7 @@ export const ReferenceFieldView = <
return ;
}
- const child = children || (
+ const child = (render ? render(referenceFieldContext) : children) || (
{getRecordRepresentation(referenceRecord)}
@@ -192,6 +208,9 @@ export interface ReferenceFieldViewProps<
> extends FieldProps,
Omit, 'link'> {
children?: ReactNode;
+ render?: (
+ context: UseReferenceFieldControllerResult
+ ) => ReactNode;
reference: string;
resource?: string;
translateChoice?: Function | boolean;
From 8398c08efeadf2db327c939abd287982c43914c1 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 15 Jul 2025 17:59:15 +0200
Subject: [PATCH 11/25] document and test render prop on ReferenceManyField
---
docs/ReferenceManyField.md | 68 ++++++++++++++++++-
.../src/field/ReferenceManyField.spec.tsx | 15 ++++
.../src/field/ReferenceManyField.stories.tsx | 27 ++++++++
3 files changed, 109 insertions(+), 1 deletion(-)
diff --git a/docs/ReferenceManyField.md b/docs/ReferenceManyField.md
index 15f6257cafd..a6a5f25c6be 100644
--- a/docs/ReferenceManyField.md
+++ b/docs/ReferenceManyField.md
@@ -89,7 +89,8 @@ This example leverages [``](./SingleFieldList.md) to display an
| Prop | Required | Type | Default | Description |
| -------------- | -------- | --------------------------------------------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------- |
-| `children` | Required | `Element` | - | One or several elements 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. |
| `filter` | Optional | `Object` | - | Filters to use when fetching the related records, passed to `getManyReference()` |
@@ -163,6 +164,71 @@ 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.
+
+```jsx
+import { Show, SimpleShowLayout, ReferenceManyField, 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.
diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx
index db966fcb3f0..1f9c052e586 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.spec.tsx
@@ -14,6 +14,7 @@ import {
Empty,
WithPagination,
WithPaginationAndSelectAllLimit,
+ WithRenderProp,
} from './ReferenceManyField.stories';
const theme = createTheme();
@@ -206,6 +207,20 @@ describe('', () => {
});
});
+ it('should use render prop when provides', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.queryAllByRole('progressbar')).toHaveLength(0);
+ });
+ const items = await screen.findAllByRole('listitem');
+ expect(items).toHaveLength(5);
+ expect(items[0].textContent).toEqual('War and Peace');
+ expect(items[1].textContent).toEqual('Anna Karenina');
+ expect(items[2].textContent).toEqual('Resurrection');
+ expect(items[3].textContent).toEqual('The Idiot');
+ expect(items[4].textContent).toEqual('The Last Day of a Condemned');
+ });
+
describe('pagination', () => {
it('should render pagination based on total from getManyReference', async () => {
const data = [
diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyField.stories.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyField.stories.tsx
index 9b45707ea22..eadc8c64fd1 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceManyField.stories.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceManyField.stories.tsx
@@ -286,3 +286,30 @@ export const FullApp = () => (
);
+
+export const WithRenderProp = () => (
+
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return {error.message}
;
+ }
+ return (
+ <>
+ {data?.map((datum, index) => (
+
+ {datum.title}
+
+ ))}
+ >
+ );
+ }}
+ />
+
+);
From 90b5ca655ed3f24b9f27fef37a0b6e2cc054c4ac Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Tue, 15 Jul 2025 18:14:19 +0200
Subject: [PATCH 12/25] add render prop on ReferenceArrayField (remove
pagination from ReferenceArrayFieldBase)
---
docs/ReferenceArrayField.md | 33 +++++++++++++++++
docs/ReferenceArrayFieldBase.md | 1 +
.../src/field/ReferenceArrayField.spec.tsx | 15 ++++++++
.../src/field/ReferenceArrayField.stories.tsx | 37 +++++++++++++++++++
.../src/field/ReferenceArrayField.tsx | 17 +++++++--
5 files changed, 99 insertions(+), 4 deletions(-)
diff --git a/docs/ReferenceArrayField.md b/docs/ReferenceArrayField.md
index c9f1a789a76..fa1494d4d5a 100644
--- a/docs/ReferenceArrayField.md
+++ b/docs/ReferenceArrayField.md
@@ -86,6 +86,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` | 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 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 |
@@ -179,6 +180,38 @@ export const PostShow = () => (
);
```
+
+## `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 .
+When receiving a render prop the `` component will ignore the children 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).
diff --git a/docs/ReferenceArrayFieldBase.md b/docs/ReferenceArrayFieldBase.md
index 789def03769..4323f4bcfc9 100644
--- a/docs/ReferenceArrayFieldBase.md
+++ b/docs/ReferenceArrayFieldBase.md
@@ -135,6 +135,7 @@ const MyTagList = (props: { children: React.ReactNode }) => {
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
', () => {
expect(await screen.findByText('artist_3')).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();
+ });
+ });
+
describe('"Select all" button', () => {
it('should be displayed if an item is selected', async () => {
render();
diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.stories.tsx b/packages/ra-ui-materialui/src/field/ReferenceArrayField.stories.tsx
index 80c346f2a28..41bc49c7fd2 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.stories.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.stories.tsx
@@ -164,3 +164,40 @@ export const WithPagination = () => (
);
+
+export const WithRenderProp = () => (
+
+
+
+
+
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return (
+
+ {error.toString()}
+
+ );
+ }
+
+ return (
+
+ {data?.map((datum, index) => (
+
{datum.name}
+ ))}
+
+ );
+ }}
+ />
+
+
+
+
+);
diff --git a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
index 6cc9b9d1d8d..8d8e5634e58 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceArrayField.tsx
@@ -80,6 +80,7 @@ export const ReferenceArrayField = <
ReferenceRecordType extends RaRecord = RaRecord,
>({
pagination,
+ render,
...inProps
}: ReferenceArrayFieldProps) => {
const props = useThemeProps({
@@ -88,7 +89,11 @@ export const ReferenceArrayField = <
});
return (
-
+
);
};
@@ -110,8 +115,10 @@ export interface ReferenceArrayFieldViewProps
export const ReferenceArrayFieldView = (
props: ReferenceArrayFieldViewProps
) => {
- const { children, pagination, className, sx } = props;
- const { isPending, total } = useListContext();
+ const { children, render, pagination, className, sx } = props;
+ const listContext = useListContext();
+
+ const { isPending, total } = listContext;
return (
@@ -121,7 +128,9 @@ export const ReferenceArrayFieldView = (
/>
) : (
- {children || }
+ {(render ? render(listContext) : children) || (
+
+ )}
{pagination && total !== undefined ? pagination : null}
)}
From 5add7d56b408fd47bff20376582afc764ecb5825 Mon Sep 17 00:00:00 2001
From: ThieryMichel
Date: Thu, 17 Jul 2025 15:07:26 +0200
Subject: [PATCH 13/25] add render prop to ReferenceOneField
---
docs/ReferenceOneField.md | 29 +++++++++++++++++++
.../src/field/ReferenceOneField.spec.tsx | 6 ++++
.../src/field/ReferenceOneField.stories.tsx | 22 ++++++++++++++
.../src/field/ReferenceOneField.tsx | 3 ++
4 files changed, 60 insertions(+)
diff --git a/docs/ReferenceOneField.md b/docs/ReferenceOneField.md
index a09fc1e5a41..ec574a50d24 100644
--- a/docs/ReferenceOneField.md
+++ b/docs/ReferenceOneField.md
@@ -60,6 +60,7 @@ const BookShow = () => (
| `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` | - | The Field element used to render the referenced record |
+| `render` | Optional | `(ReferenceFieldContext) => Element` | - | A function that takes the `ReferenceFieldContext` and returns 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. |
@@ -80,6 +81,34 @@ For instance, if you want to render both the genre and the ISBN for a book:
```
+
+## `render`
+
+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
+ {
+ if (isPending) {
+ return Loading...;
+ }
+
+ if (error) {
+ return {error.toString()};
+ }
+ return (
+
+ {referenceRecord ? referenceRecord.genre : ''}
+ {referenceRecord ? referenceRecord.ISBN : ''}
+
+ );
+ }}
+/>
+```
+
## `empty`
Use `empty` to customize the text displayed when the related record is empty.
diff --git a/packages/ra-ui-materialui/src/field/ReferenceOneField.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceOneField.spec.tsx
index ee10bc6eb60..fe863bac4e3 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceOneField.spec.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceOneField.spec.tsx
@@ -9,6 +9,7 @@ import {
EmptyText,
Empty,
Themed,
+ WithRenderProp,
} from './ReferenceOneField.stories';
describe('ReferenceOneField', () => {
@@ -54,6 +55,11 @@ describe('ReferenceOneField', () => {
});
});
+ it('should allow to render the referenceRecord using a render prop', async () => {
+ render();
+ await screen.findByText('9780393966473');
+ });
+
describe('emptyText', () => {
it('should render the emptyText prop when the record is not found', async () => {
render();
diff --git a/packages/ra-ui-materialui/src/field/ReferenceOneField.stories.tsx b/packages/ra-ui-materialui/src/field/ReferenceOneField.stories.tsx
index 0db123a2b9b..bc988013257 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceOneField.stories.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceOneField.stories.tsx
@@ -568,3 +568,25 @@ export const Themed = () => (
);
+
+export const WithRenderProp = () => (
+
+ {
+ if (isPending) {
+ return Loading...
;
+ }
+
+ if (error) {
+ return {error.toString()}
;
+ }
+
+ return (
+ {referenceRecord ? referenceRecord.ISBN : ''}
+ );
+ }}
+ />
+
+);
diff --git a/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx b/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx
index 768da81f09a..4102eaf6490 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceOneField.tsx
@@ -7,6 +7,7 @@ import {
SortPayload,
RaRecord,
ReferenceOneFieldBase,
+ UseReferenceResult,
} from 'ra-core';
import { FieldProps } from './types';
@@ -37,6 +38,7 @@ export const ReferenceOneField = <
const {
children,
+ render,
reference,
source = 'id',
target,
@@ -88,6 +90,7 @@ export interface ReferenceOneFieldProps<
ReferenceRecordType extends RaRecord = RaRecord,
> extends Omit, 'source' | 'emptyText'> {
children?: ReactNode;
+ render?: (record: UseReferenceResult) => ReactElement;
reference: string;
target: string;
sort?: SortPayload;
From 1a873b42af784a1e04030077578017afe3931369 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Zaninotto?=
Date: Mon, 21 Jul 2025 20:16:43 +0200
Subject: [PATCH 14/25] [doc] Improve Create page
---
docs/Create.md | 123 ++++++++++++++++++++++++++++++-------------------
1 file changed, 75 insertions(+), 48 deletions(-)
diff --git a/docs/Create.md b/docs/Create.md
index 67428208f6b..55cb5f48a5a 100644
--- a/docs/Create.md
+++ b/docs/Create.md
@@ -55,51 +55,25 @@ export default App;
You can customize the `` component using the following props:
-* [`actions`](#actions): override the actions toolbar with a custom component
-* [`aside`](#aside): component to render aside to the main content
-* `children`: the components that renders the form
-* `render`: Alternative to children. A function that renders the form, receive the create context as its argument
-* `className`: passed to the root component
-* [`component`](#component): override the root component
-* [`disableAuthentication`](#disableauthentication): disable the authentication check
-* [`mutationMode`](#mutationmode): switch to optimistic or undoable mutations (pessimistic by default)
-* [`mutationOptions`](#mutationoptions): options for the `dataProvider.create()` call
-* [`record`](#record): initialize the form with a record
-* [`redirect`](#redirect): change the redirect location after successful creation
-* [`resource`](#resource): override the name of the resource to create
-* [`sx`](#sx-css-api): Override the styles
-* [`title`](#title): override the page title
-* [`transform`](#transform): transform the form data before calling `dataProvider.create()`
-
-## `render`
-
-Alternatively to children you can pass a render prop to ``. The render prop will receive the create context as its argument, allowing to inline the render logic for the create form.
-When receiving a render prop the `` component will ignore the children property.
-
-{% raw %}
-```tsx
- {
- if (createContext.isPending) {
- return Loading...
;
- }
- if (createContext.error) {
- return Error: {error.message}
;
- }
-
- return (
-
- {`Create new ${createContext.resource}`}
-
-
-
-
-
-
-
- );
-}} />
-```
-{% endraw %}
+| Prop | Required | Type | Default | Description |
+|---------------------|----------|---------------------|----------------|--------------------------------------------------------------------------------------------------|
+| `children` | Optional* | `ReactNode` | - | The components that render the form |
+| `render` | Optional* | `function` | - | Alternative to children. Function that renders the form, receives the create context as argument |
+| `actions` | Optional | `ReactNode` | Default toolbar| Override the actions toolbar with a custom component |
+| `aside` | Optional | `ReactNode` | - | Component to render aside to the main content |
+| `className` | Optional | `string` | - | Passed to the root component |
+| `component` | Optional | `string`/`Component`| `Card` | Override the root component |
+| `disableAuthentication` | Optional | `boolean` | `false` | Disable the authentication check |
+| `mutationMode` | Optional | `string` | `pessimistic` | Switch to optimistic or undoable mutations |
+| `mutationOptions` | Optional | `object` | - | Options for the `dataProvider.create()` call |
+| `record` | Optional | `object` | `{}` | Initialize the form with a record |
+| `redirect` | Optional | `string`/`function` | `'edit'` | Change the redirect location after successful creation |
+| `resource` | Optional | `string` | From URL | Override the name of the resource to create |
+| `sx` | Optional | `object` | - | Override the styles |
+| `title` | Optional | `string`/`ReactNode`| Translation | Override the page title |
+| `transform` | Optional | `function` | - | Transform the form data before calling `dataProvider.create()` |
+
+* You must provide either `children` or `render`.
## `actions`
@@ -151,6 +125,28 @@ const PostCreate = () => (
{% endraw %}
+## `children`
+
+The `` component will render its children inside a `CreateContext` provider, which the `save` function. Children can be any React node, but are usually a form component like [``](./SimpleForm.md), [``](./TabbedForm.md), or the headless [`