Skip to content

Commit

Permalink
Replace connect by hooks in easy components
Browse files Browse the repository at this point in the history
  • Loading branch information
fzaninotto committed Sep 3, 2019
1 parent 9b08ceb commit 84db55b
Show file tree
Hide file tree
Showing 13 changed files with 138 additions and 209 deletions.
15 changes: 5 additions & 10 deletions docs/Admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
{resources.map(resource => (
Expand All @@ -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 `<SvgIcon>`).

**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 `<Admin>` component as the `menu` prop:

```jsx
Expand Down
19 changes: 9 additions & 10 deletions docs/Authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
<MenuItemLink to="/posts" primaryText="Posts" onClick={onMenuClick} />
<MenuItemLink to="/comments" primaryText="Comments" onClick={onMenuClick} />
{ permissions === 'admin' &&
<MenuItemLink to="/custom-route" primaryText="Miscellaneous" onClick={onMenuClick} />
}
{logout}
</div>
);
<div>
<MenuItemLink to="/posts" primaryText="Posts" onClick={onMenuClick} />
<MenuItemLink to="/comments" primaryText="Comments" onClick={onMenuClick} />
{permissions === 'admin' &&
<MenuItemLink to="/custom-route" primaryText="Miscellaneous" onClick={onMenuClick} />
}
{logout}
</div>
);
}
```
2 changes: 0 additions & 2 deletions docs/Theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions docs/UnitTesting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down
33 changes: 10 additions & 23 deletions examples/demo/src/configuration/Configuration.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
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({
label: { width: '10em', display: 'inline-block' },
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 (
<Card>
<Title title={translate('pos.configuration')} />
Expand All @@ -27,15 +29,15 @@ 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')}
</Button>
<Button
variant="contained"
className={classes.button}
color={theme === 'dark' ? 'primary' : 'default'}
onClick={() => changeTheme('dark')}
onClick={() => dispatch(changeTheme('dark'))}
>
{translate('pos.theme.dark')}
</Button>
Expand All @@ -46,15 +48,15 @@ const Configuration = ({ theme, locale, changeTheme, changeLocale }) => {
variant="contained"
className={classes.button}
color={locale === 'en' ? 'primary' : 'default'}
onClick={() => changeLocale('en')}
onClick={() => dispatch(changeLocale('en'))}
>
en
</Button>
<Button
variant="contained"
className={classes.button}
color={locale === 'fr' ? 'primary' : 'default'}
onClick={() => changeLocale('fr')}
onClick={() => dispatch(changeLocale('fr'))}
>
fr
</Button>
Expand All @@ -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;
25 changes: 15 additions & 10 deletions examples/demo/src/layout/Layout.js
Original file line number Diff line number Diff line change
@@ -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 => <Sidebar {...props} size={200} />;
const CustomLayout = props => (
<Layout {...props} appBar={AppBar} sidebar={CustomSidebar} menu={Menu} />
);

export default connect(
state => ({
theme: state.theme === 'dark' ? darkTheme : lightTheme,
}),
{}
)(CustomLayout);
export default props => {
const theme = useSelector(state =>
state.theme === 'dark' ? darkTheme : lightTheme
);
return (
<Layout
{...props}
appBar={AppBar}
sidebar={CustomSidebar}
menu={Menu}
theme={theme}
/>
);
};
24 changes: 6 additions & 18 deletions examples/demo/src/layout/Menu.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -16,14 +15,17 @@ 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,
menuCustomers: false,
});
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] }));
Expand Down Expand Up @@ -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);
3 changes: 0 additions & 3 deletions packages/ra-core/src/dataProvider/withDataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 3 additions & 0 deletions packages/ra-core/src/form/withDefaultValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export interface DefaultValueProps extends InputProps {
initializeForm: typeof initializeFormAction;
}

/**
* @deprecated
*/
export class DefaultValueView extends Component<any> {
static propTypes = {
decoratedComponent: PropTypes.oneOfType([
Expand Down
78 changes: 36 additions & 42 deletions packages/ra-ui-materialui/src/button/RefreshButton.js
Original file line number Diff line number Diff line change
@@ -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: <NavigationRefresh />,
};

handleClick = event => {
const { refreshView, onClick } = this.props;
event.preventDefault();
refreshView();

if (typeof onClick === 'function') {
onClick();
}
};

render() {
const { label, refreshView, icon, ...rest } = this.props;

return (
<Button label={label} onClick={this.handleClick} {...rest}>
{icon}
</Button>
);
}
}

const enhance = connect(
null,
{ refreshView: refreshViewAction }
);

export default enhance(RefreshButton);
const defaultIcon = <NavigationRefresh />;

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 (
<Button label={label} onClick={handleClick} {...rest}>
{icon}
</Button>
);
};

RefreshButton.propTypes = {
label: PropTypes.string,
icon: PropTypes.element,
};

export default RefreshButton;
Loading

0 comments on commit 84db55b

Please sign in to comment.