Querying the API

Admin interfaces often have to query the API beyond CRUD requests. For instance, in an administration for comments, an “Approve” button (allowing to update the is_approved property and to save the updated record in one click) - is a must have.

How can you add such custom actions with react-admin? There are several answers to that question, and you should understand the strengths and drawbacks of each solution before choosing one.

Tip: If you don’t have the time to read this entire chapter, head to the <Query> and <Mutation> components section. It’s the best choice in 90% of the cases.

The Basic Way: Using fetch

Here is an implementation of the “Approve” button using the browser fetch() function that works fine:

// in src/comments/ApproveButton.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Button from '@material-ui/core/Button';
import { showNotification } from 'react-admin';
import { push } from 'react-router-redux';

class ApproveButton extends Component {
    handleClick = () => {
        const { push, record, showNotification } = this.props;
        const updatedRecord = { ...record, is_approved: true };
        fetch(`/comments/${record.id}`, { method: 'PUT', body: updatedRecord })
            .then(() => {
                showNotification('Comment approved');
                push('/comments');
            })
            .catch((e) => {
                showNotification('Error: comment not approved', 'warning')
            });
    }

    render() {
        return <Button label="Approve" onClick={this.handleClick} />;
    }
}

ApproveButton.propTypes = {
    push: PropTypes.func,
    record: PropTypes.object,
    showNotification: PropTypes.func,
};

export default connect(null, {
    showNotification,
    push,
})(ApproveButton);

The handleClick function makes a PUT request the REST API with fetch, then displays a notification (with showNotification) and redirects to the comments list page (with push);

showNotification and push are action creators. This is a Redux term for functions that return a simple action object. When given an object of action creators in the second argument, connect() will decorate each action creator with Redux’ dispatch method, so in the handleClick function, a call to showNotification() is actually a call to dispatch(showNotification()).

This ApproveButton can be used right away, for instance in the list of comments, where <Datagrid> automatically injects the record to its children:

// in src/comments/index.js
import ApproveButton from './ApproveButton';

export const CommentList = (props) =>
    <List {...props}>
        <Datagrid>
            <DateField source="created_at" />
            <TextField source="author.name" />
            <TextField source="body" />
            <BooleanField source="is_approved" />
            <ApproveButton />
        </Datagrid>
    </List>;

Or, in the <Edit> page, as a custom action:

// in src/comments/CommentEditActions.js
import React from 'react';
import CardActions from '@material-ui/core/CardActions';
import ApproveButton from './ApproveButton';

const cardActionStyle = {
    zIndex: 2,
    display: 'inline-block',
    float: 'right',
};

const CommentEditActions = ({ basePath, data, resource }) => (
    <CardActions style={cardActionStyle}>
        <ApproveButton record={data} />
    </CardActions>
);

export default CommentEditActions;

// in src/comments/index.js
import CommentEditActions from './CommentEditActions';

export const CommentEdit = (props) =>
    <Edit {...props} actions={<CommentEditActions />}>
        ...
    </Edit>;

Using The Data Provider Instead of Fetch

The previous code uses fetch(), which means it makes HTTP requests directly. But APIs often require a bit of HTTP plumbing to deal with authentication, query parameters, encoding, headers, etc. It turns out you probably already have a function that maps from a REST request to an HTTP request: the Data Provider. So it’s a good idea to use this function instead of fetch - provided you have exported it:

// in src/dataProvider.js
import jsonServerProvider from 'ra-data-json-server';
export default jsonServerProvider('http://Mydomain.com/api/');

The dataProvider function returns a Promise, so the difference with fetch is minimal:

// in src/comments/ApproveButton.js
-import { showNotification } from 'react-admin';
+import { showNotification, UPDATE } from 'react-admin';
+import dataProvider from '../dataProvider';

class ApproveButton extends Component {
    handleClick = () => {
        const { push, record, showNotification } = this.props;
        const updatedRecord = { ...record, is_approved: true };
-       fetch(`/comments/${record.id}`, { method: 'PUT', body: updatedRecord })
+       dataProvider(UPDATE, 'comments', { id: record.id, data: updatedRecord })
            .then(() => {
                showNotification('Comment approved');
                push('/comments');
            })
            .catch((e) => {
                showNotification('Error: comment not approved', 'warning')
            });
    }

    render() {
        return <Button label="Approve" onClick={this.handleClick} />;
    }
}

As a reminder, the signature of the dataProvider function is:

