Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TypeScript] Make types more strict in ra-core, part II #9743

Merged
merged 20 commits into from Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/Datagrid.md
Expand Up @@ -1128,7 +1128,7 @@ const MyCustomList = () => {
};
```

This list has no filtering, sorting, or row selection - it's static. If you want to allow users to interact with this list, you should pass more props to the `<Datagrid>` component, but the logic isn't trivial. Fortunately, react-admin provides [the `useList` hook](./useList.md) to build callbacks to manipulate local data. You just have to put the result in a `ListContext` to have an interactive `<Datagrid>`:
This list has no filtering, sorting, or row selection - it's static. If you want to allow users to interact with the `<Datagrid>`, use [the `useList` hook](./useList.md) to build callbacks to manipulate local data. You will have to put the result in a `<ListContextProvider>` parent component:

```tsx
import {
Expand Down
122 changes: 122 additions & 0 deletions docs/Upgrade.md
Expand Up @@ -270,6 +270,26 @@ const App = () => (
);
```

## Custom Edit or Show Actions No Longer Receive Any Props

React-admin used to inject the `record` and `resource` props to custom edit or show actions. These props are no longer injected in v5. If you need them, you'll have to use the `useRecordContext` and `useResourceContext` hooks instead. But if you use the standard react-admin buttons like `<ShowButton>`, which already uses these hooks, you don't need inject anything.

```diff
-const MyEditActions = ({ data }) => (
+const MyEditActions = () => (
<TopToolbar>
- <ShowButton record={data} />
+ <ShowButton />
</TopToolbar>
);

const PostEdit = () => (
<Edit actions={<MyEditActions />} {...props}>
...
</Edit>
);
```

## Removed deprecated hooks

The following deprecated hooks have been removed
Expand Down Expand Up @@ -751,6 +771,108 @@ describe('my test suite', () => {
});
```

## TypeScript: Page Contexts Are Now Types Instead of Interfaces

The return type of page controllers is now a type. If you were using an interface extending one of:

- `ListControllerResult`,
- `InfiniteListControllerResult`,
- `EditControllerResult`,
- `ShowControllerResult`, or
- `CreateControllerResult`,

you'll have to change it to a type:

```diff
import { ListControllerResult } from 'react-admin';

-interface MyListControllerResult extends ListControllerResult {
+type MyListControllerResult = ListControllerResult & {
customProp: string;
};
```

## TypeScript: Stronger Types For Page Contexts

The return type of page context hooks is now smarter. This concerns the following hooks:

- `useListContext`,
- `useEditContext`,
- `useShowContext`, and
- `useCreateContext`

Depending on the fetch status of the data, the type of the `data`, `error`, and `isPending` properties will be more precise:

- Loading: `{ data: undefined, error: undefined, isPending: true }`
- Success: `{ data: <Data>, error: undefined, isPending: false }`
- Error: `{ data: undefined, error: <Error>, isPending: false }`
- Error After Refetch: `{ data: <Data>, error: <Error>, isPending: false }`

This means that TypeScript may complain if you use the `data` property without checking if it's defined first. You'll have to update your code to handle the different states:

```diff
const MyCustomList = () => {
const { data, error, isPending } = useListContext();
if (isPending) return <Loading />;
+ if (error) return <Error />;
return (
<ul>
{data.map(record => (
<li key={record.id}>{record.name}</li>
))}
</ul>
);
};
```

Besides, these hooks will now throw an error when called outside of a page context. This means that you can't use them in a custom component that is not a child of a `<List>`, `<ListBase>`, `<Edit>`, `<EditBase>`, `<Show>`, `<ShowBase>`, `<Create>`, or `<CreateBase>` component.

## List Components Can No Longer Be Used In Standalone

An undocumented feature allowed some components designed for list pages to be used outside of a list page, by relying on their props instead of the `ListContext`. This feature was removed in v5.

This concerns the following components:

- `<BulkActionsToolbar>`
- `<BulkDeleteWithConfirmButton>`
- `<BulkDeleteWithUndoButton>`
- `<BulkExportButton>`
- `<BulkUpdateWithConfirmButton>`
- `<BulkUpdateWithUndoButton>`
- `<EditActions>`
- `<ExportButton>`
- `<FilterButton>`
- `<FilterForm>`
- `<ListActions>`
- `<Pagination>`
- `<UpdateWithConfirmButton>`
- `<UpdateWithUndoButton>`

To continue using these components, you'll have to wrap them in a `<ListContextProvider>` component:

```diff
const MyPagination = ({
page,
perPage,
total,
setPage,
setPerPage,
}) => {
return (
- <Pagination page={page} perPage={perPage} total={total} setPage={setPage} setPerPage={setPerPage} />
+ <ListContextProvider value={{ page, perPage, total, setPage, setPerPage }}>
+ <Pagination />
+ </ListContextProvider>
);
};
```

The following components are not affected and can still be used in standalone mode:

- `<Datagrid>`
- `<SimpleList>`
- `<SingleFieldList>`

## Upgrading to v4

If you are on react-admin v3, follow the [Upgrading to v4](https://marmelab.com/react-admin/doc/4.16/Upgrade.html) guide before upgrading to v5.
8 changes: 4 additions & 4 deletions examples/crm/src/companies/CompanyShow.tsx
Expand Up @@ -146,8 +146,8 @@ const TabPanel = (props: TabPanelProps) => {
};

const ContactsIterator = () => {
const { data: contacts, isPending } = useListContext<Contact>();
if (isPending) return null;
const { data: contacts, error, isPending } = useListContext<Contact>();
if (isPending || error) return null;

const now = Date.now();
return (
Expand Down Expand Up @@ -214,8 +214,8 @@ const CreateRelatedContactButton = () => {
};

const DealsIterator = () => {
const { data: deals, isPending } = useListContext<Deal>();
if (isPending) return null;
const { data: deals, error, isPending } = useListContext<Deal>();
if (isPending || error) return null;

const now = Date.now();
return (
Expand Down
4 changes: 2 additions & 2 deletions examples/crm/src/companies/GridList.tsx
Expand Up @@ -26,9 +26,9 @@ const LoadingGridList = () => (
);

const LoadedGridList = () => {
const { data, isPending } = useListContext<Company>();
const { data, error, isPending } = useListContext<Company>();

if (isPending) return null;
if (isPending || error) return null;

return (
<Box display="flex" flexWrap="wrap" width="100%" gap={1}>
Expand Down
4 changes: 2 additions & 2 deletions examples/crm/src/contacts/ContactAside.tsx
Expand Up @@ -111,8 +111,8 @@ export const ContactAside = ({ link = 'edit' }: { link?: 'edit' | 'show' }) => {
};

const TasksIterator = () => {
const { data, isLoading } = useListContext();
if (isLoading || data.length === 0) return null;
const { data, error, isPending } = useListContext();
if (isPending || error || data.length === 0) return null;
return (
<Box>
<Typography variant="subtitle2">Tasks</Typography>
Expand Down
4 changes: 4 additions & 0 deletions examples/crm/src/contacts/ContactList.tsx
Expand Up @@ -38,13 +38,17 @@ import { Contact } from '../types';
const ContactListContent = () => {
const {
data: contacts,
error,
isPending,
onToggleItem,
selectedIds,
} = useListContext<Contact>();
if (isPending) {
return <SimpleListLoading hasLeftAvatarOrIcon hasSecondaryText />;
}
if (error) {
return null;
}
const now = Date.now();

return (
Expand Down
4 changes: 2 additions & 2 deletions examples/crm/src/deals/ContactList.tsx
Expand Up @@ -4,9 +4,9 @@ import { Box, Link } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';

export const ContactList = () => {
const { data, isPending } = useListContext();
const { data, error, isPending } = useListContext();

if (isPending) return <div style={{ height: '2em' }} />;
if (isPending || error) return <div style={{ height: '2em' }} />;
return (
<Box
component="ul"
Expand Down
4 changes: 4 additions & 0 deletions examples/crm/src/deals/DealCreate.tsx
Expand Up @@ -34,6 +34,10 @@ export const DealCreate = ({ open }: { open: boolean }) => {
const queryClient = useQueryClient();

const onSuccess = async (deal: Deal) => {
if (!allDeals) {
redirect('/deals');
return;
}
// increase the index of all deals in the same stage as the new deal
// first, get the list of deals in the same stage
const deals = allDeals.filter(
Expand Down
4 changes: 2 additions & 2 deletions examples/crm/src/notes/NotesIterator.tsx
Expand Up @@ -12,8 +12,8 @@ export const NotesIterator = ({
showStatus?: boolean;
reference: 'contacts' | 'deals';
}) => {
const { data, isPending } = useListContext();
if (isPending) return null;
const { data, error, isPending } = useListContext();
if (isPending || error) return null;
return (
<>
<NewNote showStatus={showStatus} reference={reference} />
Expand Down
5 changes: 4 additions & 1 deletion examples/demo/src/categories/CategoryList.tsx
Expand Up @@ -31,10 +31,13 @@ const CategoryList = () => (
);

const CategoryGrid = () => {
const { data, isPending } = useListContext<Category>();
const { data, error, isPending } = useListContext<Category>();
if (isPending) {
return null;
}
if (error) {
return null;
}
return (
<Grid container spacing={2} sx={{ mt: 0 }}>
{data.map(record => (
Expand Down
4 changes: 2 additions & 2 deletions examples/demo/src/orders/MobileGrid.tsx
Expand Up @@ -16,9 +16,9 @@ import CustomerReferenceField from '../visitors/CustomerReferenceField';
import { Order } from '../types';

const MobileGrid = () => {
const { data, isPending } = useListContext<Order>();
const { data, error, isPending } = useListContext<Order>();
const translate = useTranslate();
if (isPending || data.length === 0) {
if (isPending || error || data.length === 0) {
return null;
}
return (
Expand Down
6 changes: 3 additions & 3 deletions examples/demo/src/reviews/ReviewEdit.tsx
Expand Up @@ -5,7 +5,6 @@ import {
TextInput,
SimpleForm,
DateField,
EditProps,
Labeled,
} from 'react-admin';
import { Box, Grid, Stack, IconButton, Typography } from '@mui/material';
Expand All @@ -17,11 +16,12 @@ import StarRatingField from './StarRatingField';
import ReviewEditToolbar from './ReviewEditToolbar';
import { Review } from '../types';

interface Props extends EditProps<Review> {
interface ReviewEditProps {
id: Review['id'];
onCancel: () => void;
}

const ReviewEdit = ({ id, onCancel }: Props) => {
const ReviewEdit = ({ id, onCancel }: ReviewEditProps) => {
const translate = useTranslate();
return (
<EditBase id={id}>
Expand Down
4 changes: 2 additions & 2 deletions examples/demo/src/reviews/ReviewListMobile.tsx
Expand Up @@ -6,8 +6,8 @@ import { ReviewItem } from './ReviewItem';
import { Review } from './../types';

const ReviewListMobile = () => {
const { data, isPending, total } = useListContext<Review>();
if (isPending || Number(total) === 0) {
const { data, error, isPending, total } = useListContext<Review>();
if (isPending || error || Number(total) === 0) {
return null;
}
return (
Expand Down
4 changes: 2 additions & 2 deletions examples/demo/src/visitors/MobileGrid.tsx
Expand Up @@ -17,9 +17,9 @@ import { Customer } from '../types';

const MobileGrid = () => {
const translate = useTranslate();
const { data, isPending } = useListContext<Customer>();
const { data, error, isPending } = useListContext<Customer>();

if (isPending || data.length === 0) {
if (isPending || error || data.length === 0) {
return null;
}

Expand Down
2 changes: 2 additions & 0 deletions examples/simple/src/customRouteLayout.tsx
Expand Up @@ -30,6 +30,8 @@ const CustomRouteLayout = ({ title = 'Posts' }) => {
isPending={isPending}
total={total}
rowClick="edit"
bulkActionButtons={false}
resource="posts"
>
<TextField source="id" sortable={false} />
<TextField source="title" sortable={false} />
Expand Down
14 changes: 1 addition & 13 deletions packages/ra-core/src/controller/create/CreateContext.tsx
Expand Up @@ -19,18 +19,6 @@ import { CreateControllerResult } from './useCreateController';
* );
* };
*/
export const CreateContext = createContext<CreateControllerResult>({
record: null,
defaultTitle: null,
isFetching: null,
isLoading: null,
isPending: null,
redirect: null,
resource: null,
save: null,
saving: null,
registerMutationMiddleware: null,
unregisterMutationMiddleware: null,
});
export const CreateContext = createContext<CreateControllerResult | null>(null);

CreateContext.displayName = 'CreateContext';