Functional Redux Reducers with Ramda

Conroy Whitney

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 flipped, 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.

  Tweet It
✖ Clear

🕵 Search Results

🔎 Searching...