diff --git a/docs/Admin.md b/docs/Admin.md
index d7841c469a8..4a8e05ecbb3 100644
--- a/docs/Admin.md
+++ b/docs/Admin.md
@@ -161,14 +161,16 @@ If you want to add or remove menu items, for instance to link to non-resources p
```jsx
// in src/Menu.js
import React, { createElement } from 'react';
-import { connect } from 'react-redux';
+import { useSelector } from 'react-redux';
import { useMediaQuery } from '@material-ui/core';
import { MenuItemLink, getResources } from 'react-admin';
import { withRouter } from 'react-router-dom';
import LabelIcon from '@material-ui/icons/Label';
-const Menu = ({ resources, onMenuClick, open, logout }) => {
+const Menu = ({ onMenuClick, logout }) => {
const isXSmall = useMediaQuery(theme => theme.breakpoints.down('xs'));
+ const open = useSelector(state => state.admin.ui.sidebarOpen);
+ const resources = useSelector(getResources);
return (
{resources.map(resource => (
@@ -193,20 +195,13 @@ const Menu = ({ resources, onMenuClick, open, logout }) => {
);
}
-const mapStateToProps = state => ({
- open: state.admin.ui.sidebarOpen,
- resources: getResources(state),
-});
-
-export default withRouter(connect(mapStateToProps)(Menu));
+export default withRouter(Menu);
```
**Tip**: Note the `MenuItemLink` component. It must be used to avoid unwanted side effects in mobile views. It supports a custom text and icon (which must be a material-ui `
`).
**Tip**: Note that we include the `logout` item only on small devices. Indeed, the `logout` button is already displayed in the AppBar on larger devices.
-**Tip**: Note that we use React Router [`withRouter`](https://reacttraining.com/react-router/web/api/withRouter) Higher Order Component and that it is used **before** Redux [`connect](https://github.com/reactjs/react-redux/blob/master/docs/api.html#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options). This is required if you want the active menu item to be highlighted.
-
Then, pass it to the `` component as the `menu` prop:
```jsx
diff --git a/docs/Authorization.md b/docs/Authorization.md
index f0a36ff08fd..c9dd64b8c4b 100644
--- a/docs/Authorization.md
+++ b/docs/Authorization.md
@@ -244,20 +244,19 @@ What if you want to check the permissions inside a [custom menu](./Admin.md#menu
```jsx
// in src/myMenu.js
import React from 'react';
-import { connect } from 'react-redux';
import { MenuItemLink, usePermissions } from 'react-admin';
const Menu = ({ onMenuClick, logout }) => {
const { permissions } = usePermissions();
return (
-
-
-
- { permissions === 'admin' &&
-
- }
- {logout}
-
-);
+
+
+
+ {permissions === 'admin' &&
+
+ }
+ {logout}
+
+ );
}
```
diff --git a/docs/Theming.md b/docs/Theming.md
index e002bc84ebf..5f98d87b83f 100644
--- a/docs/Theming.md
+++ b/docs/Theming.md
@@ -720,8 +720,6 @@ export default withRouter(Menu);
**Tip**: Note that we include the `logout` item only on small devices. Indeed, the `logout` button is already displayed in the AppBar on larger devices.
-**Tip**: Note that we use React Router [`withRouter`](https://reacttraining.com/react-router/web/api/withRouter) Higher Order Component and that it is used **before** Redux [`connect](https://github.com/reactjs/react-redux/blob/master/docs/api.html#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options). This is required if you want the active menu item to be highlighted.
-
**Tip**: The `primaryText` prop accepts a React node. You can pass a custom element in it. For example:
```jsx
diff --git a/docs/UnitTesting.md b/docs/UnitTesting.md
index 4bd6c47b12c..4abc4e35c88 100644
--- a/docs/UnitTesting.md
+++ b/docs/UnitTesting.md
@@ -68,7 +68,7 @@ This means that reducers will work as they will within the app.
## Spying on the store 'dispatch'
-If you are using `mapDispatch` within connected components, it is likely you will want to test that actions have been dispatched with the correct arguments. You can return the `store` being used within the tests using a `renderProp`.
+If you are using `useDispatch` within your components, it is likely you will want to test that actions have been dispatched with the correct arguments. You can return the `store` being used within the tests using a `renderProp`.
```jsx
let dispatchSpy;
@@ -87,7 +87,6 @@ it('should send the user to another url', () => {
});
```
-
## Testing Permissions
As explained on the [Authorization page](./Authorization.md), it's possible to manage permissions via the authentication provider in order to filter page and fields the users can see.
diff --git a/examples/demo/src/configuration/Configuration.js b/examples/demo/src/configuration/Configuration.js
index a8b5651bb3b..0dabcc8ba8e 100644
--- a/examples/demo/src/configuration/Configuration.js
+++ b/examples/demo/src/configuration/Configuration.js
@@ -1,11 +1,10 @@
import React from 'react';
-import { connect } from 'react-redux';
+import { useSelector, useDispatch } from 'react-redux';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button';
import { useTranslate, changeLocale, Title } from 'react-admin';
import { makeStyles } from '@material-ui/core/styles';
-import compose from 'recompose/compose';
import { changeTheme } from './actions';
const useStyles = makeStyles({
@@ -13,9 +12,12 @@ const useStyles = makeStyles({
button: { margin: '1em' },
});
-const Configuration = ({ theme, locale, changeTheme, changeLocale }) => {
+const Configuration = () => {
const translate = useTranslate();
const classes = useStyles();
+ const theme = useSelector(state => state.theme);
+ const locale = useSelector(state => state.i18n.locale);
+ const dispatch = useDispatch();
return (
@@ -27,7 +29,7 @@ const Configuration = ({ theme, locale, changeTheme, changeLocale }) => {
variant="contained"
className={classes.button}
color={theme === 'light' ? 'primary' : 'default'}
- onClick={() => changeTheme('light')}
+ onClick={() => dispatch(changeTheme('light'))}
>
{translate('pos.theme.light')}
@@ -35,7 +37,7 @@ const Configuration = ({ theme, locale, changeTheme, changeLocale }) => {
variant="contained"
className={classes.button}
color={theme === 'dark' ? 'primary' : 'default'}
- onClick={() => changeTheme('dark')}
+ onClick={() => dispatch(changeTheme('dark'))}
>
{translate('pos.theme.dark')}
@@ -46,7 +48,7 @@ const Configuration = ({ theme, locale, changeTheme, changeLocale }) => {
variant="contained"
className={classes.button}
color={locale === 'en' ? 'primary' : 'default'}
- onClick={() => changeLocale('en')}
+ onClick={() => dispatch(changeLocale('en'))}
>
en
@@ -54,7 +56,7 @@ const Configuration = ({ theme, locale, changeTheme, changeLocale }) => {
variant="contained"
className={classes.button}
color={locale === 'fr' ? 'primary' : 'default'}
- onClick={() => changeLocale('fr')}
+ onClick={() => dispatch(changeLocale('fr'))}
>
fr
@@ -63,19 +65,4 @@ const Configuration = ({ theme, locale, changeTheme, changeLocale }) => {
);
};
-const mapStateToProps = state => ({
- theme: state.theme,
- locale: state.i18n.locale,
-});
-
-const enhance = compose(
- connect(
- mapStateToProps,
- {
- changeLocale,
- changeTheme,
- }
- )
-);
-
-export default enhance(Configuration);
+export default Configuration;
diff --git a/examples/demo/src/layout/Layout.js b/examples/demo/src/layout/Layout.js
index c23ad56e2c6..471327203ea 100644
--- a/examples/demo/src/layout/Layout.js
+++ b/examples/demo/src/layout/Layout.js
@@ -1,18 +1,23 @@
import React from 'react';
-import { connect } from 'react-redux';
+import { useSelector } from 'react-redux';
import { Layout, Sidebar } from 'react-admin';
import AppBar from './AppBar';
import Menu from './Menu';
import { darkTheme, lightTheme } from './themes';
const CustomSidebar = props => ;
-const CustomLayout = props => (
-
-);
-export default connect(
- state => ({
- theme: state.theme === 'dark' ? darkTheme : lightTheme,
- }),
- {}
-)(CustomLayout);
+export default props => {
+ const theme = useSelector(state =>
+ state.theme === 'dark' ? darkTheme : lightTheme
+ );
+ return (
+
+ );
+};
diff --git a/examples/demo/src/layout/Menu.js b/examples/demo/src/layout/Menu.js
index 1242d22eeaf..52404cf552f 100644
--- a/examples/demo/src/layout/Menu.js
+++ b/examples/demo/src/layout/Menu.js
@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import compose from 'recompose/compose';
+import { useSelector } from 'react-redux';
import SettingsIcon from '@material-ui/icons/Settings';
import LabelIcon from '@material-ui/icons/Label';
import { useMediaQuery } from '@material-ui/core';
@@ -16,7 +15,7 @@ import categories from '../categories';
import reviews from '../reviews';
import SubMenu from './SubMenu';
-const Menu = ({ onMenuClick, open, logout }) => {
+const Menu = ({ onMenuClick, logout }) => {
const [state, setState] = useState({
menuCatalog: false,
menuSales: false,
@@ -24,6 +23,9 @@ const Menu = ({ onMenuClick, open, logout }) => {
});
const translate = useTranslate();
const isXsmall = useMediaQuery(theme => theme.breakpoints.down('xs'));
+ const open = useSelector(state => state.admin.ui.sidebarOpen);
+ useSelector(state => state.theme); // force rerender on theme change
+ useSelector(state => state.i18n.locale); // force rerender on locale change
const handleToggle = menu => {
setState(state => ({ ...state, [menu]: !state[menu] }));
@@ -141,18 +143,4 @@ Menu.propTypes = {
logout: PropTypes.object,
};
-const mapStateToProps = state => ({
- open: state.admin.ui.sidebarOpen,
- theme: state.theme,
- locale: state.i18n.locale,
-});
-
-const enhance = compose(
- withRouter,
- connect(
- mapStateToProps,
- {}
- )
-);
-
-export default enhance(Menu);
+export default withRouter(Menu);
diff --git a/packages/ra-core/src/dataProvider/withDataProvider.tsx b/packages/ra-core/src/dataProvider/withDataProvider.tsx
index ca0535ea6f4..1cc8f3b4c12 100644
--- a/packages/ra-core/src/dataProvider/withDataProvider.tsx
+++ b/packages/ra-core/src/dataProvider/withDataProvider.tsx
@@ -20,9 +20,6 @@ export interface DataProviderProps {
* the injected dataProvider prop accepts a fourth parameter, an object literal
* which may contain side effects, of make the action optimistic (with undoable: true).
*
- * As it uses connect() from react-redux, this HOC also injects the dispatch prop,
- * allowing developers to dispatch additional actions upon completion.
- *
* @example
*
* import { withDataProvider, showNotification } from 'react-admin';
diff --git a/packages/ra-core/src/form/withDefaultValue.tsx b/packages/ra-core/src/form/withDefaultValue.tsx
index e36a0e79974..38e157e115a 100644
--- a/packages/ra-core/src/form/withDefaultValue.tsx
+++ b/packages/ra-core/src/form/withDefaultValue.tsx
@@ -10,6 +10,9 @@ export interface DefaultValueProps extends InputProps {
initializeForm: typeof initializeFormAction;
}
+/**
+ * @deprecated
+ */
export class DefaultValueView extends Component {
static propTypes = {
decoratedComponent: PropTypes.oneOfType([
diff --git a/packages/ra-ui-materialui/src/button/RefreshButton.js b/packages/ra-ui-materialui/src/button/RefreshButton.js
index a75ce9e39f8..90be0025fc3 100644
--- a/packages/ra-ui-materialui/src/button/RefreshButton.js
+++ b/packages/ra-ui-materialui/src/button/RefreshButton.js
@@ -1,47 +1,41 @@
-import React, { Component } from 'react';
+import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
+import { useDispatch } from 'react-redux';
import NavigationRefresh from '@material-ui/icons/Refresh';
-import { refreshView as refreshViewAction } from 'ra-core';
+import { refreshView } from 'ra-core';
import Button from './Button';
-class RefreshButton extends Component {
- static propTypes = {
- label: PropTypes.string,
- refreshView: PropTypes.func.isRequired,
- icon: PropTypes.element,
- };
-
- static defaultProps = {
- label: 'ra.action.refresh',
- icon: ,
- };
-
- handleClick = event => {
- const { refreshView, onClick } = this.props;
- event.preventDefault();
- refreshView();
-
- if (typeof onClick === 'function') {
- onClick();
- }
- };
-
- render() {
- const { label, refreshView, icon, ...rest } = this.props;
-
- return (
-
- {icon}
-
- );
- }
-}
-
-const enhance = connect(
- null,
- { refreshView: refreshViewAction }
-);
-
-export default enhance(RefreshButton);
+const defaultIcon = ;
+
+const RefreshButton = ({
+ label = 'ra.action.refresh',
+ icon = defaultIcon,
+ onClick,
+ ...rest
+}) => {
+ const dispatch = useDispatch();
+ const handleClick = useCallback(
+ event => {
+ event.preventDefault();
+ dispatch(refreshView());
+ if (typeof onClick === 'function') {
+ onClick();
+ }
+ },
+ [dispatch, onClick]
+ );
+
+ return (
+
+ {icon}
+
+ );
+};
+
+RefreshButton.propTypes = {
+ label: PropTypes.string,
+ icon: PropTypes.element,
+};
+
+export default RefreshButton;
diff --git a/packages/ra-ui-materialui/src/button/RefreshIconButton.js b/packages/ra-ui-materialui/src/button/RefreshIconButton.js
index c936bed37e8..d090868fe93 100644
--- a/packages/ra-ui-materialui/src/button/RefreshIconButton.js
+++ b/packages/ra-ui-materialui/src/button/RefreshIconButton.js
@@ -1,67 +1,52 @@
-import React, { Component } from 'react';
+import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import compose from 'recompose/compose';
+import { useDispatch } from 'react-redux';
import Tooltip from '@material-ui/core/Tooltip';
import IconButton from '@material-ui/core/IconButton';
import NavigationRefresh from '@material-ui/icons/Refresh';
-import { refreshView, translate } from 'ra-core';
+import { refreshView, useTranslate } from 'ra-core';
-class RefreshButton extends Component {
- static propTypes = {
- className: PropTypes.string,
- label: PropTypes.string,
- refreshView: PropTypes.func.isRequired,
- translate: PropTypes.func.isRequired,
- icon: PropTypes.element,
- };
+const defaultIcon = ;
- static defaultProps = {
- label: 'ra.action.refresh',
- icon: ,
- };
+const RefreshIconButton = ({
+ label = 'ra.action.refresh',
+ icon = defaultIcon,
+ onClick,
+ className,
+ ...rest
+}) => {
+ const dispatch = useDispatch();
+ const translate = useTranslate();
+ const handleClick = useCallback(
+ event => {
+ event.preventDefault();
+ dispatch(refreshView());
+ if (typeof onClick === 'function') {
+ onClick();
+ }
+ },
+ [dispatch, onClick]
+ );
- handleClick = event => {
- const { refreshView, onClick } = this.props;
- event.preventDefault();
- refreshView();
+ return (
+
+
+ {icon}
+
+
+ );
+};
- if (typeof onClick === 'function') {
- onClick();
- }
- };
+RefreshIconButton.propTypes = {
+ className: PropTypes.string,
+ label: PropTypes.string,
+ icon: PropTypes.element,
+};
- render() {
- const {
- className,
- label,
- refreshView,
- translate,
- icon,
- ...rest
- } = this.props;
-
- return (
-
-
- {icon}
-
-
- );
- }
-}
-
-const enhance = compose(
- connect(
- null,
- { refreshView }
- ),
- translate
-);
-export default enhance(RefreshButton);
+export default RefreshIconButton;
diff --git a/packages/ra-ui-materialui/src/button/SaveButton.js b/packages/ra-ui-materialui/src/button/SaveButton.js
index cfac5409699..19539fa1955 100644
--- a/packages/ra-ui-materialui/src/button/SaveButton.js
+++ b/packages/ra-ui-materialui/src/button/SaveButton.js
@@ -1,13 +1,11 @@
-import React, { cloneElement, useCallback } from 'react';
+import React, { cloneElement } from 'react';
import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import compose from 'recompose/compose';
import Button from '@material-ui/core/Button';
import CircularProgress from '@material-ui/core/CircularProgress';
import { makeStyles } from '@material-ui/core/styles';
import ContentSave from '@material-ui/icons/Save';
import classnames from 'classnames';
-import { showNotification, translate } from 'ra-core';
+import { useTranslate, useNotify } from 'ra-core';
const useStyles = makeStyles(theme => ({
button: {
@@ -29,7 +27,6 @@ const sanitizeRestProps = ({
label,
invalid,
variant,
- translate,
handleSubmit,
handleSubmitWithRedirect,
submitOnEnter,
@@ -37,7 +34,6 @@ const sanitizeRestProps = ({
redirect,
resource,
locale,
- showNotification,
undoable,
...rest
}) => rest;
@@ -51,15 +47,15 @@ export function SaveButton({
redirect,
saving,
submitOnEnter,
- translate,
variant = 'contained',
icon,
onClick,
handleSubmitWithRedirect,
- showNotification,
...rest
}) {
const classes = useStyles({ classes: classesOverride });
+ const notify = useNotify();
+ const translate = useTranslate();
// We handle the click event through mousedown because of an issue when
// the button is not as the same place when mouseup occurs, preventing the click
@@ -72,7 +68,7 @@ export function SaveButton({
event.preventDefault();
} else {
if (invalid) {
- showNotification('ra.message.invalid_form', 'warning');
+ notify('ra.message.invalid_form', 'warning');
}
// always submit form explicitly regardless of button type
if (event) {
@@ -135,9 +131,7 @@ SaveButton.propTypes = {
PropTypes.func,
]),
saving: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
- showNotification: PropTypes.func,
submitOnEnter: PropTypes.bool,
- translate: PropTypes.func.isRequired,
variant: PropTypes.oneOf(['text', 'outlined', 'contained']),
icon: PropTypes.element,
};
@@ -147,12 +141,4 @@ SaveButton.defaultProps = {
icon: ,
};
-const enhance = compose(
- translate,
- connect(
- undefined,
- { showNotification }
- )
-);
-
-export default enhance(SaveButton);
+export default SaveButton;
diff --git a/packages/ra-ui-materialui/src/layout/LoadingIndicator.js b/packages/ra-ui-materialui/src/layout/LoadingIndicator.js
index 464275760cb..812b1594215 100644
--- a/packages/ra-ui-materialui/src/layout/LoadingIndicator.js
+++ b/packages/ra-ui-materialui/src/layout/LoadingIndicator.js
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import { connect } from 'react-redux';
+import { useSelector } from 'react-redux';
import { makeStyles } from '@material-ui/core/styles';
import CircularProgress from '@material-ui/core/CircularProgress';
@@ -16,11 +16,11 @@ const useStyles = makeStyles({
export const LoadingIndicator = ({
classes: classesOverride,
className,
- isLoading,
...rest
}) => {
+ const loading = useSelector(state => state.admin.loading > 0);
const classes = useStyles({ classes: classesOverride });
- return isLoading ? (
+ return loading ? (
({
- isLoading: state.admin.loading > 0,
-});
-
-export default connect(
- mapStateToProps,
- {} // Avoid connect passing dispatch in props
-)(LoadingIndicator);
+export default LoadingIndicator;