/**
 * Query a data provider and return a promise for a response
 *
 * @example
 * dataProvider(GET_ONE, 'posts', { id: 123 })
 *  => new Promise(resolve => resolve({ id: 123, title: "hello, world" }))
 *
 * @param {string} type Request type, e.g GET_LIST
 * @param {string} resource Resource name, e.g. "posts"
 * @param {Object} payload Request parameters. Depends on the action type
 * @returns {Promise} the Promise for a response
 */
const dataProvider = (type, resource, params) => new Promise();

As for the syntax of the various request types (GET_LIST, GET_ONE, UPDATE, etc.), head to the Data Provider documentation for more details.

Using the withDataProvider Decorator

Using either fetch or the dataProvider has one drawback: while the request is being processed by the server, the UI doesn’t show the loading indicator.

React-admin components don’t call the dataProvider function directly. Instead, they dispatch special Redux actions that react-admin turns into dataProvider calls. This allows react-admin to handle the loading state automatically.

You can use the same feature for your own components. You’ll need to wrap your component with a function called withDataProvider, which injects a dataProvider prop to the component. This dataProvider prop is a function which behaves exactly like your own dataProvider: it has the same signature, and it returns a Promise. The only difference is that it uses Redux under the hood. That means you get a loading indicator! In addition, withDataProvider injects the dispatch function into the component, so you don’t even need to connect() your own component to dispatch actions anymore.

Here is the ApproveButton component modified to use withDataProvider:

// in src/comments/ApproveButton.js
import {
   showNotification,
   UPDATE,
+  withDataProvider,
} from 'react-admin';
-import { connect } from 'react-redux';
-import dataProvider from '../dataProvider';

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

    render() {
        return <Button label="Approve" onClick={this.handleClick} />;
    }
}

ApproveButton.propTypes = {
+   dataProvider: PropTypes.func.isRequired,
+   dispatch: PropTypes.func.isRequired,
-   push: PropTypes.func,
    record: PropTypes.object,
-   showNotification: PropTypes.func,
};

-export default connect(null, {
-    showNotification,
-    push,
-})(ApproveButton);
+export default withDataProvider(ApproveButton)

Handling Side Effects

Fetching data is called a side effect, since it calls the outside world, and is asynchronous. Usual actions may have other side effects, like showing a notification, or redirecting the user to another page. The dataProvider function injected by withDataProvider accepts a fourth parameter, which lets you describe the options of the query, including success and failure side effects. So the previous component can be even further simplified as follows:

// in src/comments/ApproveButton.js
import {
-  showNotification,
   UPDATE,
   withDataProvider,
} from 'react-admin';
-import { push } from 'react-router-redux';

class ApproveButton extends Component {
    handleClick = () => {
        const { dataProvider, dispatch, record } = this.props;
        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'))
-           });
-   }
+       dataProvider(UPDATE, 'comments', { id: record.id, data: updatedRecord }, {
+           onSuccess: {
+               notification: { body: 'Comment approved', level: 'info' },
+               redirectTo: '/comments',
+           },
+           onError: {
+               notification: { body: 'Error: comment not approved', level: 'warning' }
+           }
+       })

    render() {
        return <FlatButton label="Approve" onClick={this.handleClick} />;
    }
}

ApproveButton.propTypes = {
    dataProvider: PropTypes.func.isRequired,
-   dispatch: PropTypes.func.isRequired,
    record: PropTypes.object,
};

export default withDataProvider(ApproveButton);

React-admin can handle the following side effects:

  • notification: Display a notification. The property value should be an object describing the notification to display. The body can be a translation key. level can be either info or warning.
  • redirectTo: Redirect the user to another page. The property value should be the path to redirect the user to.
  • basePath: This is not a side effect, but it’s used internally to compute redirection paths. Set it when you have a redirection side effect.
  • refresh: Force a rerender of the current view (equivalent to pressing the Refresh button). Set to true to enable.
  • unselectAll: Unselect all lines in the current datagrid. Set to true to enable.
  • callback: Execute an arbitrary function. The value should be the function to execute. React-admin will call the function with an object as parameter ({ requestPayload, payload, error }). The payload contains the decoded response body when it’s successful. When it’s failed, the response body is passed in the error.

Optimistic Rendering and Undo

In the previous example, after clicking on the “Approve” button, a spinner displays while the data provider is fetched. Then, users are redirected to the comments list. But in most cases, the server returns a success response, so the user waits for this response for nothing.

