Easily Integrate Ramda into Your React Workflow

nshoes

Nate Shoemaker

Posted on September 13, 2019

Easily Integrate Ramda into Your React Workflow

Originally posted on Hint's blog.

Here at Hint, we often use React for writing our user interfaces. We enjoy its declarative API, the mental-model that makes it easier to communicate and collaborate with teams, and especially, the recent addition of hooks. React doesn't provide the entire toolkit, however. It's missing a few things out of the box: data fetching, handling async functions, applying styles in a pragmatic way, etc.

As I was learning React, the biggest hole in React's feature set actually turned out to be an issue with JavaScript itself. Compared to other toolkit heavy languages such as Ruby or Elixir, JavaScript doesn't give you a ton to work with. I started writing my own helper libraries until a friend told me about Ramda. Straight from their homepage:

A practical functional library for JavaScript programmers.

Hey! I like functional things, libraries, JavaScript... and I'm a programmer! It was love at first byte (no, I don't feel any shame for that).

The first Ramda hurdle is functional programming. If you have never dipped a toe in the functional waters, please read Randy Coulman's "Thinking in Ramda" series, it's brilliant.

The second Ramda hurdle (as a React developer) is knowing how to use it with React effectively. I'm still learning and experimenting with how the two libraries can work together, and I wanted to share some of the patterns that I have held onto over the past few years. Let's get into it!

Make Your Code Read Better With isNil And isEmpty

Sometimes, React code isn't the easiest to read. I would argue that post-hooks this has gotten even worse. More and more logic is being added to the component's body, and without lifecycle methods that automatically help organize code out of render, any help I can get to cleanup, I take.

Ramda's isNil and isEmpty are a great start to make your component's body dazzle 🕺. For example:

  const Entry = ({ client }) => (
    <Query query={currentUserQuery}>
      {({ loading, data }) => {
        if (!loading && !data.user.posts)
          return <NoPosts />

        if (data.user) {
          setErrorTrackingContext(data.user)
          getPostMetaData(data.user, client)
        }

        return (
          // code that renders things here
        )
      }}
    </Query>
  )

Note on code examples: all code in this article is based on real life code that I've written. There are some references to Apollo's React library, which Hint loves. Most imports have been removed for brevity. No blogpost-ed, fooBar-filled, faux-code here. Nearly Production Readyâ„¢.

Note the first if: we'll return a component early if we're done loading and the data.user.posts is falsy. The second if: if we have a user, let's set the context for whatever error tracking we're using (at Hint we love Honeybadger), then get some post metadata. Let's not worry about any implementations of these functions and focus on our logic. At first glance, things aren't that bad - but "not that bad" is not the bar. Excellence is! Let's take another pass, but with Ramda:

  import { isNil, isEmpty } from 'ramda'

  const Entry = ({ client }) => (
    <Query query={currentUserQuery}>
      {({ loading, data }) => {
        if (isNil(loading) && isEmpty(data.user.posts))
          return <NoPosts />

        if (data.user) {
          setErrorTrackingContext(data.user)
          getPostMetaData(data.user, client)
        }

        return (
          // code that renders things here
        )
      }}
    </Query>
  )

Note the import at the top and the update to our first if. isNil will return true if loading is null or undefined. This function is extremely helpful because it doesn't just check if the value is falsy, which is essentially what it did before (!loading). Hindquarters saved from a nasty bug!

On the same line, isEmpty will return true if the value passed in is '', [], or {}. When working with GraphQL, if you ask for a collection of things but there are none, more often than not you'll get back an empty array. Our logic check before, !data.user.posts could have also introduced an unintended bug! Hindquarters saved AGAIN.

Pro-Tip

First point and already a pro-tip? Today is a good day.

Ramda is built of many tiny functions that have a single specific purpose. Assembled together properly, you can create some fun stuff! Let's create a helper that's the inverse of isNil:

  import { isNil, isEmpty, complement } from 'ramda'

  const isPresent = complement(isNil)

  const Entry = ({ client }) => (
    <Query query={currentUserQuery}>
      {({ loading, data }) => {
        if (isNil(loading) && isEmpty(data.user.posts))
          return <NoPosts />

        if (isPresent(data.user)) {
          setErrorTrackingContext(data.user)
          getPostMetaData(data.user, client)
        }

        return (
          // code that renders things here
        )
      }}
    </Query>
  )

complement takes a function as its first argument, and a value as its second. If a falsy value is returned when it's called, the output will be true (the inverse is also true). Using complement makes our second if a little nicer.

You may say, "Well that's really simple. Why doesn't Ramda come with a helper like that?" Think of Ramda functions like individual LEGOS pieces. On their own, they don't do a ton, but put them together, and you can create something incredibly useful. If you want a more "comprehensive set of utilities", check out Ramda Adjunct.

It's Dangerous to Operate on Objects Alone! Take These Functions: prop and path

+1 internet points if you get the title joke

As a developer, nothing is more scary than deeply accessing an object. If this doesn't make you slightly cringe:

if (foo.bar.baz.theLastPropertyIPromise.justKiddingOneMore) doTheThing()

Then we need to have a talk. If this is your proposed solution:

if (
  foo &&
  foo.bar &&
  foo.bar.baz &&
  foo.bar.baz.theLastPropertyIPromise &&
  foo.bar.baz.theLastPropertyIPromise.justKiddingOneMore
)
  doTheThing()

Then we really need to talk.

Joking aside, we've all been there. It's easy to gloss over complex checks completely or write conditionals that take up too many bytes and are difficult to read. Ramda gives us prop and path to safely access objects. Let's see how they work:

