In a recent poll of react
developers about their favourite libraries that change the way they code, ramda
(a functional JavaScript library) came out on top. In this tutorial, we walk through a redux
reducer, introducing ten ramda
functions which you can start using in your own projects.
What Is Ramda?
ramda
is a JavaScript library which allows you to create pure, functional, immutable, side-effect-free functions which are automatically curried and thus composable. There are a ton of methods on ramda
, and their repl is very helpful when developing.
If you are not familiar with the concept of functional programming, don’t worry! It can sound a lot scarier than it really is, largely due to the fact that the term is usually paired with esoteric words like functor and monoids. I recommend watching this 30-minute video Learning Functional Programming with JavaScript by Anjana Vakil. This will get you up to speed on the concepts in no time. You can continue reading even if this is your first time doing functional programming, as we’ll go through everything piece by piece.
Example Redux Reducer
I personally believe in jumping into the deep end when learning something, so in this example, we are going to look at a redux
reducer which uses a lot of ramda
functions. In this example, our react
app has just successfully fetched a list of gators
from our GatorAPI
. Now we need to take those results and add them to our redux
state tree.
In plain words, this reducer:
- 1. Updates our state tree to include a unique list of
gatorIds
in the all
array - 2. Adds each of the new
gator
object to a lookup hash byId
, indexed by its id
- 3. Turns off the
loading
flag so that our UI can remove the loading spinner
Gator Reducer: gators/reducer.js
const reverseMerge = flip(merge); export function fetchSuccess(state, { payload: { gators } }) { const gatorIds = map(prop("id"), gators); const gatorLookup = indexBy(prop("id"), gators); return evolve( { all: compose(uniq, concat(gatorIds)), byId: reverseMerge(gatorLookup), loading: always(false) }, state ); }
Ramda Functions
Let’s walk through each of these functions in alphabetical order. If you have any questions, I suggest you view the ramda
docs for more in depth examples, and play around in the repl to experiment with how each of these work.
-
always: In ramda
, everything is assumed to be a function. But what if you want to use a primitive like an integer, string, or boolean? That’s where always
comes in – it is a function that takes a value, and returns a function, that returns that value. It could be written something like val => () => val
. While this may seem silly, in practice it’s necessary to use when you want to use a scalar with other ramda
functions such as evolve
which expect a function as a parameter.
-
compose: Since everything in ramda
is a curried function, you can apply parameters piecemeal. That’s super helpful to be able to build a pipeline of data transformation, where the output of one function becomes the input of the next function. Indeed, ramda
has two functions for that, called pipe
(which reads left-to-right), and compose
(which reads right-to-left). In this case, we use compose
because it reads as if you had nested parenthesis: uniq(concat(gatorIds, all))
.
-
concat: Concat is short for “concatenate” and combines two lists. The contents of the second list are added to the end of the first list, and a new list is returned.
-
evolve: This takes two parameters: a set of transformations, and an object to be transformed. The transformations is an object whose keys are what you want to change, and the values are the function you want to apply to that key. In our case, the transformations are everything inside the {}
s, and the object to be transformed is our redux
state. The hidden magic of evolve
is that the current value of whatever key we are transforming (all
, byId
, or loading
) will be passed in as the last parameter to our transformation functions (compose
, reverseMerge
, and always
).
-
flip: This reverses the order of parameters for a function. Most operations in ramda
are backwards from what you might think since more often than not you are setting up a curried function which you want to apply later. Like for instance, ramda
’s map
function takes the function as the first parameter, and the list as the last parameter, which is backwards from JavaScript’s list-first / function-last order. In some cases, you want to be able to re-order the parameters to make them play nicely with other ramda
functions. In all honesty, though, merge
is pretty much the only one that comes to mind, so I often just define reverseMerge
in a constants/ramda.js
file.
-
indexBy: Sometimes you have a list of objects such as [{ id: 1, title: "one" }, { id: 2, title: "two" }]
that you want to turn into an object for easier lookup by its id
, for example: { 1: { id: 1, title: "one" }, 2: { id: 2, title: "two" } }
. indexBy
lets you do just that. You simply give it which attribute you want to use as the key, and the list you want to transform, and it gives back the new object.
-
map: Just like regular JavaScript map
, except that – like most ramda
functions – its parameters are backwards from how we would normally see them. In this case, map
takes a function you want to apply, and the list you want to apply it over. For example: map(elem => console.log(elem), elements)
will loop through all elements
and log them to the console.
-
merge: Takes in an original object as the first parameter, and a set of new values as the second parameter. It then returns a new object which contain a combination of keys from both the original and new objects, where the values present in the new have replaced those in the original. merge({ a: "alligator", c: "crocodile" }, { b: "bayou", c: "cayman" })
yields { a: "alligator", b: "bayou", c: "cayman" }
. In order to use merge
with other ramda
functions such as evolve
, where you want the new object to be the first parameter, and the original object to be the second parameter, it is necessary to use flip
as described above.
-
prop: Returns the value of that object attribute. For example prop("a", { a: "alligator", b: "bayou", c: "cayman" })
returns "alligator"
. It is very useful when used in a pipeline like compose
so you can pull out values as you transform the data. There are also many derivative functions such as path
, propOr
, and propEq
which extend this functionality into more specific use cases.
-
uniq: Takes a list and returns a new list where all duplicate values have been removed. No surprises here.
Once More, With Feeling
Now that we have a good idea of what each of these ramda
functions do, let’s go through this redux
reducer code one more time and see it all come together.
Gator Reducer: gators/reducer.js
const reverseMerge = flip(merge); export function fetchSuccess(state, { payload: { gators } }) { const gatorIds = map(prop("id"), gators); const gatorLookup = indexBy(prop("id"), gators); return evolve( { all: compose(uniq, concat(gatorIds)), byId: reverseMerge(gatorLookup), loading: always(false) }, state ); }
- 1. We define a new function,
reverseMerge
, which is just regular merge
with the parameters flip
ped, so that we can use it in the evolve
function. - 2. We use
map
to loop through the gators
list, applying prop("id")
to each element, so we end up with a list of ids
. - 3. We use
indexBy
to turn the list of gators
into an object where each gator
can be accessed by its id
property. - 4. We return the results of the
evolve
function, where our transformation is the keys of the parts of the state tree that we want to update, with the functions that we want to apply to each key. We pass in state
, since that is the object that we want to transform. - 5. The existing
all
list will be passed in as the hidden parameter to the compose
function, which will pass it from right-to-left as the hidden parameter to concat
, which will append the existing all
list to the end of the new gatorIds
. compose
then passes the resulting list to uniq
, which will ensure there are no duplicates. - 6. The existing
byId
object will be passed in as the hidden second parameter to reverseMerge
, which will merge in our new gatorLookup
objects. merge
will automatically ensure there are no duplicates, by overwriting the older value with the newer values. - 7. The existing
loading
value will be passed in to always
as the hidden second parameter, but it does not matter, because always
ignores that, and simply returns the value we give it, in this case false
.
Conclusion
Wrapping your head around functional JavaScript in general, and ramda
in particular, can take some getting used to. We jumped right into the deep end on this one, introducing ten new ramda
functions used to perform a rather complex update to the state tree in a redux
reducer. While that does not even scratch the surface of the hundreds of functions in the ramda
library, it still represents a significant change in thinking from the normal way of creating reducers.
👉 In a future article, we will take a look at additional ramda functions which can help simplify your redux selectors.