From f6fbf923459eb62856a204dfadbb7f239edbad57 Mon Sep 17 00:00:00 2001
From: Will-thom <116388885+Will-thom@users.noreply.github.com>
Date: Tue, 19 May 2026 21:28:02 -0300
Subject: [PATCH] feat: Allow customizing ReferenceManyCount rendering
---
docs/ReferenceManyCount.md | 13 ++++++
.../content/docs/ReferenceManyCountBase.md | 19 ++++++++
.../field/ReferenceManyCountBase.spec.tsx | 12 +++++
.../field/ReferenceManyCountBase.stories.tsx | 40 ++++++++++++++++-
.../field/ReferenceManyCountBase.tsx | 45 ++++++++++++++-----
.../src/field/ReferenceManyCount.spec.tsx | 15 +++++++
.../src/field/ReferenceManyCount.stories.tsx | 33 ++++++++++++++
.../src/field/ReferenceManyCount.tsx | 7 ++-
8 files changed, 171 insertions(+), 13 deletions(-)
diff --git a/docs/ReferenceManyCount.md b/docs/ReferenceManyCount.md
index a136a0e86a0..0d59df4f47d 100644
--- a/docs/ReferenceManyCount.md
+++ b/docs/ReferenceManyCount.md
@@ -66,6 +66,7 @@ export const PostList = () => (
| ----------- | -------- | ------------------------------------------ | --------------------------------- | ------------------------------------------------------------------------- |
| `reference` | Required | string | - | Name of the related resource to fetch (e.g. `comments`) |
| `target` | Required | string | - | Name of the field in the related resource that points to the current one. |
+| `children` | Optional | `ReactNode` | - | The component used to render the count. |
| `filter` | Optional | Object | - | Filter to apply to the query. |
| `link` | Optional | bool | `false` | If true, the count is wrapped in a `` to the filtered list view. |
| `offline` | Optional | `ReactNode` | | The component to render when there is no connectivity and the record isn't in the cache
@@ -77,6 +78,18 @@ export const PostList = () => (
Additional props are passed to [the underlying Material UI `` element](https://mui.com/material-ui/api/typography/).
+## `children`
+
+By default, `` renders the total as a raw number. You can pass a child component to customize the rendering. `` creates a `RecordContext` with a `total` field, so any Field component can read it:
+
+```jsx
+import { NumberField, ReferenceManyCount } from 'react-admin';
+
+
+
+
+```
+
## `filter`
If you want to count the number of records matching a given filter, pass it as the `filter` prop. For example, to count the number of comments already published:
diff --git a/docs_headless/src/content/docs/ReferenceManyCountBase.md b/docs_headless/src/content/docs/ReferenceManyCountBase.md
index 2ff527f59a2..2344863123a 100644
--- a/docs_headless/src/content/docs/ReferenceManyCountBase.md
+++ b/docs_headless/src/content/docs/ReferenceManyCountBase.md
@@ -60,12 +60,31 @@ export const PostList = () => (
| ----------- | -------- | ------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------- |
| `reference` | Required | string | - | Name of the related resource to fetch (e.g. `comments`) |
| `target` | Required | string | - | Name of the field in the related resource that points to the current one. |
+| `children` | Optional | `ReactNode` | - | The component used to render the count. |
| `filter` | Optional | Object | - | Filter to apply to the query. |
| `resource` | Optional | string | - | Resource to count. Default to the current `ResourceContext` |
| `sort` | Optional | `{ field: string, order: 'ASC' or 'DESC' }` | `{ field: 'id', order: 'DESC' }` | The sort option sent to `getManyReference` |
| `timeout` | Optional | number | 1000 | Number of milliseconds to wait before displaying the loading indicator. |
+## `children`
+
+By default, `` renders the total as a raw number. You can pass a child component to customize the rendering. `` creates a `RecordContext` with a `total` field, so descendants can read it with `useRecordContext`:
+
+```jsx
+import { ReferenceManyCountBase, useRecordContext } from 'ra-core';
+
+const Count = () => {
+ const record = useRecordContext();
+
+ return {record?.total} comments;
+};
+
+
+
+
+```
+
## `filter`
If you want to count the number of records matching a given filter, pass it as the `filter` prop. For example, to count the number of comments already published:
diff --git a/packages/ra-core/src/controller/field/ReferenceManyCountBase.spec.tsx b/packages/ra-core/src/controller/field/ReferenceManyCountBase.spec.tsx
index 1f15ae14657..fa05a206c2a 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyCountBase.spec.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyCountBase.spec.tsx
@@ -5,6 +5,8 @@ import {
ErrorState,
LoadingState,
Offline,
+ WithChildren,
+ WithRenderFunction,
} from './ReferenceManyCountBase.stories';
import { onlineManager } from '@tanstack/react-query';
@@ -31,6 +33,16 @@ describe('ReferenceManyCountBase', () => {
await screen.findByText('3');
});
+ it('should render children in a record context containing the total', async () => {
+ render();
+ await screen.findByText('3 comments');
+ });
+
+ it('should accept a render function as children', async () => {
+ render();
+ await screen.findByText('3 comments');
+ });
+
it('should render the offline prop node when offline', async () => {
render();
fireEvent.click(await screen.findByText('Simulate offline'));
diff --git a/packages/ra-core/src/controller/field/ReferenceManyCountBase.stories.tsx b/packages/ra-core/src/controller/field/ReferenceManyCountBase.stories.tsx
index c92bc03ec00..786d3b442dc 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyCountBase.stories.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyCountBase.stories.tsx
@@ -4,7 +4,7 @@ import {
QueryClient,
onlineManager,
} from '@tanstack/react-query';
-import { RecordContextProvider } from '../record';
+import { RecordContextProvider, useRecordContext } from '../record';
import { DataProviderContext } from '../../dataProvider';
import { ResourceContextProvider, useIsOffline } from '../../core';
import { TestMemoryRouter } from '../../routing';
@@ -65,6 +65,38 @@ export const Basic = () => (
);
+export const WithChildren = () => (
+
+ Promise.resolve({
+ data: [comments.filter(c => c.post_id === 1)[0]],
+ total: comments.filter(c => c.post_id === 1).length,
+ }),
+ }}
+ >
+
+
+
+
+);
+
+export const WithRenderFunction = () => (
+
+ Promise.resolve({
+ data: [comments.filter(c => c.post_id === 1)[0]],
+ total: comments.filter(c => c.post_id === 1).length,
+ }),
+ }}
+ >
+
+ {({ total }) => {total} comments}
+
+
+);
+
export const LoadingState = () => (
new Promise(() => {}) }}>
{
>
);
};
+
+const Count = () => {
+ const record = useRecordContext();
+
+ return {record?.total} comments;
+};
diff --git a/packages/ra-core/src/controller/field/ReferenceManyCountBase.tsx b/packages/ra-core/src/controller/field/ReferenceManyCountBase.tsx
index 6591678f4be..b2ae97e381a 100644
--- a/packages/ra-core/src/controller/field/ReferenceManyCountBase.tsx
+++ b/packages/ra-core/src/controller/field/ReferenceManyCountBase.tsx
@@ -4,6 +4,7 @@ import {
type UseReferenceManyFieldControllerParams,
} from './useReferenceManyFieldController';
import { useTimeout } from '../../util/hooks';
+import { RecordContextProvider } from '../record';
/**
* Fetch and render the number of records related to the current one
@@ -17,7 +18,14 @@ import { useTimeout } from '../../util/hooks';
*
*/
export const ReferenceManyCountBase = (props: ReferenceManyCountBaseProps) => {
- const { loading, error, offline, timeout = 1000, ...rest } = props;
+ const {
+ children,
+ loading,
+ error,
+ offline,
+ timeout = 1000,
+ ...rest
+ } = props;
const oneSecondHasPassed = useTimeout(timeout);
const {
@@ -37,24 +45,41 @@ export const ReferenceManyCountBase = (props: ReferenceManyCountBaseProps) => {
isPending && isPaused && offline !== undefined && offline !== false;
const shouldRenderError =
!isPending && fetchError && error !== undefined && error !== false;
+ const totalRecord = React.useMemo(() => ({ id: 'count', total }), [total]);
+ const renderChildren =
+ typeof children === 'function' ? children(totalRecord) : children;
return (
<>
- {shouldRenderLoading
- ? oneSecondHasPassed
- ? loading
- : null
- : shouldRenderOffline
- ? offline
- : shouldRenderError
- ? error
- : total}
+ {shouldRenderLoading ? (
+ oneSecondHasPassed ? (
+ loading
+ ) : null
+ ) : shouldRenderOffline ? (
+ offline
+ ) : shouldRenderError ? (
+ error
+ ) : children != null ? (
+
+ {renderChildren}
+
+ ) : (
+ total
+ )}
>
);
};
+export interface ReferenceManyCountRecord {
+ id: string;
+ total: number | undefined;
+}
+
export interface ReferenceManyCountBaseProps
extends UseReferenceManyFieldControllerParams {
+ children?:
+ | React.ReactNode
+ | ((record: ReferenceManyCountRecord) => React.ReactNode);
timeout?: number;
loading?: React.ReactNode;
error?: React.ReactNode;
diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyCount.spec.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyCount.spec.tsx
index 8c866fd0e8e..61907774643 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceManyCount.spec.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceManyCount.spec.tsx
@@ -4,8 +4,10 @@ import { fireEvent, render, screen } from '@testing-library/react';
import {
Basic,
ErrorState,
+ LinkWithChildren,
Offline,
Themed,
+ WithChildren,
WithFilter,
Wrapper,
} from './ReferenceManyCount.stories';
@@ -20,6 +22,19 @@ describe('', () => {
render();
await screen.findByText('3');
});
+
+ it('should render children in a record context containing the total', async () => {
+ render();
+ await screen.findByText('30,060');
+ });
+
+ it('should wrap children in a link when link is true', async () => {
+ render();
+ expect(
+ await screen.findByRole('link', { name: '30,060' })
+ ).toHaveAttribute('href', '/comments?filter={"post_id":1}');
+ });
+
it('should render an error icon when the request fails', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
render();
diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyCount.stories.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyCount.stories.tsx
index 14522b8e774..0e45b501d2e 100644
--- a/packages/ra-ui-materialui/src/field/ReferenceManyCount.stories.tsx
+++ b/packages/ra-ui-materialui/src/field/ReferenceManyCount.stories.tsx
@@ -16,6 +16,7 @@ import { createTheme, ThemeOptions } from '@mui/material';
import { ReferenceManyCount } from './ReferenceManyCount';
import { defaultLightTheme, ThemeProvider, ThemesContext } from '../theme';
+import { NumberField } from './NumberField';
export default {
title: 'ra-ui-materialui/fields/ReferenceManyCount',
@@ -84,6 +85,22 @@ export const Basic = () => (
);
+export const WithChildren = () => (
+
+ Promise.resolve({
+ data: [comments.filter(c => c.post_id === 1)[0]],
+ total: 30060,
+ }),
+ }}
+ >
+
+
+
+
+);
+
export const LoadingState = () => (
new Promise(() => {}) }}>
@@ -144,6 +161,22 @@ export const Link = () => (
);
+export const LinkWithChildren = () => (
+
+ Promise.resolve({
+ data: [comments.filter(c => c.post_id === 1)[0]],
+ total: 30060,
+ }),
+ }}
+ >
+
+
+
+
+);
+
export const LinkWithFilter = () => (
(
resource,
source = 'id',
offline = defaultOffline,
+ children,
...rest
} = props;
const record = useRecordContext(props);
@@ -66,7 +67,9 @@ export const ReferenceManyCount = (
}
offline={offline}
- />
+ >
+ {children}
+
);
return (
;
export interface ReferenceManyCountProps
extends Omit, 'source'>,
Omit,
- Omit {
+ Omit {
link?: boolean;
}