For its own fetch actions, react-admin uses an approach called optimistic rendering. The idea is to handle the calls to the dataProvider on the client side first (i.e. updating entities in the Redux store), and re-render the screen immediately. The user sees the effect of their action with no delay. Then, react-admin applies the success side effects, and only after that, it triggers the call to the data provider. If the fetch ends with a success, react-admin does nothing more than a refresh to grab the latest data from the server. In most cases, the user sees no difference (the data in the Redux store and the data from the data provider are the same). If the fetch fails, react-admin shows an error notification, and forces a refresh, too.

As a bonus, while the success notification is displayed, users have the ability to cancel the action before the data provider is even called.

You can benefit from optimistic rendering when you call the dataProvider prop function, too. You just need to pass the undoable: true option in the options parameter:

// in src/comments/ApproveButton.js
class ApproveButton extends Component {
    handleClick = () => {
        const { dataProvider, dispatch, record } = this.props;
        const updatedRecord = { ...record, is_approved: true };
        dataProvider(UPDATE, 'comments', { id: record.id, data: updatedRecord }, {
+           undoable: true,
            onSuccess: {
                notification: { body: 'Comment approved', level: 'info' },
                redirectTo: '/comments',
            },
            onError: {
                notification: { body: 'Error: comment not approved', level: 'warning' }
            }
        })

    render() {
        return <FlatButton label="Approve" onClick={this.handleClick} />;
    }
}

The fact that react-admin can handle side effects and undo a call to the API if you use withDataProvider should be a good motivation to prefer it to raw fetch.

<Query> and <Mutation> Components

When using the withDataProvider decorator to fetch data from the API, you must create a stateful class component to handle the initial state, the loading state, the loaded state, and the error state. That’s a lot of boilerplate for a simple query.

For such cases, react-admin provides a <Query> component, which uses withDataProvider under the hood. It leverages the render props pattern to reduce the boilerplate.

For instance, to fetch and display a user profile in a standalone component:

import { Query } from 'react-admin';

const UserProfile = ({ record }) => (
    <Query type="GET_ONE" resource="users" payload={{ id: record.id }}>
        {({ data, loading, error }) => {
            if (loading) { return <Loading />; }
            if (error) { return <p>ERROR</p>; }
            return <div>User {data.username}</div>;
        }}
    </Query>
);

Or a user list on the dashboard:

const payload = {
   pagination: { page: 1, perPage: 10 },
   sort: { field: 'username', order: 'ASC' },
};

const UserList = () => (
    <Query type="GET_LIST" resource="users" payload={payload}>
        {({ data, total, loading, error }) => {
            if (loading) { return <Loading />; }
            if (error) { return <p>ERROR</p>; }
            return (
                <div>
                    <p>Total users: {total}</p>
                    <ul>
                        {data.map(user => <li key={user.username}>{user.username}</li>)}
                    </ul>
                </div>
            );
        }}
    </Query>
);

Just like the dataProvider injected prop, the <Query> component expects three parameters: type, resource, and payload. It fetches the data provider on mount, and passes the data to its child component once the response from the API arrives.

The <Query> component is designed to read data from the API. When calling the API to update (“mutate”) data, use the <Mutation> component instead. It passes a callback to trigger the API call to its child function. And the <ApproveButton> component from previous sections is a great use case for demonstrating <Mutation>:

import { Mutation } from 'react-admin';

const options = {
    undoable: true,
    onSuccess: {
        notification: { body: 'Comment approved', level: 'info' },
        redirectTo: '/comments',
    },
    onError: {
        notification: { body: 'Error: comment not approved', level: 'warning' }
    }
};

const ApproveButton = ({ record }) => {
    const payload = { id: record.id, data: { ...record, is_approved: true } };
    return (
        <Mutation
            type="UPDATE"
            resource="comments"
            payload={payload}
            options={options}
        >
            {(approve) => (
                <FlatButton label="Approve" onClick={approve} />
            )}
        </Mutation>
    );
}

export default ApproveButton;

Thanks to Query and Mutation, you can use a stateless function component instead of a class component, avoid the decoration with the withDataProvider HOC, and write less code.

And if you need to chain API calls, don’t hesitate to nest <Query> components!

Using a Custom Action Creator

In some rare cases, several components may share the same data fetching logic. In these cases, you will probably want to extract that logic into a custom Redux action.

Warning: This is for advanced use cases only, and it requires a good level of understanding of Redux and react-admin internals. In most cases, withDataProvider is enough.

First, extract the request into a custom action creator. Use the dataProvider verb (UPDATE) as the fetch meta, pass the resource name as the resource meta, and pass the request parameters as the action payload:

// in src/comment/commentActions.js
import { UPDATE } from 'react-admin';

export const COMMENT_APPROVE = 'COMMENT_APPROVE';
export const commentApprove = (id, data, basePath) => ({
    type: COMMENT_APPROVE,
    payload: { id, data: { ...data, is_approved: true } },
    meta: { fetch: UPDATE, resource: 'comments' },
});

Upon dispatch, this action will trigger the call to dataProvider(UPDATE, 'comments', { id, data: { ...data, is_approved: true }), dispatch a COMMENT_APPROVE_LOADING action, then after receiving the response, dispatch either a COMMENT_APPROVE_SUCCESS, or a COMMENT_APPROVE_FAILURE.

To use the new action creator in the component, connect it:

// in src/comments/ApproveButton.js
import { connect } from 'react-redux';
import { commentApprove } from './commentActions';

class ApproveButton extends Component {
    handleClick = () => {
        const { commentApprove, record } = this.props;
        commentApprove(record.id, record);
        // how about push and showNotification?
    }

    render() {
        return <Button onClick={this.handleClick}>Approve</Button>;
    }
}

ApproveButton.propTypes = {
    commentApprove: PropTypes.func.isRequired,,
    record: PropTypes.object,
};

export default connect(null, { commentApprove })(ApproveButton);

It works fine: when a user presses the “Approve” button, the API receives the UPDATE call, and that approves the comment. Another added benefit of using custom actions with the fetch meta is that react-admin automatically handles the loading state, so you don’t need to mess up with fetchStart() and fetchEnd() manually.

But it’s not possible to call push or showNotification in handleClick anymore. This is because commentApprove() returns immediately, whether the API call succeeds or not. How can you run a function only when the action succeeds?

Adding Side Effects to Actions

Just like for the withDataProvider, you can associate side effects to a fetch action declaratively by setting the appropriate keys in the action meta.

So the side effects will be declared in the action creator rather than in the component. For instance, to display a notification when the COMMENT_APPROVE action is successfully dispatched, add the notification meta:

// in src/comment/commentActions.js
import { UPDATE } from 'react-admin';
export const COMMENT_APPROVE = 'COMMENT_APPROVE';
export const commentApprove = (id, data, basePath) => ({
    type: COMMENT_APPROVE,
    payload: { id, data: { ...data, is_approved: true } },
    meta: {
        resource: 'comments',
        fetch: UPDATE,
+       onSuccess: {
+           notification: {
+               body: 'resources.comments.notification.approved_success',
+               level: 'info',
+           },
+           redirectTo: '/comments',
+           basePath,
+       },
+       onFailure: {
+           notification: {
+               body: 'resources.comments.notification.approved_failure',
+               level: 'warning',
+           },
+       },
    },
});

The side effects accepted in the meta field of the action are the same as in the fourth parameter of the dataProvider function injected by withDataProvider:

  • notification: Display a notification. The property value should be an object describing the notification to display. The body can be a translation key. level can be either info or warning.
  • redirectTo: Redirect the user to another page. The property value should be the path to redirect the user to.
  • refresh: Force a rerender of the current view (equivalent to pressing the Refresh button). Set to true to enable.
  • unselectAll: Unselect all lines in the current datagrid. Set to true to enable.
  • callback: Execute an arbitrary function. The value should be the function to execute. React-admin will call the function with an object as parameter ({ requestPayload, payload, error }). The payload contains the decoded response body when it’s successful. When it’s failed, the response body is passed in the error.
  • basePath: This is not a side effect, but it’s used internally to compute redirection paths. Set it when you have a redirection side effect.

Making An Action Undoable

when using the withDataProvider function, you could trigger optimistic rendering and get an undo button for free. the same feature is possible using custom actions. You need to decorate the action with the startUndoable action creator:

// in src/comments/ApproveButton.js
+import { startUndoable as startUndoableAction } from 'ra-core';
-import { commentApprove as commentApproveAction } from './commentActions';
+import { commentApprove } from './commentActions';

class ApproveButton extends Component {
    handleClick = () => {
-       const { commentApprove, record } = this.props;
-       commentApprove(record.id, record);
+       const { startUndoable, record } = this.props;
+       startUndoable(commentApprove(record.id, record));
    }

    render() {
        return <Button onClick={this.handleClick}>Approve</Button>;
    }
}

ApproveButton.propTypes = {
-   commentApprove: PropTypes.func,
+   startUndoable: PropTypes.func,
    record: PropTypes.object,
};

export default connect(null, {
-   commentApprove: commentApproveAction,
+   startUndoable: startUndoableAction,
})(ApproveButton);

And that’s all it takes to make a fetch action optimistic. Note that the startUndoable action creator is passed to Redux connect as mapDispatchToProp, to be decorated with dispatch - but commentApprove is not. Only the first action must be decorated with dispatch.

Altering the Form Values before Submitting

Sometimes, you may want your custom action to alter the form values before actually sending them to the dataProvider. For those cases, you should know that every buttons inside a form Toolbar receive two props:

  • handleSubmitWithRedirect which calls the default form save methods
  • handleSubmit which is the same prop as in react-form

Knowing this, you can dispatch a custom action with a button and still benefit from the default crud action side effects (notifications, optimistic ui, undo, etc.). For instance, in the simple example:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { crudCreate, SaveButton, Toolbar } from 'react-admin';

// A custom action creator which modifies the values before calling the default crudCreate action creator
const saveWithNote = (values, basePath, redirectTo) =>
    crudCreate('posts', { ...values, average_note: 10 }, basePath, redirectTo);

class SaveWithNoteButtonView extends Component {
    handleClick = () => {
        const { basePath, handleSubmit, redirect, saveWithNote } = this.props;

        return handleSubmit(values => {
            saveWithNote(values, basePath, redirect);
        });
    };

    render() {
        const { handleSubmitWithRedirect, saveWithNote, ...props } = this.props;

        return (
            <SaveButton
                handleSubmitWithRedirect={this.handleClick}
                {...props}
            />
        );
    }
}

const SaveWithNoteButton = connect(
    undefined,
    { saveWithNote }
)(SaveWithNoteButtonView);

This button can be used in the PostCreateToolbar component:

const PostCreateToolbar = props => (
    <Toolbar {...props}>
        <SaveButton
            label="post.action.save_and_show"
            redirect="show"
            submitOnEnter={true}
        />
        <SaveWithNoteButton
            label="post.action.save_with_average_note"
            redirect="show"
            submitOnEnter={false}
            variant="flat"
        />
    </Toolbar>
);

Custom Side Effects

Sometimes, you may want to trigger other side effects - like closing a popup window, or sending a message to an analytics server. The easiest way to achieve this is to use the callback side effect:

// in src/comment/commentActions.js
import { UPDATE } from 'react-admin';
export const COMMENT_APPROVE = 'COMMENT_APPROVE';
export const commentApprove = (id, data, basePath) => ({
    type: COMMENT_APPROVE,
    payload: { id, data: { ...data, is_approved: true } },
    meta: {
        resource: 'comments',
        fetch: UPDATE,
        onSuccess: {
            notification: {
                body: 'resources.comments.notification.approved_success',
                level: 'info',
            },
            redirectTo: '/comments',
+           callback: ({ payload, requestPayload }) => { /* your own logic */ }
            basePath,
        },
        onFailure: {
            notification: {
                body: 'resources.comments.notification.approved_failure',
                level: 'warning',
            },
+           callback: ({ payload, requestPayload }) => { /* your own logic */ }
        },
    },
});

Under the hood, withDataProvider uses the callback side effect to provide a Promise interface for dispatching fetch actions. As chaining custom side effects will quickly lead you to callback hell, we recommend that you use the callback side effect sparingly.

Custom Sagas

React-admin promotes a programming style where side effects are decoupled from the rest of the code, which has the benefit of making them testable.

In react-admin, side effects are handled by Sagas. Redux-saga is a side effect library built for Redux, where side effects are defined by generator functions. If this is new to you, take a few minutes to go through the Saga documentation.

Here is the generator function necessary to handle the side effects for a failed COMMENT_APPROVE action which would log the error with an external service such as Sentry:

// in src/comments/commentSaga.js
import { call, takeEvery } from 'redux-saga/effects';

function* commentApproveFailure({ error }) {
    yield call(Raven.captureException, error);
}

export default function* commentSaga() {
    yield takeEvery('COMMENT_APPROVE_FAILURE', commentApproveFailure);
}

Let’s explain all of that, starting with the final commentSaga generator function. A generator function (denoted by the * in the function name) gets paused on statements called by yield - until the yielded statement returns. yield takeEvery([ACTION_NAME], callback) executes the provided callback every time the related action is called. To summarize, this will execute commentApproveFailure when the fetch initiated by commentApprove() fails.

As for commentApproveFailure, it just dispatch a call side effect to the captureException function from the global Raven object.

To use this saga, pass it in the customSagas props of the <Admin> component:

// in src/App.js
import React from 'react';
import { Admin, Resource } from 'react-admin';

import { CommentList } from './comments';
import commentSaga from './comments/commentSaga';

const App = () => (
    <Admin customSagas={[ commentSaga ]} dataProvider={jsonServerProvider('http://jsonplaceholder.typicode.com')}>
        <Resource name="comments" list={CommentList} />
    </Admin>
);

export default App;

With this code, a failed review approval now sends the the correct signal to Sentry.

Tip: The side effects are testable, too.

Using a Custom Reducer

In addition to triggering REST calls, you may want to store the effect of your own actions in the application state. For instance, if you want to display a widget showing the current exchange rate for the bitcoin, you might need the following action:

// in src/bitcoinRateReceived.js
export const BITCOIN_RATE_RECEIVED = 'BITCOIN_RATE_RECEIVED';
export const bitcoinRateReceived = (rate) => ({
    type: BITCOIN_RATE_RECEIVED,
    payload: { rate },
});

This action can be triggered on mount by the following component:

// in src/BitCoinRate.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bitcoinRateReceived as bitcoinRateReceivedAction } from './bitcoinRateReceived';

class BitCoinRate extends Component {
    componentWillMount() {
        fetch('https://blockchain.info/fr/ticker')
            .then(response => response.json())
            .then(rates => rates.USD['15m'])
            .then(bitcoinRateReceived) // dispatch action when the response is received
    }

    render() {
        const { rate } = this.props;
        return <div>Current bitcoin value: {rate}$</div>
    }
}

BitCoinRate.propTypes = {
    bitcoinRateReceived: PropTypes.func,
    rate: PropTypes.number,
};

const mapStateToProps = state => ({ rate: state.bitcoinRate });

export default connect(mapStateToProps, {
    bitcoinRateReceived: bitcoinRateReceivedAction,
})(BitCoinRate);

In order to put the rate passed to bitcoinRateReceived() into the Redux store, you’ll need a reducer:

// in src/rateReducer.js
import { BITCOIN_RATE_RECEIVED } from './bitcoinRateReceived';

export default (previousState = 0, { type, payload }) => {
    if (type === BITCOIN_RATE_RECEIVED) {
        return payload.rate;
    }
    return previousState;
}

Now the question is: How can you put this reducer in the <Admin> app? Simple: use the customReducers props:

// in src/App.js
import React from 'react';
import { Admin } from 'react-admin';

import rate from './rateReducer';

const App = () => (
    <Admin customReducers={{ rate }} dataProvider={jsonServerProvider('http://jsonplaceholder.typicode.com')}>
        ...
    </Admin>
);

export default App;

Tip: You can avoid storing data in the Redux state by storing data in a component state instead. It’s much less complicated to deal with, and more performant, too. Use the global state only when you really need to.

List Bulk Actions

Almost everything we saw before about custom actions is true for custom List bulk action buttons too, with the following few differences:

  • Bulk action button components receive the following props: resource, selectedIds and filterValues
  • They do not receive the current record in the record prop as there are many of them.

You can find a complete example of a custom Bulk Action button in the List documentation, in the Bulk Action Buttons section.

Conclusion

Which style should you choose for your own action buttons? Here is a quick benchmark:

Solution Advantages Drawbacks
fetch
  • Nothing to learn
  • Requires duplication of authentication
  • Does not handle the loading state
  • Adds boilerplate
dataProvider
  • Familiar API
  • Does not handle the loading state
  • Adds boilerplate
withDataProvider
  • Familiar API
  • Handles side effects
  • Adds boilerplate
  • Uses HOC
<Query> and <Mutation>
  • Declarative
  • Dense
  • Handles loading and error states
  • Handles side effects
  • Mix logic and presentation in markup
Custom action
  • Allows logic reuse
  • Handles side effects
  • Idiomatic to Redux
  • Hard to chain calls