Skip to content

Commit 0678a7e

Browse files
committed
Add offline support to <InfiniteListBase> and <InfiniteList>
1 parent 845152f commit 0678a7e

File tree

9 files changed

+283
-14
lines changed

9 files changed

+283
-14
lines changed

docs/InfiniteList.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ The props are the same as [the `<List>` component](./List.md):
7272
| `filters` | Optional | `ReactElement` | - | The filters to display in the toolbar. |
7373
| `filter` | Optional | `object` | - | The permanent filter values. |
7474
| `filter DefaultValues` | Optional | `object` | - | The default filter values. |
75+
| `offline` | Optional | `ReactNode` | `<Offline>` | The component to render when there is no connectivity and there is no data in the cache |
7576
| `pagination` | Optional | `ReactElement` | `<Infinite Pagination>` | The pagination component to use. |
7677
| `perPage` | Optional | `number` | `10` | The number of records to fetch per page. |
7778
| `queryOptions` | Optional | `object` | - | The options to pass to the `useQuery` hook. |
@@ -85,6 +86,49 @@ Check the [`<List>` component](./List.md) for details about each prop.
8586

8687
Additional props are passed down to the root component (a MUI `<Card>` by default).
8788

89+
## `offline`
90+
91+
By default, `<List>` renders the `<Offline>` component when there is no connectivity and there are no records in the cache yet for the current parameters (page, sort, etc.). You can provide your own component via the `offline` prop:
92+
93+
```jsx
94+
import { InfiniteList, InfinitePagination } from 'react-admin';
95+
import { Alert } from '@mui/material';
96+
97+
const offline = <Alert severity="warning">No network. Could not load the posts.</Alert>;
98+
// The offline component may be displayed at the bottom of the page if the network connectivity is lost
99+
// when loading new pages. Make sure you pass your custom offline component here too
100+
const pagination = <InfinitePagination offline={offline} />;
101+
102+
export const PostList = () => (
103+
<InfiniteList offline={offline} pagination={pagination}>
104+
...
105+
</InfiniteList>
106+
);
107+
```
108+
109+
**Tip**: If the record is in the Tanstack Query cache but you want to warn the user that they may see an outdated version, you can use the `<IsOffline>` component:
110+
111+
```jsx
112+
import { InfiniteList, InfinitePagination, IsOffline } from 'react-admin';
113+
import { Alert } from '@mui/material';
114+
115+
const offline = <Alert severity="warning">No network. Could not load the posts.</Alert>;
116+
// The offline component may be displayed at the bottom of the page if the network connectivity is lost
117+
// when loading new pages. Make sure you pass your custom offline component here too
118+
const pagination = <InfinitePagination offline={offline} />;
119+
120+
export const PostList = () => (
121+
<InfiniteList offline={offline} pagination={pagination}>
122+
<IsOffline>
123+
<Alert severity="warning">
124+
You are offline, the data may be outdated
125+
</Alert>
126+
</IsOffline>
127+
...
128+
</InfiniteList>
129+
);
130+
```
131+
88132
## `pagination`
89133

90134
You can replace the default "load on scroll" pagination (triggered by a component named `<InfinitePagination>`) by a custom pagination component. To get the pagination state and callbacks, you'll need to read the `InfinitePaginationContext`.

packages/ra-core/src/controller/list/InfiniteListBase.stories.tsx

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@ import fakeRestProvider from 'ra-data-fakerest';
33
import englishMessages from 'ra-language-english';
44
import frenchMessages from 'ra-language-french';
55
import polyglotI18nProvider from 'ra-i18n-polyglot';
6-
import { InfiniteListBase } from './InfiniteListBase';
6+
import { InfiniteListBase, InfiniteListBaseProps } from './InfiniteListBase';
77
import { CoreAdminContext } from '../../core';
88
import { useListContext } from './useListContext';
99
import { useInfinitePaginationContext } from './useInfinitePaginationContext';
1010
import {
1111
AuthProvider,
1212
DataProvider,
1313
I18nProvider,
14+
IsOffline,
1415
mergeTranslations,
16+
TestMemoryRouter,
1517
useLocaleState,
1618
} from '../..';
19+
import { onlineManager } from '@tanstack/react-query';
1720

