Skip to content

Commit

Permalink
Merge pull request #3181 from marmelab/data-hooks
Browse files Browse the repository at this point in the history
[BC Break] [RFR] Data hooks
  • Loading branch information
Gildas Garcia authored May 4, 2019
2 parents 13aa576 + f0c74e6 commit 91bfb83
Show file tree
Hide file tree
Showing 25 changed files with 1,013 additions and 828 deletions.
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
File renamed without changes.
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

0 comments on commit 91bfb83

Please sign in to comment.