Currying Reducers
Yesterday I was reading curried (partial) functions in Scala and I remembered the first time I used them in an actual production application.
This was with a ReactJS SPA which used Redux for managing state.
Note: This blogpost assumes that the reader has basic understanding of Redux - namely defining and combining reducers.
The application I worked on was fetching data from backend APIs with paginated results and uniform response structure.
For example:
GET /products
{
"items": [
{"name": "Foo", "age": 13},
{"name": "Bar", "age": 16},
...
],
"total_count": 108,
"total_pages": 11
}
For which I wrote a reducer:
// productsReducer.js
// status of data fetch - to show loaders on page
const status = (state = null, action) => {
// based on action.type return either of "in-progress", "success", "failure"
}
const items = (state = [], action) => {
switch (action.type) {
case "FETCH_PRODUCTS_SUCCESS":
return action.items;
case "FETCH_PRODUCTS_FAILURE":
return []; // clear existing data in store
default:
return state;
}
}
const totalCount = (state = 0, action) => {
// same as `items` - return action.total_count if success
}
const totalPages = (state = 0, action) => {
// same as `items` - return action.total_pages if success
}
export default combineReducers({
status,
items,
totalCount,
totalPages
});
But this was just for one API. There were a few more:
GET /orders
{
"items": [
...
],
"total_count": ..
"total_pages": ..
}
GET /warehouse_locations
// and so on...
As I wrote the second reducer for orders
, I realized there was clearly a pattern here - a pattern that mirrored the backend API structure. But how do we harness this to create a good abstraction?
If we could pass another argument to the reducer functions - say type
which is "products"
, "orders"
, etc. -
that would help us define different action-types for each API.
But the problem is those reducers are invoked by redux
library.
Javascript being dynamically typed will happily allow us to add a third parameter,
but its value would end up being undefined
always because redux will not pass any value there.
So we cannot just add a third argument to every function. The function we provide to redux
has to match the function signature:
(state = <initValue>, action) => {}
I was studying functional programming back then with Dan Grossman's fantastic course on Coursera which I highly recommend to any programmer - irrespective of the language you work with.
Functional programming invariably means functions as first-class citizens
i.e. functions are
just like any other values (integers, strings, etc.). Functions can be passed around as arguments and
returned as return-values from other functions. And redux being designed in a functional way helps us here.
✨ Enter currying
✨
Currying is a technique which splits one function that takes multiple arguments into multiple functions each taking one or more arguments.
In our case, we can create a higher-order function that takes the listType
argument, then builds and returns another function which matches the signature that redux expects.
// listReducer.js
const listReducer = listType => {
const type = listType.toUpperCase();
const items = (state = [], action) => {
switch (action.type) {
case `FETCH_${type}_SUCCESS`:
return action.items;
case `FETCH_${type}_FAILURE`:
return [];
default:
return state;
}
}
// stripping other functions for brevity
return combineReducers({
status,
items,
totalCount,
totalPages
})
}
See full code in this gist.
Here's how we now use our listReducer
:
// index.js - top-level reducer
const rootReducer = combineReducers({
products: listReducer("products"),
orders: listReducer("orders"),
warehouse_locations: listReducer("warehouse_locations"),
...
})
These type of reducers are also sometimes called higher-order reducers
-
because they are higher-order functions.
If you found this interesting, also check out higher order components.