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

Fix react-admin requires custom routers to be data routers #9723

Merged
merged 6 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/AccordionForm.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ export const TagEdit = () => (
);
```

**Warning**: This feature only works if you have a dependency on react-router 6.3.0 **at most**. The react-router team disabled this possibility in react-router 6.4, so `warnWhenUnsavedChanges` will silently fail with react-router 6.4 or later.
**Note**: Due to limitations in react-router, this feature only works if you use the default router provided by react-admin, or if you use a Data Router.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: I'd add a link to react-router's documentation for those not knowing what a Data Router is

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're right, added.


## `sx`: CSS API

Expand Down
2 changes: 1 addition & 1 deletion docs/EditTutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ const Form = ({ onSubmit }) => {

**Tip**: You can customize the message displayed in the confirm dialog by setting the `ra.message.unsaved_changes` message in your i18nProvider.

**Warning**: This feature only works if you have a dependency on react-router 6.3.0 **at most**. The react-router team disabled this possibility in react-router 6.4, so `warnWhenUnsavedChanges` will silently fail with react-router 6.4 or later.
**Note**: Due to limitations in react-router, this feature only works if you use the default router provided by react-admin, or if you use a Data Router.

## Submit On Enter

Expand Down
2 changes: 1 addition & 1 deletion docs/Form.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export const TagEdit = () => (
);
```

**Warning**: This feature only works if you have a dependency on react-router 6.3.0 **at most**. The react-router team disabled this possibility in react-router 6.4, so `warnWhenUnsavedChanges` will silently fail with react-router 6.4 or later.
**Note**: Due to limitations in react-router, this feature only works if you use the default router provided by react-admin, or if you use a Data Router.

## Subscribing To Form Changes

Expand Down
2 changes: 1 addition & 1 deletion docs/LongForm.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ export const TagEdit = () => (
);
```

**Warning**: This feature only works if you have a dependency on react-router 6.3.0 **at most**. The react-router team disabled this possibility in react-router 6.4, so `warnWhenUnsavedChanges` will silently fail with react-router 6.4 or later.
**Note**: Due to limitations in react-router, this feature only works if you use the default router provided by react-admin, or if you use a Data Router.

## `<LongForm.Section>`

Expand Down
2 changes: 1 addition & 1 deletion docs/SimpleForm.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ export const TagEdit = () => (
);
```

**Warning**: This feature only works if you have a dependency on react-router 6.3.0 **at most**. The react-router team disabled this possibility in react-router 6.4, so `warnWhenUnsavedChanges` will silently fail with react-router 6.4 or later.
**Note**: Due to limitations in react-router, this feature only works if you use the default router provided by react-admin, or if you use a Data Router.

## Using Fields As Children

Expand Down
2 changes: 1 addition & 1 deletion docs/TabbedForm.md
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ export const TagEdit = () => (
);
```

**Warning**: This feature only works if you have a dependency on react-router 6.3.0 **at most**. The react-router team disabled this possibility in react-router 6.4, so `warnWhenUnsavedChanges` will silently fail with react-router 6.4 or later.
**Note**: Due to limitations in react-router, this feature only works if you use the default router provided by react-admin, or if you use a Data Router.

## `<TabbedForm.Tab>`

Expand Down
47 changes: 5 additions & 42 deletions docs/Upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -613,53 +613,16 @@ import { FieldProps, useRecordContext } from 'react-admin';
}
```

## Custom Routers Must Be Upgraded To Data Routers

