IDX + GraphQL = ❤️
IDX is a library for accessing arbitrarily nested, possibly nullable properties on a JavaScript object. At least this is how it is described in the documentation. What does this mean in reality and why is it such a big problem in JavaScript? And why is it so important when working with GraphQL?
It all started a while ago. We decided to disallow non-null fields in our GraphQL output types. This means that all of our output fields are nullable. There is only one exception — opaque IDs cannot be null. I still consider this one of the better decisions but there is one huge drawback: everything can be null now. Surprise… :))
Why nullable fields? The main argument is backward compatibility (BC). You can change a nullable field to non-nullable if you want but not vice versa because it would be a BC break. But a bigger advantage is the GraphQL behaviour. Consider the following query:
{
allLocations(term: "PRG") {
edges {
node {
locationId
name # <- this field will fail
}
cursor
}
}
}
It can fail nicely with the name
field being nullable. It'll basically return null instead of the actual value. But we can still reach locationId
without any problems. The situation would be very different if we declared thename
field as non-nullable. The whole node
would be null because we cannot return null instead of a name and we would just throw away the other fields like locationId
even when they’re absolutely fine.
Working with nullable fields
From the very beginning I was a GraphQL API creator but not really a user. So when I had the opportunity of digging deeper into GraphQL usage I started to feel the disadvantages of this approach. Basically, the biggest issue is the fact that you have to check every field to see if it exists or not. And that's painful — especially for really large datasets. But I knew it was the right thing to do and it was just about finding the right tools. Imagine the following object:
const props = {
user: {
name: 'string',
friends: [
{
user: {
name: 'string', // <- we want to access this field
},
},
],
},
};
It's basically an object representing the user with friends containing the same object (in a recursive way). Every field can be nullable (or just missing) so how do we get the name of the first friend? The first approach would be something like this:
if (props.user != null) {
if (props.user.friends != null) {
if (props.user.friends[0] != null) {
if (props.user.friends[0].user != null) {
console.log(props.user.friends[0].user.name);
}
}
}
}
You cannot access the field directly (at least not in the current JS version) because the path may be null or undefined somewhere on the way. There are multiple ways of writing it but there's always going to be a lot of ugly code. The second approach would be to use something like Lodash because every programmer is too lazy to write these conditions:
console.log(
_.get(props, 'user.friends[0].user.name')
);
This code is much better looking. It's perfectly valid and easy to read. There is one problem however. You need to use that magic string as a second function argument which makes it super fragile. IDE refactoring tools and suggestions will not work and most importantly — Flow types will not work as well. Basically, everything after this function call may be potentially broken and you wouldn't know about it. I asked my colleague what was his experience with removing Lodash from one project he’s been working on:
The greatest benefit was, as expected, of uncovering wrong Flow types. I assume that these incorrect Flow types occurred when we were refactoring components into smaller fragments…
It's really hard to stick with the contract you wrote in the magic string somewhere in the code especially if this piece of code is supposed to work all the time or fail silently. To solve this problem you'll need to write something a little bit more sophisticated:
const nullPattern = /^null | null$|^[^(]* null /;
const undefinedPattern = /^undefined | undefined$|^[^(]* undefined /;
const read = (input, accessor) => {
try {
return accessor(input);
} catch (error) {
if (error instanceof TypeError) {
if (nullPattern.test(error)) {
return null;
} else if (undefinedPattern.test(error)) {
return undefined;
}
}
throw error;
}
};
console.log(
read(props, _ => _.user.friends[0].user.name)
);
It's a little bit more complicated but the idea is simple: first try to access a user's name on props
and if it doesn't work just return null
or undefined
depending on which error occurred. So congratulations — now you understand how IDX works because I just basically copy-pasted the code from the IDX repository. Just replace the function name read
with idx
and you are good to go. Sort of…
IDX in production
IDX goes one step further. The implementation I revealed above is just for development but there’s also a Babel plugin which transforms the IDX calls into better performing, less tricky code:
var _ref;
(_ref = props) != null
? (_ref = _ref.user) != null
? (_ref = _ref.friends) != null
? (_ref = _ref[0]) != null
? (_ref = _ref.user) != null ? _ref.name : _ref
: _ref
: _ref
: _ref
: _ref;
Yes, it's basically the first code snippet I wrote in this article, but generated automatically. So we are effectively returning back to the best implementation but with a nice interface (one IDX function call).
I also tried to compare the performance of Lodash and IDX but it doesn't make much sense. The performance is very similar. I had to call the functions more than a million times to see really significant differences. At this point Lodash loses it completely because it gets very exponential but for me this is not a big argument. The biggest thumbs up for IDX is the Flow (or Typescript) compatibility.
Be careful
IDX can sometimes be very tricky. Especially with default values. It's easy with Lodash because you can use the last argument for a default value. But you need to do something like this with IDX:
const name = idx(props, _ => _.user.friends[0].user.name) || ''
In other words: just return an empty string if it's not possible to get a friend's name. You need to be really careful with how you use this result. It's very common to write similar code in React:
{name && <Component />}
This works with name
being null or undefined but not with an empty string because in JS:
('' && 'whatever') === ''
This is probably fine in normal React.js but it's illegal in React Native. So, be careful and write proper conditions or default values… :)
It's also not that easy to deal with the question “what should I do if I don't get the data?” And there’s no simple answer. Sometimes it's OK to just leave it empty but sometimes it's necessary to throw up an error or display an error message explaining the failure. It really depends on the situation.
Don't use it everywhere, please
People tend to use IDX everywhere. But in the GraphQL context it sometimes doesn't make sense. I often see code like this:
const id = idx(props, _ => _.id)
What is the point, really? You can just write:
props && props.id
And even this may be unnecessary if you are sure you'll get props
every time. I know, sometimes it's used as a safety mechanism to prevent the issue with empty strings (just in case) but you'll never get an empty string when accessing nested objects in GraphQL. The same extreme is for super deeply nested objects. If you need to call:
idx(props, _ => _.user.friends[0].user.cars[0].engine.valves)
Maybe it's time to decompose your component into multiple components? You'll usually fetch only what you need for your component and therefore you don't need to access 5 levels of the object.
It's not only for GraphQL
Actually, you will not find a single sentence about GraphQL in the IDX repository. As I wrote at the very beginning:
IDX is a library for accessing arbitrarily nested, possibly nullable properties on a JavaScript object.
And that's it. It's just a really great fit for GraphQL client libraries and I highly recommend it. Thanks Lee Byron for showing me this library during the GraphQL Summit 2017 in San Francisco. I hope this little trick will save you a few more hours while building client applications using (not only) GraphQL!