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

[RFR] Replace connect by hooks in a few components #3643

Merged
merged 3 commits into from
Sep 3, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
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