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 5 commits
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
24 changes: 12 additions & 12 deletions docs/Upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -613,11 +613,20 @@ import { FieldProps, useRecordContext } from 'react-admin';
}
```

## Custom Routers Must Be Upgraded To Data Routers
## `warnWhenUnsavedChanges` Changes

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.
The `warnWhenUnsavedChanges` feature is a little more restrictive than before:

For instance, if you were using `react-router`'s `BrowserRouter`, you will need to migrate to `createBrowserRouter` and wrap your app in a `RouterProvider`:
- It will 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 allows to prevent unwanted data loss in more situations. No changes are required in the code.

However, the `warnWhenUnsavedChanges` now requires a [Data Router](https://reactrouter.com/en/6.22.3/routers/picking-a-router) (a new type of router from react-router) to work. React-admin uses such a data router by default, so the feature works out of the box in v5.

However, if you use a [custom router](./Routing.md#using-a-custom-router) and the `warnWhenUnsavedChanges` prop, the "warn when unsaved changes" feature will be disabled.

To re-enable it, you'll have to migrate your custom router to use the 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';
Expand Down Expand Up @@ -652,15 +661,6 @@ root.render(

**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:

- `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).
- [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.

## `<Admin history>` Prop Was Removed

The `<Admin history>` prop was deprecated since version 4. It is no longer supported.
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
12 changes: 12 additions & 0 deletions packages/ra-core/src/form/Form.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ZodResolver,
SanitizeEmptyValues,
NullValue,
InNonDataRouter,
} from './Form.stories';
import { mergeTranslations } from '../i18n';

Expand Down Expand Up @@ -756,4 +757,15 @@ describe('Form', () => {
expect(translate).not.toHaveBeenCalledWith('This field is required');
mock.mockRestore();
});

it('should work even inside a non-data router', async () => {
render(<InNonDataRouter />);
fireEvent.click(screen.getByText('Go to form'));
await screen.findByText('title');
fireEvent.change(screen.getByLabelText('title'), {
target: { value: '' },
});
fireEvent.click(screen.getByText('Leave the form'));
await screen.findByText('Go to form');
});
});
47 changes: 47 additions & 0 deletions packages/ra-core/src/form/Form.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import { Route, Routes, useNavigate, Link, HashRouter } from 'react-router-dom';

import { CoreAdminContext } from '../core';
import { Form } from './Form';
Expand Down Expand Up @@ -290,3 +291,49 @@ export const ZodResolver = ({
</CoreAdminContext>
);
};

const FormUnderTest = () => {
const navigate = useNavigate();
return (
<>
<Form
record={{ title: 'lorem', body: 'ipsum' }}
onSubmit={() => setTimeout(() => navigate('/'), 0)}
warnWhenUnsavedChanges
>
<Input source="title" />
<Input source="body" />
<button type="submit">Submit</button>
</Form>
<Link to="/">Leave the form</Link>
</>
);
};

export const WarnWhenUnsavedChanges = ({
i18nProvider = defaultI18nProvider,
}: {
i18nProvider?: I18nProvider;
}) => (
<CoreAdminContext i18nProvider={i18nProvider}>
<Routes>
<Route path="/" element={<Link to="/form">Go to form</Link>} />
<Route path="/form" element={<FormUnderTest />} />
</Routes>
</CoreAdminContext>
);

export const InNonDataRouter = ({
i18nProvider = defaultI18nProvider,
}: {
i18nProvider?: I18nProvider;
}) => (
<HashRouter>
<CoreAdminContext i18nProvider={i18nProvider}>
<Routes>
<Route path="/" element={<Link to="/form">Go to form</Link>} />
<Route path="/form" element={<FormUnderTest />} />
</Routes>
</CoreAdminContext>
</HashRouter>
);
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