import { prop, path, pipe } from 'ramda'

const obj = { foo: 'bar', baz: { a: 1, b: 2 } }

const getFoo = prop('foo')
getFoo(obj) // => 'bar'

const getBazA = path(['baz', 'a'])
getBazA(obj) // => 1

Great! "But what about that is safe? All the properties you asked for are present!" Glad you asked:

import { path, pipe } from 'ramda'

const obj = { foo: 'bar', baz: { a: 1, b: 2 } }

const getSomethingThatDoesNotExist = path([
  'foo',
  'bar',
  'baz',
  'theLastPropertyIPromise',
  'justKiddingOneMore'
])
getSomethingThatDoesNotExist(obj) // => undefined

Thanks Ramda! Hindquarters, yet again, saved. Note that undefined, a falsy value is returned. Very useful for presence checks! Let's apply our new learnings to our <Entry /> component:

  import { isNil, isEmpty, complement, prop } from 'ramda'

  const getUser = prop('user')
  const userIsPresent = pipe(
    getUser,
    complement(isNil)
  )

  const Entry = ({ client }) => (
    <Query query={currentUserQuery}>
      {({ loading, data }) => {
        if (isNil(loading) && isEmpty(data.user.posts))
          return <NoPosts />

        if (userIsPresent(data)) {
          const user = getUser(data)
          setErrorTrackingContext(user)
          getPostMetaData(user, client)
        }

        return (
          // code that renders things here
        )
      }}
    </Query>
  )

Looking better for sure. Further refactoring could be done in our second if condition. For fun, see if you can figure out how to use Ramda to bring that if into one function. Answer is at the end of this post!

Prep Your Props With evolve

Transforming component props into something useful is common practice. Let's take a look at this example where we concat a first and last name as well as format a date:

const NameAndDateDisplay = ({ date, firstName, lastName }) => (
  <>
    <div>
      Hello {firstName.toUpperCase()} {lastName.toUpperCase()}!
    </div>
    <div>It is {dayjs(date).format('M/D/YYYY dddd')}</div>
  </>
)

Straightforward, but there is something fishy about this code, though. Can you spot it? The problem is that it's a little too straightforward. When working with real data, real API's, and real code that humans have written, things aren't always straightforward. Sometimes you are working on a project that consumes a third-party API and you don't have full control on what you get back from the server.

In these cases, we tend to throw all of our logic in our component bodies, like so:

const NameAndDateDisplay = ({ date, firstName, lastName }) => {
  const formattedDate = formatDate(date)
  const formattedFirstName = formatFirstName(firstName)
  const formattedLastName = formatLastName(lastName)

  return (
    <>
      <div>
        Hello {firstName} {lastName}!
      </div>
      <div>It is {formattedDate}</div>
    </>
  )
}

This presents a few issues. Some very important logic is tied to the body of our component, making testing difficult. The only way to test those formatters is to render the component. Also, it's really bloating the body of our component. In Rails you'll here "Fat models, skinny controllers"; an analogous term in React would be "Fat helpers, skinny component body".

Luckily, Ramda's evolve can really help us out. evolve takes two arguments; the first is an object whose values are functions, and the second argument is the object you want to operate on.

import { evolve, toUpper } from 'ramda'

evolve({ foo: toUpper }, { foo: 'weeee' })
// => { foo: 'WEEEE' }

Pretty neat! Two important things to note about evolve: it's recursive and it doesn't operate on values you don't specify in the first argument.

import { evolve, toUpper, add } from 'ramda'

const format = evolve({
  foo: toUpper,
  numbers: { a: add(2) },
  dontTouchMe: 'foobar'
})
format({ foo: 'weeee', numbers: { a: 3 } })
// => { foo: 'WEEEE', numbers: { a: 5 }, dontTouchMe: 'foobar' }

With this newfound knowledge, let's refactor our component:

import { evolve, pipe } from 'ramda'

const prepProps = evolve({
  date: formatDate,
  firstName: formatFirstName,
  lastName: formatLastName
})

const NameAndDateDisplay = ({ date, firstName, lastName }) => (
  <>
    <div>
      Hello {firstName} {lastName}!
    </div>
    <div>It is {date}</div>
  </>
)

export default pipe(
  prepProps,
  NameAndDateDisplay
)

Sick! We have successfully split our formatting code away from our rendering code.

Wrapping Up

React and Ramda are both incredibly powerful tools. Learning how they work and interact together can simplify and speed up development time.

Going forward, keep Ramda in mind when you find yourself copying & pasting helper libraries from one project to the next. Odds are, a Ramda function exists that can accomplish the same task, and more! There are many, many more Ramda functions not covered in this article. Look to Ramda's documentation to learn more.

Refactoring Answer

Our second if condition, fully refactored:

// setErrorTrackingContextAndGetPostMetaData.js
import { prop, pipe, complement, when, converge, curry, __ } from 'ramda'

const getUser = prop('user')
const userIsPresent = pipe(
  getUser,
  complement(isNil)
)
const curriedGetPostMetaData = curry(getPostMetaData)

const setErrorTrackingContextAndGetPostMetaData = client =>
  when(
    userIsPresent,
    converge(getUser, [
      setErrorTrackingContext,
      curriedGetPostMetaData(__, client)
    ])
  )

export default setErrorTrackingContextAndGetPostMetaData

// Entry.js
// in the body of <Entry />

// ...
setErrorTrackingContextAndGetPostMetaData(client)(data)
// ...
💖 💪 🙅 🚩
nshoes
Nate Shoemaker

Posted on September 13, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related