
Using Saga To Accumulate And Deduplicate Redux Actions

While developing the <ReferenceField>
component for admin-on-rest, I recently stumbled upon a fun use case that demonstrates the power of redux-saga. I needed to accumulate and deduplicate actions for performance reasons. Saga proved to be perfect for that job! Read on to see if Saga can help you, too.
The <ReferenceField>
Component
We recently released Admin-on-rest, a frontend framework for building admin GUIs on top of REST APIS, based on React.js. One of the components of admin-on-rest is called <ReferenceField>
, and it’s perfect to fetch references based on a foreign key.
Let me give you an example. Let’s say you have a REST endpoint that returns a list of posts. Each post references an author, through an author_id
attribute:
GET /posts
HTTP 1.1 OK
[
{
id: 93,
title: 'seatae soluta recusante',
author_id: 789,
},
{
id: 124,
title: 'commodi ulam sint et',
author_id: 456
},
{
id: 125,
title: 'consequatur id enim sint',
author_id: 735
},
...
]
If you want to display this list with admin-on-rest, all you have to do is write the following code:
// in src/posts.js
import React from "react";
import {
List,
Datagrid,
TextField,
ReferenceField,
} from "admin-on-rest/lib/mui";
export const PostList = (props) => (
<List {...props}>
<Datagrid>
<TextField source="id" />
<ReferenceField label="User" source="author_id" reference="users">
<TextField source="name" />
</ReferenceField>
<TextField source="title" />
<TextField source="body" />
</Datagrid>
</List>
);

The <ReferenceField>
component receives the current record
for each line in the datagrid. It then fetches the /users/:id
API endpoint for the author_id
of each post. It’s very simple:
export class ReferenceField extends Component {
componentDidMount() {
const { reference, record, source } = this.props;
dispatch({
type: "CRUD_GET_ONE_REFERENCE",
payload: { resource: reference, id: record[source] },
});
}
componentWillReceiveProps(nextProps) {
if (this.props.record.id !== nextProps.record.id) {
const { reference, record, source } = nextProps;
dispatch({
type: "CRUD_GET_ONE_REFERENCE",
payload: { resource: reference, id: record[source] },
});
}
}
render() {
const { reference, referenceRecord, children } = this.props;
if (!referenceRecord && !allowEmpty) {
return <LinearProgress />;
}
return React.cloneElement(children, {
record: referenceRecord,
resource: reference,
});
}
}
The referenceRecord
is mapped by Redux from the state. It is empty until the reference is fetched.
Saving API Calls
The naive implementation of the CRUD_GET_ONE_REFERENCE
action handler works like a DDoS attack: for each line in the datagrid, the component fetches the /users
endpoint once for the related user.
GET /users/789
GET /users/456
GET /users/735
...
This is not effective at all, especially since most REST APIs accept a filter
parameter, allowing to group these n calls into a single one:
GET /users?filter={ids:[789,456,735,...]}
Besides, if two posts are authored by the same user, the naive approach fetches the same user twice.
When a React component dispatches actions, how can another service catch the actions, accumulate and group them, then redispatch a single action?
Enter Saga
Redux-saga is a side effect library for Redux. “Side effect” here means everything Redux reducers cannot do by themselves: redispatching another action, relying on an external datasource (like an API or setTimeout
), etc. Saga uses ES5 generators to offer unique features and the <ReferenceField>
use case is a great example.
From a Redux app’s point of view, Saga is like a middleware. Saga will see every dispatched action. It can choose to catch one, and redispatch another one after a while. That’s exactly what we’ll do:
import { delay, takeEvery } from "redux-saga";
import { call, cancel, fork, put, take } from "redux-saga/effects";
/**
* Example
*
* let id = {
* posts: { 4: true, 7: true, 345: true },
* authors: { 23: true, 47: true, 78: true },
* }
*/
const ids = {};
const tasks = {};
// see http://yelouafi.github.io/redux-saga/docs/recipes/index.html#debouncing
function* fetchReference(resource) {
// combined with cancel(), this debounces the calls
yield call(delay, 50);
yield put({
type: "CRUD_GET_MANY",
payload: { resource, ids: Object.keys(ids[resource]) },
});
delete ids[resource];
delete tasks[resource];
}
function* accumulate({ payload }) {
const { id, resource } = payload;
if (!ids[resource]) {
ids[resource] = {};
}
ids[resource][id] = true; // fast UNIQUE
if (tasks[resource]) {
yield cancel(tasks[resource]);
}
tasks[resource] = yield fork(fetchReference, resource);
}
export default function* () {
yield takeEvery("CRUD_GET_ONE_REFERENCE", accumulate);
}
There is a lot to explain here, and it reads from the bottom up.
The default function registers a new watcher called accumulate()
, which will be called each time a CRUD_GET_ONE_REFERENCE
is dispatched (that’s the action dispatched by the <ReferenceField>
component).
This accumulate()
function adds the id
from the payload to an ids
object that’s in the middleware closure, with a trick to deduplicate ids. Then, accumulate()
forks another handler called fetchReference()
, and keeps a reference in tasks
.
This third function, fetchReference()
, starts by a delay()
of 50ms (equivalent of a setTimeout()
). If another CRUD_GET_ONE_REFERENCE
action is caught before the timeout ends, then the initial fetchReference()
fork is cancelled. The net effect is a debounce: fetchReference()
will only pass the call to delay
the last time a CRUD_GET_ONE_REFERENCE
is dispatched.
call
, cancel
, fork
, put
, and take
are high-level side effect helpers provided by saga. Check out the saga documentation to learn more about these side effects.
Thanks to the saga we wrote, n calls to CRUD_GET_ONE_REFERENCE
will result in a single CRUD_GET_MANY
action dispatch:
{
type: 'CRUD_GET_MANY',
payload: {
resource: 'users',
ids: [789,456,735,...],
}
}
This action is then caught by another middleware (powered by Saga too, but you could use the thunk middleware or a promise middleware) to issue an HTTP call to:
GET /users?filter={ids:[789,456,735,...]}
The frontend now displays the list in a snap!
Conclusion
Redux Saga allowed us to keep the component logic simple (each <ReferenceField>
component dispatches one single action), and to add sophisticated side effects in the controller part of the application. Even if the initial learning curve is not easy, after a day or two, you’ll understand the huge potential that Saga offers. We definitely recommend it for complex React apps with performance requirements.
Authors

Marmelab founder and CEO, passionate about web technologies, agile, sustainability, leadership, and open-source. Lead developer of react-admin, founder of GreenFrame.io, and regular speaker at tech conferences.