Relay 2: simpler, faster, more predictable

Greg Hurrell

@wincent

What we'll be covering

  1. Relay today
  2. "Relay 2"

Relay today

Relay is a framework for building data-driven applications with React and GraphQL

const app = (data) => view;

Hierarchical user interface

Hierarchical data dependencies

Hierarchical query language

Hierarchical user interface (React)

Hierarchical data dependencies (JSON)

Hierarchical query language (GraphQL)

{
  "header": {
    "notifications": [{
      "text": "...",
      "timestamp": 1470034847
    }]
  },
  "sidebar": {
    "bookmarks": [{
      "name": "...",
      "url": "...",
    }]
  },
  "content": {
    "feedItems": [{
      "title": "...",
      "thumbnailUrl": "..."
    }],
  },
}
query ProfileQuery {
  node(id: 4) {
    ... on User {
      address {
        city
        street
        zipCode
      }
      name
    }
  }
}
query FeedQuery($after: String) {
  viewer {
    feed(first: 3, after: $after) {
      count
      edges {
        cursor
        node {
          body
          likeCount
          title
        }
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}
{
  fragments: {
    profilePic: () => Relay.QL`
      fragment on User {
        name
        profilePic
      }
    `,
  },
}

Compose React components to make a UI

The composition of all GraphQL fragments

equals

The total data requirements of the app

Relay is the glue that holds it all together

Usage at Facebook: React Native

Ads Manager

Groups

Facebook

Usage at Facebook: web

Mobile site

Internal tools

Web site

Open source

6.8k watchers

560 forks

675 pull requests

20 releases

Relay 2

Simpler, faster, more predictable

Declarative API

Don't worry about how the data is fetched.

Or even when it is fetched

The framework orchestrates the data-fetching

Colocated GraphQL with React components

Performance

Aggregation into batches.

Caching.

Efficient shouldComponentUpdate.

Big bets

Persisted queries

RelayConnection

Fully static Relay 2

Persisted queries

{
  "query": "query PostsIndexQueries {viewer {id,...F3}}↩
  fragment F0 on Node {id,__typename} fragment F1 on↩
  Tagged {tags,__typename,...F0} fragment F2 on Post↩
  {id,title,createdAt,updatedAt,url,body {_html3xPqt8:↩
  html(baseHeadingLevel:2)},...F1} fragment F3 on User↩
  {_posts3KBCho:posts(first:3) {edges {node {id,...F2},↩
  cursor},pageInfo {hasNextPage,hasPreviousPage}},id}↩
  [thousands of lines]↩
  ...",
  "variables": {
    "first": 10,
    ...
  }
}
{
  "documentId": "1234",
  "variables": {
    "first": 10
  }
}

https://github.com/graphql/express-graphql/pull/109

https://goo.gl/ATFYSc

Native prefetching

RelayConnection

RelayConnection timeline

Going further

What if every query in Relay were persistable?

Problem: dynamism makes query persistence hard

{
  fragments: {
    article: () => Relay.QL`
      fragment on Article {
        title
        ${Tags.getFragment('tagged')}
      }
    `,
  },
}
{
  fragments: {
    article: function article() {
      return function (RQL_0) {
        return {
          children: [].concat.apply([], [{
            fieldName: 'title',
            kind: 'Field',
            metadata: {},
            type: 'String'
          }, _reactRelay.default.QL.__frag(RQL_0)]),
          kind: 'Fragment',
          metadata: {},
          name: 'Article_ArticleRelayQL',
          type: 'Article'
        };
      }(_Tags2.default.getFragment('tagged'));
    }
  }
}
Relay.createContainer(Parent, {
  fragments: {
    parentFragment: () => Relay.QL`
      fragment on Foo {
        id
        ${Child.getFragment('childFragment', {size: 128})}
      }
    `,
  }
});
module.exports = Relay.createContainer(ProfilePicture, {
  initialVariables: {size: 50},
  prepareVariables: prevVariables => {
    return {
      ...prevVariables,
      size: prevVariables.size * window.devicePixelRatio,
    };
  },
  // ...
});

Relay 2 fragments are entirely static

fragment UserFragment on User {
  id
  name
  ...ProfilePicFragment
}

fragment ProfilePicFragment on User {
  profilePicture {
    height
    width
    url
  }
}

But wait...

If fragment references are static, how can we pass variables from parent to child?

It is not possible to pass arguments to a fragment in GraphQL...

... or is it?

What if we were to polyfill GraphQL?

Relay 2 uses @directives to polyfill GraphQL

fragment UserFragment on User {
  name
  ...ProfilePicFragment @arguments(size: 128)
}
fragment ProfilePicFragment
      on User @argumentDefinitions(
  size: {type: "Int", defaultValue: 64}
) {
  profilePicture(size: $size) {
    height
    uri
    width
  }
}

Compiled output is entirely static

{
  "argumentDefinitions": [
    {
      "kind": "LocalArgument",
      "name": "id",
      "type": "ID!",
      "defaultValue": null
    }
  ],
  "kind": "Root",
  "name": "TestQuery",
  "operation": "query",
  "selections": [
    {
      "kind": "LinkedField",
      "alias": null,
      "concreteType": null,
      "name": "node",
      "plural": false,
      "selections": []
      "storageKey": null
    }
  ]
}

Whole-program analysis

All variables either end up resolving to inline literals

Or global variables supplied with the query

Optimizations

Skipping redundant fields

Removing unreachable nodes

Flattening fragments with matching types

Filtering out unreferenced fragments

Skipping redundant fields

actor {
  id
  ... on Actor {
    name
    ... on User {
      name # fetched by parent
      lastName
      ... on User {
        lastName# fetched by parent
      }
    }
  }
}
actor {
  id
  ... on Actor {
    name
    ... on User {
      lastName
    }
  }
}

Removing unreachable nodes

node(id: $id) {
  ... on User @include(if: false) {
    id
    name
  }
}

Flattening fragments with matching types


node(id: $id) {
  id
  .... on Node {
    id
  }
  ... on User {
    ... on Node {
      id {
    }
    firstName
    surname: lastName
    ... on User {
      lastName
    }
  }
}
node(id: $id) {
  id
  ... on User {
    firstName
    lastName
    surname: lastName
  }
}

Filtering out unreferenced fragments

query ViewerQuery {
  viewer {
    ...ReferencedFragment
  }
}

fragment ReferencedFragment on Viewer {
  ... on User {
    name
  }
}

fragment UnreferencedFragment on Viewer {
  ... on User {
    id
  }
}
query ViewerQuery {
  viewer {
    ...ReferencedFragment
  }
}

fragment ReferencedFragment on Viewer {
  ... on User {
    name
  }
}

# UnreferencedFragment removed.

Timeline

Normalization

Network format

{
  "viewer": {
    "id": 1000,
    "father": {
      "id": 1001,
      "name": "James",
      "pet": {"id": 5000, "name": "Skip"}
    },
    "mother": {
      "id": 1002,
      "name": "Jane",
      "pet": {"id": 5000, "age": 5}
    }
  }
}

Cache format

{
  "1000": {
    "father": {"__dataID__": 1001},
    "mother": {"__dataID__": 1002},
  },
  "1001": {
    "name": "James",
    "pet": {"__dataID__": 5000}
  },
  "1002": {
    "name": "Jane",
    "pet": {"__dataID__": 5000}
  },
  "5000": {
    "name": "Skip",
    "age": 5
  }
}

Time to normalize a complex feed story

Before

50ms

After

5ms

TTI

Time to interaction

Laziness, impatience and hubris

"Reserved for future expansion"

A world without diffing

Splitting deferred queries

@defer

Query batching

Client-side batching adapter

DataLoader and caching

Server-side batching adapter

Internal batch protocol

New imperative mutations API

Content of a mutation

Mutation queries are static

No more fat queries

No more tracked queries

update(store => {
  const page = store.get('4');
  const viewCount = page.getValue('viewCount');
  page.setValue('viewCount', viewCount + 1);
});

Client-side fields

fragment ExampleFragment on User {
  drafts
}

extend type User {
  drafts: PostsConnection
}
fragment ExampleFragment on Node {
  ... on Thing {
    id
  }
}

fragment ThingFragment on Thing {
  id
}

type Thing {
  id: ID!
}

Client fields in practice

Summarizing

Static everything

GraphQL polyfills

Parametrized fragments

Whole-program analysis

Imperative mutations

Client-side fields

Faster

Simpler

More predictable

When can I start using this?

Current status

Migration pathway

Compatibility container

Goal

Multiple React Native products using Relay 2 in production by end of half

Next

Bringing Relay 2 to the web

https://facebook.github.io/relay

@wincent