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)
// ...
Top comments (10)
I use lodash at work for similar problems. Lots of isNil and isEmpty. Ramda seems like it can solve a bit more, if you have used lodash how would you compare them?
Also “fat helpers and skinny component body” is great advice.
For the record, there's also a functional version of Lodash known as lodash/fp.
Oh nice! I'll take a look.
I have not used Lodash, sorry!
Also check out Rambda the tree shakable, performance optimized clone of Ramda.
Sweet, never heard of that. I like the
path
works with dot notation. Thanks!I really loved ramda. It was almost an addiction.
But currying, data last, pointfree style creates so many problems if the codebase needs to be converted to Typescript later.
I think in evolve's illustration, you need to render 'date' and not 'formattedDate'.
Nice catch! Updated on our website and here. Thanks!