1821
export default {
1922
title: 'ra-core/controller/list/InfiniteListBase',
@@ -314,6 +317,93 @@ export const WithRenderProps = () => (
314317
</CoreAdminContext>
315318
);
316319

320+
export const Offline = ({
321+
dataProvider = defaultDataProvider,
322+
isOnline = true,
323+
...props
324+
}: {
325+
dataProvider?: DataProvider;
326+
isOnline?: boolean;
327+
} & Partial<InfiniteListBaseProps>) => {
328+
React.useEffect(() => {
329+
onlineManager.setOnline(isOnline);
330+
}, [isOnline]);
331+
return (
332+
<TestMemoryRouter>
333+
<CoreAdminContext dataProvider={dataProvider}>
334+
<InfiniteListBase
335+
resource="books"
336+
perPage={5}
337+
{...props}
338+
offline={<p>You are offline, cannot load data</p>}
339+
render={controllerProps => {
340+
const {
341+
data,
342+
error,
343+
isPending,
344+
page,
345+
perPage,
346+
setPage,
347+
total,
348+
} = controllerProps;
349+
if (isPending) {
350+
return <div>Loading...</div>;
351+
}
352+
if (error) {
353+
return <div>Error...</div>;
354+
}
355+
356+
return (
357+
<div>
358+
<p>
359+
Use the story controls to simulate offline
360+
mode:
361+
</p>
362+
<IsOffline>
363+
<p style={{ color: 'orange' }}>
364+
You are offline, the data may be
365+
outdated
366+
</p>
367+
</IsOffline>
368+
<button
369+
disabled={page <= 1}
370+
onClick={() => setPage(page - 1)}
371+
>
372+
previous
373+
</button>
374+
<span>
375+
Page {page} of {Math.ceil(total / perPage)}
376+
</span>
377+
<button
378+
disabled={page >= total / perPage}
379+
onClick={() => setPage(page + 1)}
380+
>
381+
next
382+
</button>
383+
<ul>
384+
{data.map((record: any) => (
385+
<li key={record.id}>{record.title}</li>
386+
))}
387+
</ul>
388+
</div>
389+
);
390+
}}
391+
/>
392+
</CoreAdminContext>
393+
</TestMemoryRouter>
394+
);
395+
};
396+
397+
Offline.args = {
398+
isOnline: true,
399+
};
400+
401+
Offline.argTypes = {
402+
isOnline: {
403+
control: { type: 'boolean' },
404+
},
405+
};
406+
317407
const Title = () => {
318408
const { defaultTitle } = useListContext();
319409
const [locale, setLocale] = useLocaleState();

packages/ra-core/src/controller/list/InfiniteListBase.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ import { useIsAuthPending } from '../../auth';
4848
export const InfiniteListBase = <RecordType extends RaRecord = any>({
4949
children,
5050
render,
51-
loading = null,
51+
loading,
52+
offline,
5253
...props
5354
}: InfiniteListBaseProps<RecordType>) => {
5455
const controllerProps = useInfiniteListController<RecordType>(props);
@@ -57,16 +58,27 @@ export const InfiniteListBase = <RecordType extends RaRecord = any>({
5758
action: 'list',
5859
});
5960

60-
if (isAuthPending && !props.disableAuthentication) {
61-
return loading;
62-
}
63-
6461
if (!render && !children) {
6562
throw new Error(
6663
"<InfiniteListBase> requires either a 'render' prop or 'children' prop"
6764
);
6865
}
6966

67+
const showLoading =
68+
isAuthPending &&
69+
!props.disableAuthentication &&
70+
loading !== undefined &&
71+
loading !== false;
72+
73+
const { isPaused, isPending, isPlaceholderData } = controllerProps;
74+
const showOffline =
75+
isPaused &&
76+
// If isPending and isPaused are true, we are offline and couldn't even load the initial data
77+
// If isPaused and isPlaceholderData are true, we are offline and couldn't even load data with different parameters on the same useQuery observer
78+
(isPending || isPlaceholderData) &&
79+
offline !== undefined &&
80+
offline !== false;
81+
7082
return (
7183
// We pass props.resource here as we don't need to create a new ResourceContext if the props is not provided
7284
<OptionalResourceContextProvider value={props.resource}>
@@ -82,7 +94,13 @@ export const InfiniteListBase = <RecordType extends RaRecord = any>({
8294
controllerProps.isFetchingPreviousPage,
8395
}}
8496
>
85-
{render ? render(controllerProps) : children}
97+
{showLoading
98+
? loading
99+
: showOffline
100+
? offline
101+
: render
102+
? render(controllerProps)
103+
: children}
86104
</InfinitePaginationContext.Provider>
87105
</ListContextProvider>
88106
</OptionalResourceContextProvider>
@@ -93,5 +111,6 @@ export interface InfiniteListBaseProps<RecordType extends RaRecord = any>
93111
extends InfiniteListControllerProps<RecordType> {
94112
loading?: ReactNode;
95113
children?: ReactNode;
114+
offline?: ReactNode;
96115
render?: (props: InfiniteListControllerResult<RecordType>) => ReactNode;
97116
}

packages/ra-core/src/controller/list/useInfiniteListController.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ export const useInfiniteListController = <
102102
total,
103103
error,
104104
isLoading,
105+
isPaused,
105106
isPending,
107+
isPlaceholderData,
106108
isFetching,
107109
hasNextPage,
108110
hasPreviousPage,
@@ -204,7 +206,9 @@ export const useInfiniteListController = <
204206
hideFilter: queryModifiers.hideFilter,
205207
isFetching,
206208
isLoading,
209+
isPaused,
207210
isPending,
211+
isPlaceholderData,
208212
onSelect: selectionModifiers.select,
209213
onSelectAll,
210214
onToggleItem: selectionModifiers.toggle,

packages/ra-ui-materialui/src/list/InfiniteList.stories.tsx

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@ import {
77
useListContext,
88
useInfinitePaginationContext,
99
TestMemoryRouter,
10+
IsOffline,
1011
} from 'ra-core';
11-
import { Box, Button, Card, ThemeOptions, Typography } from '@mui/material';
12+
import {
13+
Alert,
14+
Box,
15+
Button,
16+
Card,
17+
ThemeOptions,
18+
Typography,
19+
} from '@mui/material';
1220
import { InfiniteList } from './InfiniteList';
1321
import { SimpleList } from './SimpleList';
1422
import { DataTable, type DataTableProps } from './datatable';
@@ -24,6 +32,7 @@ import { TopToolbar, Layout } from '../layout';
2432
import { BulkActionsToolbar } from './BulkActionsToolbar';
2533
import { deepmerge } from '@mui/utils';
2634
import { defaultLightTheme } from '../theme';
35+
import { onlineManager } from '@tanstack/react-query';
2736

2837
export default {
2938
title: 'ra-ui-materialui/list/InfiniteList',
@@ -522,3 +531,82 @@ export const WithRenderProp = () => (
522531
/>
523532
</Admin>
524533
);
534+
535+
export const Offline = ({
536+
isOnline = true,
537+
offline,
538+
pagination,
539+
}: {
540+
isOnline?: boolean;
541+
offline?: React.ReactNode;
542+
pagination?: React.ReactNode;
543+
}) => {
544+
React.useEffect(() => {
545+
onlineManager.setOnline(isOnline);
546+
}, [isOnline]);
547+
return (
548+
<Admin dataProvider={dataProvider}>
549+
<Resource
550+
name="books"
551+
list={() => (
552+
<InfiniteList offline={offline} pagination={pagination}>
553+
<BookListOffline />
554+
</InfiniteList>
555+
)}
556+
/>
557+
</Admin>
558+
);
559+
};
560+
561+
const BookListOffline = () => {
562+
const { error, isPending } = useListContext();
563+
if (isPending) {
564+
return <div>Loading...</div>;
565+
}
566+
if (error) {
567+
return <div>Error: {error.message}</div>;
568+
}
569+
return (
570+
<>
571+
<IsOffline>
572+
<Alert severity="warning">
573+
You are offline, the data may be outdated
574+
</Alert>
575+
</IsOffline>
576+
<SimpleList primaryText="%{title}" secondaryText="%{author}" />
577+
</>
578+
);
579+
};
580+
581+
const CustomOffline = () => {
582+
return <Alert severity="warning">You are offline!</Alert>;
583+
};
584+
585+
Offline.args = {
586+
isOnline: true,
587+
offline: 'default',
588+
pagination: 'infinite',
589+
};
590+
591+
Offline.argTypes = {
592+
isOnline: {
593+
control: { type: 'boolean' },
594+
},
595+
pagination: {
596+
control: { type: 'radio' },
597+
options: ['infinite', 'classic'],
598+
mapping: {
599+
infinite: <InfinitePagination />,
600+
classic: <DefaultPagination />,
601+
},
602+
},
603+
offline: {
604+
name: 'Offline component',
605+
control: { type: 'radio' },
606+
options: ['default', 'custom'],
607+
mapping: {
608+
default: undefined,
609+
custom: <CustomOffline />,
610+
},
611+
},
612+
};

packages/ra-ui-materialui/src/list/InfiniteList.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ export const InfiniteList = <RecordType extends RaRecord = any>(
103103
resource={resource}
104104
sort={sort}
105105
storeKey={storeKey}
106+
// Disable offline support from InfiniteListBase as it is handled by ListView to keep the ListView container
107+
offline={false}
106108
>
107109
<ListView<RecordType> {...rest} pagination={pagination} />
108110
</InfiniteListBase>

packages/ra-ui-materialui/src/list/List.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export const List = <RecordType extends RaRecord = any>(
100100
resource={resource}
101101
sort={sort}
102102
storeKey={storeKey}
103-
// Disable offline support from ShowBase as it is handled by ShowView to keep the ShowView container
103+
// Disable offline support from ListBase as it is handled by ListView to keep the ListView container
104104
offline={false}
105105
>
106106
<ListView<RecordType> {...rest} render={render} />

packages/ra-ui-materialui/src/list/ListView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ export interface ListViewProps<RecordType extends RaRecord = any> {
356356
* </List>
357357
* );
358358
*/
359-
pagination?: ReactElement | false;
359+
pagination?: ReactNode | false;
360360

361361
/**
362362
* The page title (main title) to display above the data. Defaults to the humanized resource name.

0 commit comments

Comments
 (0)