Relay/GraphQL: De-mystifying Node Interface/ID

Summary

Node ID is a ‘mysterious’ little thing in Relay/GraphQL; the Facebook tutorial presented it upfront with little explanation and fanfare, and it ‘just works’. We explain a scenario where it won’t just work, and in the process shed light on when, and how it is actually used by Relay/GraphQL. Armed with that knowledge we hope you can identify other scenarios when it needs more care.

Overview

The Relay/GraphQL tutorial’s chapter on Object Identification elucidates how Relay/GraphQL requires Node IDs for re-fetching objects. If you have implemented Relay in your web application, you would have implemented the interface nodeDefinitions (in Javascript), or its equivalent NodeIdentification (in Ruby), etc. (from hereforth we will use nodeDefinitions as a language-agnostic term). If you have never read the former, nor recall the latter after building your Relay/GraphQL web application, it would not be surprising at all; the standard nodeDefinitions implementation that comes in the tutorial for each Relay/GraphQL library/starter-kit (Javascript, Rails, etc.) should just work, or only require minor tweaks.

To demystify (global) Node ID, one needs to understand:

  • How & when does Node ID gets created?
  • When Node ID is used?
  • How are the nodeDefinitions methods used?

For experienced readers who are just interested in what Node ID is all about without going through any example, feel free to skip to the section Answers & Summary below, and refer back here for detailed explanations, or just refer to the codebase.

Assumptions

Although we provide snippets of explanation throughout, you are expected to be somewhat familiar with the high-level concepts of:

  • GraphQL schema
  • Relay connection/edge
  • React
  • Relay/GraphQL data declaration/co-location

Glossary

To facilitate understanding, we define:

  • GraphQL type: This is like the object-oriented concept of Class. It defines the name of the type, and the fields that the type has. E.g., a User type with fields ‘name’, and ‘age’.
  • GraphQL object: This is like the object-oriented concept of object. It is a specific instance of a Class with specific values for each field. E.g., a User instance can have fields with name ‘John’, and age ‘25’, while another instance can have fields with name ‘Sally’, and age ‘21’. However, both are of the same User type.

Node ID By Example

We will take you through an example to show you why Node ID ‘just works’, and show a counter example when it won’t ‘just work’, followed by a summary of what Node ID is all about.

Problem Statement

We motivate our example with a simple problem. We are a denim (also known as jeans) online store. We have a database of each denim’s brand, model, and minimum size. We want to display a list of jeans with those 3 fields to a user visiting the online store.

High-level Deconstruction of the Problem

We follow the flow of Facebook’s tutorial to deconstruct the problem into Relay/GraphQL concepts. Briefly:

  • Build a ‘database’ using Javascript hash to represent the store’s denims. In reality, this can be implemented using ORM, redis, etc.
  • Define a dysfunctional nodeDefinitions, which still somehow ‘just works
  • Define GraphQL schema for GraphQLTypes Denim, DenimList
  • Create a React component that holds logic to show a list of denims
  • Encapsulate the React component with a Relay container to hold the data declaration for the attributes of Denim, DenimList in GraphQL that the component intends to display (component/data co-location)

Nuts & Bolts

The details of the implementation follows. The code snippets are littered with comments to facilitate understanding.

Javascript-based Denim Database

// js/database.js
// Class for each piece of denim
class Denim {}
// Class for list of denims that contains all pieces of denims
class DenimList {}
// Our small denim database with 3 fields:
// Brand, Model, Minimum Size
var denimDb = [
[‘Acne Studios’, ‘Ace’, 28],
[‘Nudie’, ‘Tube Tom’, 27],
[‘Levi\’s’, ‘501’, 28],
];
// Create a Denim instance for each piece of denim
var denims = denimDb.map((denimArr, i) => {
var denim = new Denim();
denim.brand = denimArr[0];
denim.model = denimArr[1];
denim.size = denimArr[2];
denim.id = `${i}`;
return denim;
});
// Create our Denim list with arbitrary id
var denimList = new DenimList();
denimList.id = 1;
// Make this database methods available to enable
// external code to interact with your database
module.exports = {
getDenim: (id) => denims[id],
getDenims: () => denims,
getDenimList: () => denimList,
Denim,
DenimList,
}

GraphQL ‘nodeDefinitions’ Method

Assuming that we have no idea of what nodeDefinitions method should be, we create a dysfuntional nodeDefinitions that return null for all cases.

