Skip to content

Commit 996e452

Browse files
authored
Merge pull request #10860 from marmelab/offline-support-reference-fields
Add offline support to `<ReferenceFieldBase>` and `<ReferenceField>`
2 parents c36a139 + d9039c0 commit 996e452

File tree

11 files changed

+332
-117
lines changed

11 files changed

+332
-117
lines changed

docs/ReferenceField.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ It uses `dataProvider.getMany()` instead of `dataProvider.getOne()` [for perform
7979
| `empty` | Optional | `ReactNode` | - | What to render when the field has no value or when the reference is missing |
8080
| `label` | Optional | `string | Function` | `resources. [resource]. fields.[source]` | Label to use for the field when rendered in layout components |
8181
| `link` | Optional | `string | Function` | `edit` | Target of the link wrapping the rendered child. Set to `false` to disable the link. |
82+
| `offline` | Optional | `ReactNode` | - | What to render when there is no network connectivity when loading the record |
8283
| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options |
8384
| `sortBy` | Optional | `string | Function` | `source` | Name of the field to use for sorting when used in a Datagrid |
8485

@@ -98,6 +99,7 @@ By default, `<ReferenceField>` renders the `recordRepresentation` of the referen
9899
```
99100

100101
Alternatively, you can use [the `render` prop](#render) to render the referenced record in a custom way.
102+
101103
## `empty`
102104

103105
`<ReferenceField>` can display a custom message when the referenced record is missing, thanks to the `empty` prop.
@@ -172,6 +174,21 @@ You can also use a custom `link` function to get a custom path for the children.
172174
/>
173175
```
174176

177+
## `offline`
178+
179+
When the user is offline, `<ReferenceField>` is smart enough to display the referenced record if it was previously fetched. However, if the referenced record has never been fetched before, `<ReferenceField>` displays an error message explaining that the app has lost network connectivity.
180+
181+
You can customize this error message by passing a React element or a string to the `offline` prop:
182+
183+
```jsx
184+
<ReferenceField source="user_id" reference="users" offline={<span>No network, could not fetch data</span>} >
185+
...
186+
</ReferenceField>
187+
<ReferenceField source="user_id" reference="users" offline="No network, could not fetch data" >
188+
...
189+
</ReferenceField>
190+
```
191+
175192
## `queryOptions`
176193

177194
Use the `queryOptions` prop to pass options to [the `dataProvider.getMany()` query](./useGetOne.md#aggregating-getone-calls) that fetches the referenced record.

docs/ReferenceFieldBase.md

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ It uses `dataProvider.getMany()` instead of `dataProvider.getOne()` [for perform
6868
| `children` | Optional | `ReactNode` | - | React component to render the referenced record. |
6969
| `render` | Optional | `(context) => ReactNode` | - | Function that takes the referenceFieldContext and renders the referenced record. |
7070
| `empty` | Optional | `ReactNode` | - | What to render when the field has no value or when the reference is missing |
71+
| `offline` | Optional | `ReactNode` | - | What to render when there is no network connectivity when loading the record |
7172
| `queryOptions` | Optional | [`UseQuery Options`](https://tanstack.com/query/v5/docs/react/reference/useQuery) | `{}` | `react-query` client options |
7273
| `sortBy` | Optional | `string | Function` | `source` | Name of the field to use for sorting when used in a Datagrid |
7374

@@ -100,35 +101,6 @@ export const MyReferenceField = () => (
100101
);
101102
```
102103

103-
## `render`
104-
105-
Alternatively, you can pass a `render` function prop instead of children. This function will receive the `ReferenceFieldContext` as argument.
106-
107-
```jsx
108-
export const MyReferenceField = () => (
109-
<ReferenceFieldBase
110-
source="user_id"
111-
reference="users"
112-
render={({ error, isPending, referenceRecord }) => {
113-
if (isPending) {
114-
return <p>Loading...</p>;
115-
}
116-
117-
if (error) {
118-
return (
119-
<p className="error">
120-
{error.message}
121-
</p>
122-
);
123-
}
124-
return <p>{referenceRecord.name}</p>;
125-
}}
126-
/>
127-
);
128-
```
129-
130-
The `render` function prop will take priority on `children` props if both are set.
131-
132104
## `empty`
133105

134106
`<ReferenceFieldBase>` can display a custom message when the referenced record is missing, thanks to the `empty` prop.
@@ -155,6 +127,21 @@ You can pass either a React element or a string to the `empty` prop:
155127
</ReferenceFieldBase>
156128
```
157129

130+
## `offline`
131+
132+
When the user is offline, `<ReferenceFieldBase>` is smart enough to display the referenced record if it was previously fetched. However, if the referenced record has never been fetched before, `<ReferenceFieldBase>` displays an error message explaining that the app has lost network connectivity.
133+
134+
You can customize this error message by passing a React element or a string to the `offline` prop:
135+
136+
```jsx
137+
<ReferenceFieldBase source="user_id" reference="users" offline={<span>No network, could not fetch data</span>} >
138+
...
139+
</ReferenceFieldBase>
140+
<ReferenceFieldBase source="user_id" reference="users" offline="No network, could not fetch data" >
141+
...
142+
</ReferenceFieldBase>
143+
```
144+
158145
## `queryOptions`
159146

160147
Use the `queryOptions` prop to pass options to [the `dataProvider.getMany()` query](./useGetOne.md#aggregating-getone-calls) that fetches the referenced record.
@@ -186,6 +173,36 @@ For instance, if the `posts` resource has a `user_id` field, set the `reference`
186173
</ReferenceFieldBase>
187174
```
188175

176+
177+
## `render`
178+
179+
Alternatively, you can pass a `render` function prop instead of children. This function will receive the `ReferenceFieldContext` as argument.
180+
181+
```jsx
182+
export const MyReferenceField = () => (
183+
<ReferenceFieldBase
184+
source="user_id"
185+
reference="users"
186+
render={({ error, isPending, referenceRecord }) => {
187+
if (isPending) {
188+
return <p>Loading...</p>;
189+
}
190+
191+
if (error) {
192+
return (
193+
<p className="error">
194+
{error.message}
195+
</p>
196+
);
197+
}
198+
return <p>{referenceRecord.name}</p>;
199+
}}
200+
/>
201+
);
202+
```
203+
204+
The `render` function prop will take priority on `children` props if both are set.
205+
189206
## `sortBy`
190207

191208
By default, when used in a `<Datagrid>`, and when the user clicks on the column header of a `<ReferenceFieldBase>`, react-admin sorts the list by the field `source`. To specify another field name to sort by, set the `sortBy` prop.

packages/ra-core/src/controller/field/ReferenceFieldBase.spec.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import expect from 'expect';
3-
import { render, screen, waitFor } from '@testing-library/react';
3+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
44
import { CoreAdminContext } from '../../core/CoreAdminContext';
55
import { useResourceContext } from '../../core/useResourceContext';
66
import { testDataProvider } from '../../dataProvider';
@@ -10,8 +10,10 @@ import {
1010
Errored,
1111
Loading,
1212
Meta,
13+
Offline,
1314
WithRenderProp,
1415
} from './ReferenceFieldBase.stories';
16+
import { RecordContextProvider } from '../record';
1517

1618
describe('<ReferenceFieldBase />', () => {
1719
beforeAll(() => {
@@ -46,20 +48,20 @@ describe('<ReferenceFieldBase />', () => {
4648
return <div>{resource}</div>;
4749
};
4850
const dataProvider = testDataProvider({
49-
// @ts-ignore
50-
getList: () =>
51+
getMany: () =>
52+
// @ts-ignore
5153
Promise.resolve({ data: [{ id: 1 }, { id: 2 }], total: 2 }),
5254
});
5355
render(
5456
<CoreAdminContext dataProvider={dataProvider}>
55-
<ReferenceFieldBase reference="posts" source="post_id">
56-
<MyComponent />
57-
</ReferenceFieldBase>
57+
<RecordContextProvider value={{ post_id: 1 }}>
58+
<ReferenceFieldBase reference="posts" source="post_id">
59+
<MyComponent />
60+
</ReferenceFieldBase>
61+
</RecordContextProvider>
5862
</CoreAdminContext>
5963
);
60-
await waitFor(() => {
61-
expect(screen.queryByText('posts')).not.toBeNull();
62-
});
64+
await screen.findByText('posts');
6365
});
6466

6567
it('should accept meta in queryOptions', async () => {
@@ -70,8 +72,8 @@ describe('<ReferenceFieldBase />', () => {
7072
);
7173
const dataProvider = testDataProvider({
7274
getMany,
73-
// @ts-ignore
7475
getOne: () =>
76+
// @ts-ignore
7577
Promise.resolve({
7678
data: {
7779
id: 1,
@@ -165,4 +167,13 @@ describe('<ReferenceFieldBase />', () => {
165167
});
166168
});
167169
});
170+
171+
it('should render the offline prop node when offline', async () => {
172+
render(<Offline />);
173+
fireEvent.click(await screen.findByText('Simulate offline'));
174+
fireEvent.click(await screen.findByText('Toggle Child'));
175+
await screen.findByText('You are offline, cannot load data');
176+
fireEvent.click(await screen.findByText('Simulate online'));
177+
await screen.findByText('Leo');
178+
});
168179
});

packages/ra-core/src/controller/field/ReferenceFieldBase.stories.tsx

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { QueryClient } from '@tanstack/react-query';
2+
import { onlineManager, QueryClient } from '@tanstack/react-query';
33
import fakeRestDataProvider from 'ra-data-fakerest';
44
import { CoreAdmin } from '../../core/CoreAdmin';
55
import { Resource } from '../../core/Resource';
@@ -9,6 +9,7 @@ import { ReferenceFieldBase } from './ReferenceFieldBase';
99
import { useFieldValue } from '../../util/useFieldValue';
1010
import { useReferenceFieldContext } from './ReferenceFieldContext';
1111
import { DataProvider } from '../../types';
12+
import { useIsOffline } from '../../core/useIsOffline';
1213

1314
export default {
1415
title: 'ra-core/controller/field/ReferenceFieldBase',
@@ -395,6 +396,77 @@ export const WithRenderProp = ({ dataProvider = dataProviderWithAuthors }) => (
395396
</TestMemoryRouter>
396397
);
397398

399+
export const Offline = () => {
400+
return (
401+
<TestMemoryRouter initialEntries={['/books/1/show']}>
402+
<CoreAdmin
403+
dataProvider={dataProviderWithAuthors}
404+
queryClient={
405+
new QueryClient({
406+
defaultOptions: {
407+
queries: {
408+
retry: false,
409+
},
410+
},
411+
})
412+
}
413+
>
414+
<Resource name="authors" />
415+
<Resource
416+
name="books"
417+
show={
418+
<ShowBase>
419+
<div>
420+
<RenderChildOnDemand>
421+
<ReferenceFieldBase
422+
source="author"
423+
reference="authors"
424+
offline={
425+
<p>
426+
You are offline, cannot load
427+
data
428+
</p>
429+
}
430+
>
431+
<MyReferenceField>
432+
<TextField source="first_name" />
433+
</MyReferenceField>
434+
</ReferenceFieldBase>
435+
</RenderChildOnDemand>
436+
</div>
437+
<SimulateOfflineButton />
438+
</ShowBase>
439+
}
440+
/>
441+
</CoreAdmin>
442+
</TestMemoryRouter>
443+
);
444+
};
445+
446+
const SimulateOfflineButton = () => {
447+
const isOffline = useIsOffline();
448+
return (
449+
<button
450+
type="button"
451+
onClick={() => onlineManager.setOnline(isOffline)}
452+
>
453+
{isOffline ? 'Simulate online' : 'Simulate offline'}
454+
</button>
455+
);
456+
};
457+
458+
const RenderChildOnDemand = ({ children }) => {
459+
const [showChild, setShowChild] = React.useState(false);
460+
return (
461+
<>
462+
<button onClick={() => setShowChild(!showChild)}>
463+
Toggle Child
464+
</button>
465+
{showChild && <div>{children}</div>}
466+
</>
467+
);
468+
};
469+
398470
const MyReferenceField = (props: { children: React.ReactNode }) => {
399471
const context = useReferenceFieldContext();
400472

0 commit comments

Comments
 (0)