If you are using a [custom router](./Routing.md#using-a-custom-router), or if your React Admin app is embedded in another app with its own router, you will need to [migrate to a data router](https://reactrouter.com/en/main/upgrading/v6-data). This is because React Admin now uses `react-router`'s data router.

For instance, if you were using `react-router`'s `BrowserRouter`, you will need to migrate to `createBrowserRouter` and wrap your app in a `RouterProvider`:

```diff
import * as React from 'react';
import { Admin, Resource } from 'react-admin';
import { createRoot } from 'react-dom/client';
-import { BrowserRouter } from 'react-router-dom';
+import { createBrowserRouter, RouterProvider } from 'react-router-dom';

import dataProvider from './dataProvider';
import posts from './posts';

const App = () => (
- <BrowserRouter>
<Admin dataProvider={dataProvider}>
<Resource name="posts" {...posts} />
</Admin>
- </BrowserRouter>
);

+const router = createBrowserRouter([{ path: '*', element: <App /> }]);

const container = document.getElementById('root');
const root = createRoot(container);

root.render(
<React.StrictMode>
- <App />
+ <RouterProvider router={router} />
</React.StrictMode>
);
```

**Tip:** Check out the [Migrating to RouterProvider](https://reactrouter.com/en/main/upgrading/v6-data) documentation to learn more about the migration steps and impacts.

## `warnWhenUnsavedChanges` Is More Restrictive

Due to the migration to `react-router`'s data router, you will notice that the `useWarnWhenUnsavedChanges` hook is a little more restrictive than before. Here are the main changes:
Due to the migration to `react-router`'s data router, you will notice that the `warnWhenUnsavedChanges` feature is a little more restrictive than before. Here are the main changes:

- `useWarnWhenUnsavedChanges` will also open a confirmation dialog (and block the navigation) if a navigation is fired when the form is currently submitting (submission will continue in the background).
- `warnWhenUnsavedChanges` will also open a confirmation dialog (and block the navigation) if a navigation is fired when the form is currently submitting (submission will continue in the background).
- [Due to browser constraints](https://stackoverflow.com/questions/38879742/is-it-possible-to-display-a-custom-message-in-the-beforeunload-popup), the message displayed in the confirmation dialog when closing the browser's tab cannot be customized (it is managed by the browser).

This behavior is a little more restrictive, but it allows to prevent unwanted data loss in most situations. No changes are required in the code.
This behavior allows to prevent unwanted data loss in more situations than before.

No changes are required in the code.

## `<Admin history>` Prop Was Removed

Expand Down
2 changes: 1 addition & 1 deletion docs/WizardForm.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ export const TagEdit = () => (
);
```

**Warning**: This feature only works if you have a dependency on react-router 6.3.0 **at most**. The react-router team disabled this possibility in react-router 6.4, so `warnWhenUnsavedChanges` will silently fail with react-router 6.4 or later.
**Note**: Due to limitations in react-router, this feature only works if you use the default router provided by react-admin, or if you use a Data Router.

## `<WizardForm.Step>`

Expand Down
68 changes: 57 additions & 11 deletions packages/ra-core/src/form/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import * as React from 'react';
import { ReactNode } from 'react';
import { ReactNode, useContext } from 'react';
import {
FormProvider,
FieldValues,
UseFormProps,
SubmitHandler,
} from 'react-hook-form';
import {
UNSAFE_DataRouterContext,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately react-router doesn't expose the DataRouterContext, see remix-run/react-router#11347

UNSAFE_DataRouterStateContext,
} from 'react-router';

import { FormGroupsProvider } from './FormGroupsProvider';
import { RaRecord } from '../types';
Expand All @@ -14,8 +18,13 @@ import {
OptionalRecordContextProvider,
SaveHandler,
} from '../controller';
import { SourceContextProvider, SourceContextValue, useResourceContext } from '../core';
import {
SourceContextProvider,
SourceContextValue,
useResourceContext,
} from '../core';
import { ValidateForm } from './getSimpleValidationResolver';
import { WarnWhenUnsavedChanges } from './WarnWhenUnsavedChanges';
import { useAugmentedForm } from './useAugmentedForm';

/**
Expand Down Expand Up @@ -45,17 +54,40 @@ import { useAugmentedForm } from './useAugmentedForm';
*
* @link https://react-hook-form.com/docs/useformcontext
*/
// TODO: remove after upgrading prettier
// eslint-disable-next-line prettier/prettier
export const Form = <RecordType = any>(props: FormProps<RecordType>) => {
const { children, id, className, noValidate = false } = props;
export function Form<RecordType = any>(props: FormProps<RecordType>) {
const {
children,
id,
className,
noValidate = false,
formRootPathname,
warnWhenUnsavedChanges,
WarnWhenUnsavedChangesComponent = WarnWhenUnsavedChanges,
} = props;
const record = useRecordContext(props);
const resource = useResourceContext(props);
const { form, formHandleSubmit } = useAugmentedForm(props);
const sourceContext = React.useMemo<SourceContextValue>(() => ({
getSource: (source: string) => source,
getLabel: (source: string) => `resources.${resource}.fields.${source}`,
}), [resource]);
const sourceContext = React.useMemo<SourceContextValue>(
() => ({
getSource: (source: string) => source,
getLabel: (source: string) =>
`resources.${resource}.fields.${source}`,
}),
[resource]
);
const dataRouterContext = useContext(UNSAFE_DataRouterContext);
const dataRouterStateContext = useContext(UNSAFE_DataRouterStateContext);
if (
warnWhenUnsavedChanges &&
(!dataRouterContext || !dataRouterStateContext) &&
process.env.NODE_ENV === 'development'
) {
console.error(
'Cannot use the warnWhenUnsavedChanges feature outside of a DataRouter. ' +
'The warnWhenUnsavedChanges feature is disabled. ' +
'Remove the warnWhenUnsavedChanges prop or convert your custom router to a Data Router.'
);
}

return (
<OptionalRecordContextProvider value={record}>
Expand All @@ -70,17 +102,31 @@ export const Form = <RecordType = any>(props: FormProps<RecordType>) => {
>
{children}
</form>
{warnWhenUnsavedChanges &&
dataRouterContext &&
dataRouterStateContext && (
<WarnWhenUnsavedChangesComponent
enable
formRootPathName={formRootPathname}
formControl={form.control}
/>
)}
</FormGroupsProvider>
</FormProvider>
</SourceContextProvider>
</OptionalRecordContextProvider>
);
};
}

export type FormProps<RecordType = any> = FormOwnProps<RecordType> &
Omit<UseFormProps, 'onSubmit'> & {
validate?: ValidateForm;
noValidate?: boolean;
WarnWhenUnsavedChangesComponent?: React.ComponentType<{
enable?: boolean;
formRootPathName?: string;
formControl?: any;
}>;
};

export interface FormOwnProps<RecordType = any> {
Expand Down
10 changes: 10 additions & 0 deletions packages/ra-core/src/form/WarnWhenUnsavedChanges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useWarnWhenUnsavedChanges } from './useWarnWhenUnsavedChanges';

export const WarnWhenUnsavedChanges = ({
enable = true,
formRootPathName,
formControl,
}) => {
useWarnWhenUnsavedChanges(enable, formRootPathName, formControl);
return null;
};
1 change: 1 addition & 0 deletions packages/ra-core/src/form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ export * from './useInput';
export * from './useSuggestions';
export * from './useUnique';
export * from './useWarnWhenUnsavedChanges';
export * from './WarnWhenUnsavedChanges';
10 changes: 0 additions & 10 deletions packages/ra-core/src/form/useAugmentedForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
} from './getSimpleValidationResolver';
import { setSubmissionErrors } from './setSubmissionErrors';
import { useNotifyIsFormInvalid } from './useNotifyIsFormInvalid';
import { useWarnWhenUnsavedChanges } from './useWarnWhenUnsavedChanges';
import { sanitizeEmptyValues as sanitizeValues } from './sanitizeEmptyValues';

/**
Expand All @@ -41,7 +40,6 @@ export const useAugmentedForm = <RecordType = any>(
reValidateMode = 'onChange',
onSubmit,
sanitizeEmptyValues,
warnWhenUnsavedChanges,
validate,
disableInvalidFormNotification,
...rest
Expand Down Expand Up @@ -83,13 +81,6 @@ export const useAugmentedForm = <RecordType = any>(
// notify on invalid form
useNotifyIsFormInvalid(form.control, !disableInvalidFormNotification);

// warn when unsaved change
useWarnWhenUnsavedChanges(
Boolean(warnWhenUnsavedChanges),
formRootPathname,
form.control
);

// submit callbacks
const handleSubmit = useCallback(
async (values, event) => {
Expand Down Expand Up @@ -141,7 +132,6 @@ export interface UseFormOwnProps<RecordType = any> {
formRootPathname?: string;
record?: Partial<RaRecord>;
onSubmit?: SubmitHandler<FieldValues> | SaveHandler<RecordType>;
warnWhenUnsavedChanges?: boolean;
sanitizeEmptyValues?: boolean;
disableInvalidFormNotification?: boolean;
}
Loading