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] Data hooks #3181

Merged
merged 12 commits into from
May 4, 2019
Merged
33 changes: 33 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,36 @@ If you're using a Custom App, you had to render Resource components with the reg
- <Resource name="users" context="registration" />
+ <Resource name="users" intent="registration" />
```

## `withDataProvider` no longer injects `dispatch`

The `withDataProvider` HOC used to inject two props: `dataProvider`, and redux' `dispatch`. This last prop is now easy to get via the `useDispatch` hook from Redux, so `withDataProvider` no longer injects it.

```diff
import {
showNotification,
UPDATE,
withDataProvider,
} from 'react-admin';
+ import { useDispatch } from 'react-redux';

-const ApproveButton = ({ dataProvider, dispatch, record }) => {
+const ApproveButton = ({ dataProvider, record }) => {
+ const dispatch = withDispatch();
const handleClick = () => {
const updatedRecord = { ...record, is_approved: true };
dataProvider(UPDATE, 'comments', { id: record.id, data: updatedRecord })
.then(() => {
dispatch(showNotification('Comment approved'));
dispatch(push('/comments'));
})
.catch((e) => {
dispatch(showNotification('Error: comment not approved', 'warning'))
});
}

return <Button label="Approve" onClick={handleClick} />;
}

export default withDataProvider(ApproveButton);
```
716 changes: 332 additions & 384 deletions docs/Actions.md

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions docs/_layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -713,13 +713,10 @@
</a>
<ul class="articles" {% if page.path !='Actions.md' %}style="display:none" {% endif %}>
<li class="chapter">
<a href="#the-basic-way-using-fetch">The Basic Way: Using <code>fetch</code></a>
<a href="#usequery-hook"><code>useQuery</code></a>
</li>
<li class="chapter">
<a href="#using-the-data-provider-instead-of-fetch">Using the <code>dataProvider</code></a>
</li>
<li class="chapter">
<a href="#using-the-withdataprovider-decorator">Using <code>withDataProvider</code></a>
<a href="#usemutation-hook"><code>useMutation</code></a>
</li>
<li class="chapter">
<a href="#handling-side-effects">Handling Side Effects</a>
Expand All @@ -728,8 +725,14 @@
<a href="#optimistic-rendering-and-undo">Optimistic Rendering and Undo</a>
</li>
<li class="chapter">
<a href="#query-and-mutation-components"><code>&lt;Query&gt;</code> and <code>&lt;Mutation&gt;</code></a>
<a href="#usedataprovider-hook"><code>useDataProvider</code></a>
</li>
<li class="chapter">
<a href="#legacy-components-query-mutation-and-withdataprovider"><code>&lt;Query&gt;</code> and <code>&lt;Mutation&gt;</code></a>
</li>
<li class="chapter">
<a href="#querying-the-api-with-fetch">Querying The API With <code>fetch</code></a>
</li>
<li class="chapter">
<a href="#using-a-custom-action-creator">Using a Custom Action Creator</a>
</li>
Expand Down
30 changes: 1 addition & 29 deletions examples/demo/src/dashboard/Dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ class Dashboard extends Component {
fetchData() {
this.fetchOrders();
this.fetchReviews();
this.fetchCustomers();
}

async fetchOrders() {
Expand Down Expand Up @@ -112,34 +111,10 @@ class Dashboard extends Component {
});
}

async fetchCustomers() {
const { dataProvider } = this.props;
const aMonthAgo = new Date();
aMonthAgo.setDate(aMonthAgo.getDate() - 30);
const { data: newCustomers } = await dataProvider(
GET_LIST,
'customers',
{
filter: {
has_ordered: true,
first_seen_gte: aMonthAgo.toISOString(),
},
sort: { field: 'first_seen', order: 'DESC' },
pagination: { page: 1, perPage: 100 },
}
);
this.setState({
newCustomers,
nbNewCustomers: newCustomers.reduce(nb => ++nb, 0),
});
}

render() {
const {
nbNewCustomers,
nbNewOrders,
nbPendingReviews,
newCustomers,
pendingOrders,
pendingOrdersCustomers,
pendingReviews,
Expand Down Expand Up @@ -208,10 +183,7 @@ class Dashboard extends Component {
reviews={pendingReviews}
customers={pendingReviewsCustomers}
/>
<NewCustomers
nb={nbNewCustomers}
visitors={newCustomers}
/>
<NewCustomers />
</div>
</div>
</div>
Expand Down
99 changes: 60 additions & 39 deletions examples/demo/src/dashboard/NewCustomers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import compose from 'recompose/compose';
import Card from '@material-ui/core/Card';
import List from '@material-ui/core/List';
Expand All @@ -10,7 +10,7 @@ import Typography from '@material-ui/core/Typography';
import CustomerIcon from '@material-ui/icons/PersonAdd';
import Divider from '@material-ui/core/Divider';
import { Link } from 'react-router-dom';
import { translate } from 'react-admin';
import { translate, useQuery, GET_LIST } from 'react-admin';

import CardIcon from './CardIcon';

Expand Down Expand Up @@ -40,43 +40,64 @@ const styles = theme => ({
},
});

const NewCustomers = ({ visitors = [], nb, translate, classes }) => (
<div className={classes.main}>
<CardIcon Icon={CustomerIcon} bgColor="#4caf50" />
<Card className={classes.card}>
<Typography className={classes.title} color="textSecondary">
{translate('pos.dashboard.new_customers')}
</Typography>
<Typography
variant="headline"
component="h2"
className={classes.value}
>
{nb}
</Typography>
<Divider />
<List>
{visitors.map(record => (
<ListItem
button
to={`/customers/${record.id}`}
component={Link}
key={record.id}
>
<Avatar
src={`${record.avatar}?size=32x32`}
className={classes.avatar}
/>
<ListItemText
primary={`${record.first_name} ${record.last_name}`}
className={classes.listItemText}
/>
</ListItem>
))}
</List>
</Card>
</div>
);
const NewCustomers = ({ translate, classes }) => {
const aMonthAgo = useMemo(() => {
const date = new Date();
date.setDate(date.getDate() - 30);
return date;
}, []);

const { loaded, data: visitors } = useQuery(GET_LIST, 'customers', {
filter: {
has_ordered: true,
first_seen_gte: aMonthAgo.toISOString(),
},
sort: { field: 'first_seen', order: 'DESC' },
pagination: { page: 1, perPage: 100 },
});

if (!loaded) return null;
const nb = visitors.reduce(nb => ++nb, 0);
return (
<div className={classes.main}>
<CardIcon Icon={CustomerIcon} bgColor="#4caf50" />
<Card className={classes.card}>
<Typography className={classes.title} color="textSecondary">
{translate('pos.dashboard.new_customers')}
</Typography>
<Typography
variant="headline"
component="h2"
className={classes.value}
>
{nb}
</Typography>
<Divider />
<List>
{visitors.map(record => (
<ListItem
button
to={`/customers/${record.id}`}
component={Link}
key={record.id}
>
<Avatar
src={`${record.avatar}?size=32x32`}
className={classes.avatar}
/>
<ListItemText
primary={`${record.first_name} ${
record.last_name
}`}
className={classes.listItemText}
/>
</ListItem>
))}
</List>
</Card>
</div>
);
};

const enhance = compose(
withStyles(styles),
Expand Down
48 changes: 24 additions & 24 deletions examples/demo/src/reviews/AcceptButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { connect } from 'react-redux';
import { formValueSelector } from 'redux-form';
import Button from '@material-ui/core/Button';
import ThumbUp from '@material-ui/icons/ThumbUp';
import { translate, Mutation } from 'react-admin';
import { translate, useMutation } from 'react-admin';
import compose from 'recompose/compose';

const sideEffects = {
undoable: true,
onSuccess: {
notification: {
body: 'resources.reviews.notification.approved_success',
Expand All @@ -24,34 +25,33 @@ const sideEffects = {
};

/**
* This custom button demonstrate using <Mutation> to update data
* This custom button demonstrate using useMutation to update data
*/
const AcceptButton = ({ record, translate }) =>
record && record.status === 'pending' ? (
<Mutation
type="UPDATE"
resource="reviews"
payload={{ id: record.id, data: { status: 'accepted' } }}
options={sideEffects}
const AcceptButton = ({ record, translate }) => {
const [approve, { loading }] = useMutation(
'UPDATE',
'reviews',
{ id: record.id, data: { status: 'accepted' } },
sideEffects
);
return record && record.status === 'pending' ? (
<Button
variant="outlined"
color="primary"
size="small"
onClick={approve}
disabled={loading}
>
{approve => (
<Button
variant="outlined"
color="primary"
size="small"
onClick={approve}
>
<ThumbUp
color="primary"
style={{ paddingRight: '0.5em', color: 'green' }}
/>
{translate('resources.reviews.action.accept')}
</Button>
)}
</Mutation>
<ThumbUp
color="primary"
style={{ paddingRight: '0.5em', color: 'green' }}
/>
{translate('resources.reviews.action.accept')}
</Button>
) : (
<span />
);
};

AcceptButton.propTypes = {
record: PropTypes.object,
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"react": "~16.8.0",
"react-dom": "~16.8.0",
"react-test-renderer": "~16.8.6",
"react-testing-library": "^5.2.3",
"react-testing-library": "^7.0.0",
"rimraf": "^2.6.3"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import expect from 'expect';
import Mutation from './Mutation';
import CoreAdmin from '../CoreAdmin';
import Resource from '../Resource';
import TestContext from './TestContext';
import TestContext from '../util/TestContext';

describe('Mutation', () => {
afterEach(cleanup);
Expand Down
53 changes: 53 additions & 0 deletions packages/ra-core/src/fetch/Mutation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { FunctionComponent, ReactElement } from 'react';
import useMutation from './useMutation';

type DataProviderCallback = (
type: string,
resource: string,
payload?: any,
options?: any
) => Promise<any>;

interface ChildrenFuncParams {
data?: any;
loading: boolean;
error?: any;
}

interface Props {
children: (
mutate: () => void,
params: ChildrenFuncParams
) => ReactElement<any, any>;
type: string;
resource: string;
payload?: any;
options?: any;
}

/**
* Craft a callback to fetch the data provider and pass it to a child function
*
* @example
*
* const ApproveButton = ({ record }) => (
* <Mutation
* type="UPDATE"
* resource="comments"
* payload={{ id: record.id, data: { isApproved: true } }}
* >
* {(approve) => (
* <FlatButton label="Approve" onClick={approve} />
* )}
* </Mutation>
* );
*/
const Mutation: FunctionComponent<Props> = ({
children,
type,
resource,
payload,
options,
}) => children(...useMutation(type, resource, payload, options));

export default Mutation;
Loading