// data/schema.js
var {nodeInterface, nodeField} = nodeDefinitions(
(globalId) => {
var {type, id} = fromGlobalId(globalId);
    // Log to NodeJS console the mapping from globalId/Node ID  
// to actual object type and id
console.log("NodeDefinitions (globalId), id:", id);
console.log("NodeDefinitions (globalId), type:", type);

return null
},
(obj) => {
return null;
},
);

GraphQL Schema Definition

Import all the Denim database methods that we will use in our GraphQL schema.

// js/schema.js
import {
// Import Denim database methods that your schema
// can interact with
Denim,
DenimList,
getDenims,
getDenimList,
}

In the GraphQL schema, we need to define:

  • Denim (denimType) has 3 attributes, and their respective types
  • DenimList (denimListType) has ‘has many’ Denims (denimType) relationship using Relay connections
  • Denim Relay connection (denimConnection)
// js/schema.js
var denimType = new GraphQLObjectType({
name: 'Denim',
description: 'Details of a pair of denim',
fields: () => ({
id: globalIdField('Denim'),
brand: {
type: GraphQLString,
description: 'Brand of denim',
},
model: {
type: GraphQLString,
description: 'Model of the brand',
},
size: {
type: GraphQLInt,
description: 'Size of denim',
},
}),
interfaces: [nodeInterface],
});
var denimListType = new GraphQLObjectType({
name: 'DenimList',
description: 'List of denims',
fields: () => ({
id: globalIdField('DenimList'),
denims: {
type: denimConnection,
description: 'List of denims',
args: connectionArgs,
resolve: (_, args) => connectionFromArray(getDenims(), args),
},
}),
interfaces: [nodeInterface],
});
var {connectionType: denimConnection} =
connectionDefinitions({name: ‘Denim’, nodeType: denimType});

Expose the ability to query for denimList as:

// js/schema.js
var queryType = new GraphQLObjectType({
name: 'Query',
fields: () => ({
node: nodeField,
denimList: {
type: denimListType,
},
}),
});

React Component

Create the React component (App) to hold the logic for rendering the denim list. For those unfamiliar with Relay/GraphQL connections, the connections introduce the concept of edges, i.e., this.props.denimList.denims give you a list of instances of DenimTypes but to access each instance you need to access them through this.props.denimList.denims.edge[x].node, where x is the index with the array.

// js/app.js
class App extends React.Component {
onReload() {
this.props.relay.forceFetch();
}

render() {
return (
<div>
<h1>Denim List</h1>
<ul>
{this.props.denimList.denims.edges.map(edge =>
<li key={edge.node.id}>
<strong>{edge.node.brand </strong>’s
<em>{edge.node.model}</em>
has min size: {edge.node.size}
</li>
)}
</ul>
<button onClick={this.onReload.bind(this)}>Reload</button>
</div>
);
}
}

Co-located React Component and GraphQL Data Declaration in Relay

Define a Relay container to co-locate both the React component and the GraphQL data declaration to query for the data (DenimList)

// js/app.js
export default Relay.createContainer(App, {
fragments: {
denimList: () => Relay.QL`
fragment on DenimList {
id,
denims(first: 10) {
edges {
node {
id,
brand,
model,
size,
},
},
},
}
`,
},
})

The entire codebase is here. We built this example from relay-starter-kit.

Once we run the server, and access it, we get the list of denims, and their attributes as shown:

If we inspect the web browser’s ‘Network’ console, we can see what the data Relay/GraphQL query pulled down from the server. Note that it pulled down the DenimList object with a GraphQL auto-generated global Node ID, as well the list of denims (Relay connections/edges), and their attributes.

Notice that on our NodeJS server console, the nodeDefintions log statements (see nodeDefinitions code above) did not execute, i.e., nodeDefinitions methods were NOT executed during the initial fetch.

When Node ID Breaks

When we click ‘reload’ button, which calls this.props.relay.forceFetch() (see React component code above), nothing happens. In fact, on our browser ‘Console’, we can see an error as shown below.

If we look at the ‘Network’ console, we can see that nothing was returned, which was what caused the error.

We can see on the NodeJS console, during re-fetch, the nodeDefinitions methods are called, with the fromGlobalId method correctly mapping the Node ID, used in re-fetch, back to the server-side object type and id.

However, since our dysfunction nodeDefinitions merely return null, that is what the client-side receives.

Handling Node IDs with Care

Most standard nodeDefinitions implementation will just use the server-side object type and id derived by fromGlobalId to retrieve the original server-side object, and return it in response to the re-fetch.

var {nodeInterface, nodeField} = nodeDefinitions(
(globalId) => {
var {type, id} = fromGlobalId(globalId);
if (type === 'DenimList') {
      // ADD THIS instead of 'return null'
// Return your list of denims
// NOTE: we did not utilize id in this simple example
// but we could have used it to retrieve a specific
// database object
return getDenimList();
    } else {
return null;
}
},
(obj) => {
if (obj instanceof DenimList)
      // ADD THIS instead of 'return null'
// Return the GraphQL Schema type definition
// See GraphQL code below
return denimListType;
    } else {
return null;
}
}
);

