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

[BC Break] Use context to store notifications instead of Redux #7082

Merged
merged 12 commits into from
Jan 14, 2022
90 changes: 90 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,26 @@ const PostEdit = () => {
};
```

## `useNotify` Now Takes An Options Object

When a component has to display a notification, developers may want to tweak the type, duration, translatino arguments, or the ability to undo the action. The callback returned by `useNotify()` used to accept a long series of argument, but the syntax wasn't very intuitive. To improve the developer experience, these options are now part of an `options` object, passed as second argument.
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved

```diff
```jsx
import { useNotify } from 'react-admin';

const NotifyButton = () => {
const notify = useNotify();
const handleClick = () => {
- notify(`Comment approved`, 'success', undefined, true);
+ notify(`Comment approved`, { type: 'success', undoable: true });
}
return <button onClick={handleClick}>Notify</button>;
};
```

Check [the `useNotify`documentation](https://marmelab.com/react-admin/useNotify.html) for more information.
WiXSL marked this conversation as resolved.
Show resolved Hide resolved

## The `useVersion` Hook Was Removed

React-admin v3 relied on a global `version` variable stored in the Redux state to force page refresh. This is no longer the case, as the refresh functionality is handled by react-query.
Expand Down Expand Up @@ -1168,6 +1188,24 @@ export const Menu = (props) => {

Reducers for the **list parameters** (current sort & filters, selected ids, expanded rows) have moved up to the root reducer (so they don't need the resource to be registered first). This shouldn't impact you if you used the react-admin hooks (`useListParams`, `useSelection`) to read the state.

React-admin no longer uses Redux for **notifications**. Instead, it uses a custom context. This change is backwards compatible, as the APIs for the `useNotify` and the `<Notification>` component are the same. If you used to `dispatch` a `showNotification` action, you'll have to use the `useNotify` hook instead:

```diff
-import { useDispatch } from 'react-redux';
-import { showNotification } from 'react-admin';
+import { useNotify } from 'react-admin';

const NotifyButton = () => {
- const dispatch = useDispatch();
+ const notify = useNotify();
const handleClick = () => {
- dispatch(showNotification('Comment approved', 'success'));
+ notify('Comment approved', { type: 'success' });
}
return <button onClick={handleClick}>Notify</button>;
};
```

## Redux-Saga Was Removed

The use of sagas has been deprecated for a while. React-admin v4 doesn't support them anymore. That means that the Redux actions don't include meta parameters anymore to trigger sagas, the Redux store doesn't include the saga middleware, and the saga-based side effects were removed.
Expand Down Expand Up @@ -1411,6 +1449,58 @@ const Layout = (props) => {
};
```

## The `<Notification>` Component Is Included By `<Admin>` Rather Than `<Layout>`

If you customized the `<Notification>` component (e.g. to tweak the delay after which a notification disappears), you passed your custom notification component to the `<Layout>` component. The `<Notification>` is now included by the `<Admin>` component, which facilitates custom layouts and login screens. As a consequence, you'll need to move your custom notification component to the `<Admin>` component.

```diff
// in src/MyNotification.js
import { Notification } from 'react-admin';

export const MyNotification = props => (
<Notification {...props} autoHideDuration={5000} />
);

// in src/MyLayout.js
-import { Layout } from 'react-admin';
-import { MyNotification } from './MyNotification';

-export const MyLayout = props => (
- <Layout {...props} notification={MyNotification} />
-);

// in src/App.js
-import { MyLayout } from './MyLayout';
+import { MyNotification } from './MyNotification';
import dataProvider from './dataProvider';

const App = () => (
- <Admin layout={MyLayout} dataProvider={dataProvider}>
+ <Admin notification={MyNotification} dataProvider={dataProvider}>
// ...
</Admin>
);
```

If you had a custom Layout and/or Login component, you no longer need to include the `<Notification>` component.

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

export const MyLayout = ({
children,
dashboard,
logout,
title,
}) => {
// ...
return (<>
// ...
- <Notification />
</>);
};
```

# Upgrade to 3.0

We took advantage of the major release to fix all the problems in react-admin that required a breaking change. As a consequence, you'll need to do many small changes in the code of existing react-admin v2 applications. Follow this step-by-step guide to upgrade to react-admin v3.
Expand Down
29 changes: 5 additions & 24 deletions docs/Theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,22 +375,20 @@ const App = () => (
);
```

Your custom layout can extend the default `<Layout>` component if you only want to override the sidebar, the appBar, the menu, the notification component or the error page. For instance:
Your custom layout can extend the default `<Layout>` component if you only want to override the sidebar, the appBar, the menu or the error page. For instance:

```jsx
// in src/MyLayout.js
import { Layout } from 'react-admin';
import MyAppBar from './MyAppBar';
import MySidebar from './MySidebar';
import MyMenu from './MyMenu';
import MyNotification from './MyNotification';

const MyLayout = props => <Layout
{...props}
appBar={MyAppBar}
sidebar={MySidebar}
menu={MyMenu}
notification={MyNotification}
/>;

export default MyLayout;
Expand Down Expand Up @@ -528,7 +526,6 @@ import { ThemeProvider } from '@material-ui/styles';
import {
AppBar,
Menu,
Notification,
Sidebar,
setSidebarVisibility,
ComponentPropType,
Expand Down Expand Up @@ -588,7 +585,6 @@ const MyLayout = ({
{children}
</div>
</main>
<Notification />
</div>
</div>
);
Expand All @@ -607,8 +603,6 @@ MyLayout.propTypes = {
export default MyLayout;
```

**Tip**: Don't forget to render a `<Notification>` component in your custom layout, otherwise the undoable updates will never be sent to the server. That's because part of the "undo" logic of react-admin lies in the `<Notification>` component.

## Adding a Breadcrumb

The `<Breadcrumb>` component is part of `ra-navigation`, an [Enterprise Edition](https://marmelab.com/ra-enterprise)<img class="icon" src="./img/premium.svg" /> module. It displays a breadcrumb based on a site structure that you can override at will.
Expand Down Expand Up @@ -1026,28 +1020,15 @@ const MyNotification = props => <Notification {...props} autoHideDuration={5000}
export default MyNotification;
```

**Tip**: if you use the `showNotification` action, then you can define `autoHideDuration` per message as the third parameter of the `showNotification` action creator.

To use this custom notification component, pass it to a custom Layout, as explained above:

```jsx
// in src/MyLayout.js
import { Layout } from 'react-admin';
import MyNotification from './MyNotification';

const MyLayout = (props) => <Layout {...props} notification={MyNotification} />;

export default MyLayout;
```

Then, use this layout in the `<Admin>` `layout` prop:
To use this custom notification component, pass it to the `<Admin>` component as the `notification` prop:

```jsx
// in src/App.js
import MyLayout from './MyLayout';
import MyNotification from './MyNotification';
import dataProvider from './dataProvider';

const App = () => (
<Admin layout={MyLayout} dataProvider={simpleRestProvider('http://path.to.my.api')}>
<Admin notification={MyNotification} dataProvider={dataProvider}>
// ...
</Admin>
);
Expand Down
41 changes: 33 additions & 8 deletions docs/useNotify.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,27 @@ const NotifyButton = () => {

The callback takes 2 arguments:
- The message to display
- an `options` object with the following keys
- The `type` of the notification (`info`, `success` or `warning` - the default is `info`)
- A `messageArgs` object to pass to the `translate` function (because notification messages are translated if your admin has an `i18nProvider`). It is useful for inserting variables into the translation.
- An `undoable` boolean. Set it to `true` if the notification should contain an "undo" button
- An `autoHideDuration` number. Set it to `0` if the notification should not be dismissible.
- A `multiLine` boolean. Set it to `true` if the notification message should be shown in more than one line.
- an `options` object with the following keys:
- `type`: The notification type (`info`, `success` or `warning` - the default is `info`)
- `messageArgs`: options to pass to the `translate` function (because notification messages are translated if your admin has an `i18nProvider`). It is useful for inserting variables into the translation.
- `undoable`: Set it to `true` if the notification should contain an "undo" button
- `autoHideDuration`: Duration (in milliseconds) after which the notification hides. Set it to `0` if the notification should not be dismissible.
- `multiLine`: Set it to `true` if the notification message should be shown in more than one line.

Here are more examples of `useNotify` calls:

```js
// notify a warning
notify(`This is a warning`, 'warning');
notify(`This is a warning`, { type: 'warning' });
// pass translation arguments
notify('item.created', { type: 'info', messageArgs: { resource: 'post' } });
// send an undoable notification
notify('Element updated', { type: 'info', undoable: true });
```

**Tip**: When using `useNotify` as a side effect for an `undoable` mutation, you MUST set the `undoable` option to `true`, otherwise the "undo" button will not appear, and the actual update will never occur.
## `undoable` Option

When using `useNotify` as a side effect for an `undoable` mutation, you MUST set the `undoable` option to `true`, otherwise the "undo" button will not appear, and the actual update will never occur.

```jsx
import * as React from 'react';
Expand All @@ -61,3 +63,26 @@ const PostEdit = () => {
);
}
```

## `autoHideDuration` Option

You can define a custom delay for hiding for a given notification.
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved

```jsx
import { useNotify } from 'react-admin';

const LogoutButton = () => {
const notify = useNotify();
const logout = useLogout();

const handleClick = () => {
logout().then(() => {
notify('Form submitted successfully', { autoHideDuration: 5000 });
});
};

return <button onClick={handleClick}>Logout</button>;
};
```

To change the default delay for all notifications, check [the Theming documentation](./Theming.md#notifications).
1 change: 0 additions & 1 deletion packages/ra-core/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export * from './clearActions';
export * from './filterActions';
export * from './listActions';
export * from './notificationActions';
export * from './uiActions';
export * from './undoActions';
67 changes: 0 additions & 67 deletions packages/ra-core/src/actions/notificationActions.ts

This file was deleted.

27 changes: 21 additions & 6 deletions packages/ra-core/src/auth/Authenticated.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Provider } from 'react-redux';
import { createMemoryHistory } from 'history';
import { Routes, Route, useLocation } from 'react-router-dom';
import { CoreAdminContext, createAdminStore } from '../core';
import { showNotification } from '../actions';
import { useNotificationContext } from '../notification';
import Authenticated from './Authenticated';
import { testDataProvider } from '../dataProvider';

Expand Down Expand Up @@ -63,13 +63,24 @@ describe('<Authenticated>', () => {
</div>
);
};

let notificationsSpy;
const Notification = () => {
const { notifications } = useNotificationContext();
React.useEffect(() => {
notificationsSpy = notifications;
}, [notifications]);
return null;
};

render(
<Provider store={store}>
<CoreAdminContext
authProvider={authProvider}
dataProvider={testDataProvider()}
history={history}
>
<Notification />
<Routes>
<Route
path="/"
Expand All @@ -87,11 +98,15 @@ describe('<Authenticated>', () => {
await waitFor(() => {
expect(authProvider.checkAuth.mock.calls[0][0]).toEqual({});
expect(authProvider.logout.mock.calls[0][0]).toEqual({});
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch.mock.calls[0][0]).toEqual(
showNotification('ra.auth.auth_check_error', 'warning')
);
expect(dispatch.mock.calls[1][0]).toEqual({
expect(dispatch).toHaveBeenCalledTimes(1);
expect(notificationsSpy).toEqual([
{
message: 'ra.auth.auth_check_error',
type: 'warning',
notificationOptions: {},
},
]);
expect(dispatch.mock.calls[0][0]).toEqual({
type: 'RA/CLEAR_STATE',
});
expect(screen.getByLabelText('nextPathname').innerHTML).toEqual(
Expand Down
Loading