With that fix, when the ‘reload’ button is clicked, you can see from the browser ‘Network’ console that a 2nd GraphQL request is triggered, and the appropriate DenimList data is returned.

Answers & Summary

These are the answers to the questions we asked at the beginning; they also summarize the things we have learned if you went through the code example above.

How & When Node IDs gets Created?

A Node ID is created for each GraphQL object returned as response if you declare certain keywords pre-defined by the your server-specific GraphQL library (e.g., globalIdField for Javascript, global_id_field for Ruby) when declaring the GraphQL type for that object. Below is an example usage of globalIdField from a GraphQL schema defined in Javascript:

// Javascript example of declaring
// globalIdField for GraphQL type DenimListType
var denimListType = new GraphQLObjectType({
name: ‘DenimList’,
description: ‘List of denims’,
fields: () => ({
    // Use globalIdField keyword to request GraphQL to
// auto-generate node ID when a DenimList object is created
id: globalIdField(‘DenimList’),
    denims: {
...
},
}),
...
});

For most implementations of GraphQL, the library will combine the GraphQL type (in the example above, this is the ‘DenimList’ string passed in to globalIdField as parameter) and the specific server-side object’s id to create the object’s Node ID.

When Node ID is used?

As explained in the official Relay/GraphQL tutorial’s chapter on Object Identification, node ID is used for re-fetching, but when does re-fetch happens?

The first GraphQL request to fetch data is NOT a re-fetch, and Node IDs have not been generated yet, thus NodeIdentification methods are NEVER called. A re-fetch happens when on the client-side we already have existing GraphQL objects with Node IDs, and we:

  • Request for more data for an existing GraphQL object, which can arise from another component relying on the same GraphQL object but requiring different pieces of data of that object
  • Call this.props.relay.forceFetch to re-fetch data of a GraphQL object (as demonstrated in our example above), e.g., when you believe that server-side object data has been changed by external code
  • Call this.props.relay.setVariables to change the parameters used to fetch the original GraphQL object, e.g., fetching a profile picture of a different size for the same GraphQL User object

How are ‘nodeDefinitions’ Methods Used?

For GraphQL types that you want to re-fetch, you request GraphQL to auto-generate a Node ID for the object during creation (see above ‘How and When Node IDs are create?’), and you also need to tell GraphQL how to map the Node ID provided during re-fetch into the corresponding GraphQL object. You do this with pre-defined keywords of the language-specific GraphQL library, e.g., forJavascript use ‘interfaces: [nodeInterface]’, for Ruby use ‘interfaces [NodeIdentification.interface]’. Below is a Javascript example:

var denimListType = new GraphQLObjectType({
name: ‘DenimList’,
description: ‘List of denims’,
fields: () => ({
id: globalIdField(‘DenimList’),
denims: {
...
},
}),
  // Declare that this GraphQL type uses nodeInterface for
// re-fecthing by node ID
interfaces: [nodeInterface],
});

Note that nodeInterface, and NodeIdentification.interface refers to the actual implementation of nodeDefinitions provided above, which performs the mapping, and has 2 methods:

  • Transform the Node ID provided in re-fetch into a server-side object that represents the same the GraphQL object returned in the original response
  • Identify the GraphQL type given the server-side object

Conclusion

If your object needs to be re-fetched, ensure that you

  • Declare globalIdField on your GraphQL type (in GraphQL schema) to make GraphQL auto-generate a unique global Node ID for your GraphQL object during creation
  • Implement nodeDefinitions methods to map Node ID back to the corresponding server-side object, which you then can use to return other or updated attributes, etc. when a re-fetch provides the Node ID
  • Declare nodeInterface on your GraphQL type to instruct GraphQL to use the nodeDefinitions implementation to resolve between Node ID and actual server-side objects
One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.