Save 37% off PRO during our Black Friday Sale! »

Relay Deep Dive

Relay Deep Dive

Relay is an open source data-fetching framework for React applications invented and used at Facebook. The Relay Deep Dive explores some aspects of its design and internal implementation. It will show how Relay turns declarative descriptions of data-fetching requirements into imperative operations behind the scenes, and how these aim to provide efficiency, performance, predictability, and consistency.

https://facebook.github.io/relay/

Part of a series of tech talks at Facebook HQ on April 19 2016. Greg Hurrell is a member of the Relay team at Facebook.

Video:

https://www.youtube.com/watch?v=oPSuvaYmXBY

Cb12fe50ffb673e2031ee9bd339a6522?s=128

Greg Hurrell

April 19, 2016
Tweet

Transcript

  1. Relay Deep Dive 19 April 2016 Greg Hurrell (@wincent)

  2. Relay Data-management for React applications

  3. Data-fetching framework for React applications

  4. Data-fetching framework for React applications

  5. Data-management for React applications

  6. Enterprise Object Data Lifecycle Management for React applications

  7. Enterprise Object Data Lifecycle-management for React applications

  8. Data-management for component-based view architectures

  9. What we'll cover • React <-> Relay <-> GraphQL •

    How Relay integrates with the view layer • Transform (Babel plug-in) • Data-fetching pipeline • Traversals • Query modeling • End-to-end flow from fetch-to-render
  10. React

  11. React is a JavaScript library for building user interfaces

  12. Build using declarative, reusable, composable components

  13. Turtles Components all the way down

  14. Component hierarchies as nested objects

  15. Component hierarchies as trees

  16. GraphQL

  17. GraphQL query StoryQuery { node(id: 1) { actor { name

    } createdAt updatedAt title } }
  18. GraphQL query StoryQuery { node(id: 1) { actor { name

    } createdAt updatedAt title } }
  19. GraphQL query StoryQuery { node(id: 1) { actor { name

    } createdAt updatedAt title } } { "node": { "actor": { "name": "Greg" }, "createdAt": "2015-10-26", "updatedAt": "2015-10-26", "title": "Relay Deep Dive" } }
  20. GraphQL query StoryQuery { node(id: 1) { actor { name

    } createdAt updatedAt title } } { "node": { "actor": { "name": "Greg" }, "createdAt": "2015-10-26", "updatedAt": "2015-10-26", "title": "Relay Deep Dive" } }
  21. React

  22. React GraphQL

  23. React GraphQL Relay

  24. Relay colocates your data fetching queries with your components Fragment

    Fragment Fragment Fragment Fragment Fragment Fragment Fragment
  25. Each component gets exactly (and only) the data it needs

    Data Data Data Data Data Data Data Data
  26. Why colocate?

  27. Why colocate? • Avoid overfetching (for performance)

  28. Why colocate? • Avoid overfetching (for performance) • Avoid underfetching

    (for stability)
  29. Why colocate? • Avoid overfetching (for performance) • Avoid underfetching

    (for stability) • Enable component-local reasoning
  30. Why colocate? • Avoid overfetching (for performance) • Avoid underfetching

    (for stability) • Enable component-local reasoning • Reusability
  31. Why colocate? • Avoid overfetching (for performance) • Avoid underfetching

    (for stability) • Enable component-local reasoning • Reusability • Fearless refactoring
  32. Why colocate? • Avoid overfetching (for performance) • Avoid underfetching

    (for stability) • Enable component-local reasoning • Reusability • Fearless refactoring • Developer velocity
  33. Query aggregation Fragment Fragment Fragment Fragment Fragment Fragment Fragment Fragment

  34. Query aggregation Data Data Data Data Data Data Data Data

    Fragment Fragment Fragment Fragment Fragment Fragment Fragment Fragment
  35. Query aggregation Fragment Fragment Fragment Fragment Fragment Fragment Fragment Fragment

    Data Data Data Data Data Data Data Data
  36. Containers

  37. Containers Configuration

  38. Containers Configuration Logic

  39. "Higher-Order" Components Story Relay.createContainer(Story, {fragments}) const WrappedComponent = wrap(<Other />);

  40. A fully-wrapped component tree

  41. Fragments: pre-transform { fragments: { post: () => Relay.QL` fragment

    on Post { createdAt title ${Tags.getFragment('tagged')} } ` }, }
  42. Fragments: post-transform post: function post() { return (function (RQL_0) {

    return { children: [{ fieldName: 'id', kind: 'Field', metadata: { isGenerated: true, isRequisite: true }, type: 'ID' }, _reactRelay2.default.QL.__frag(RQL_0)], hash: 'NrTO0r2X', kind: 'Fragment', metadata: {}, name: 'PostPreview', type: 'Post' }; })(_Tags2.default.getFragment('tagged')); }
  43. The concrete AST

  44. The concrete AST • Objects with simple properties: POJOs

  45. The concrete AST • Objects with simple properties: POJOs •

    "Concrete Nodes"
  46. The concrete AST • Objects with simple properties: POJOs •

    "Concrete Nodes" • Metadata from schema
  47. Why transform? fieldName: 'id', kind: 'Field', metadata: { isGenerated: true,

    isRequisite: true }, type: 'ID'
  48. RelayQuery.* • Immutable wrapper for concrete GraphQL nodes • Methods

    for cloning, traversing, querying metadata
  49. Data-fetching Client RelayStore

  50. Data-fetching Client RelayStore Server GraphQL

  51. Data-fetching Client RelayStore Server GraphQL

  52. Data-fetching Client RelayStore Server GraphQL

  53. Data-fetching Client RelayStore

  54. The Store Client RelayStore • A client-side cache. • Normalized,

    flattened graph representation.
  55. Store Data query SampleQuery { node(id: 660361306) { address {

    country } id name hometown { id name } } }
  56. Store Data query SampleQuery { node(id: 660361306) { address {

    country } id name hometown { id name } } } { "660361306": { "address": { "country": "US" }, "hometown": { "id": "115970731750761", "name": "Adelaide" }, "id": "660361306", "name": "Greg Hurrell", } }
  57. { 115970731750761: { __dataID__: '115970731750761', name: 'Adelaide', }, 660361306: {

    __dataID__: '660361306', address: { __dataID__: 'client:1', }, hometown: { __dataID__: '115970731750761', }, id: '660361306', name: 'Greg Hurrell', }, 'client:1': { __dataID__: 'client:1', country: 'US', }, } Store Data { "660361306": { "address": { "country": "US" }, "hometown": { "id": "115970731750761", "name": "Adelaide" }, "id": "660361306", "name": "Greg Hurrell", } }
  58. { 115970731750761: { __dataID__: '115970731750761', name: 'Adelaide', }, 660361306: {

    __dataID__: '660361306', address: { __dataID__: 'client:1', }, hometown: { __dataID__: '115970731750761', }, id: '660361306', name: 'Greg Hurrell', }, 'client:1': { __dataID__: 'client:1', country: 'US', }, } Store Data { "660361306": { "address": { "country": "US" }, "hometown": { "id": "115970731750761", "name": "Adelaide" }, "id": "660361306", "name": "Greg Hurrell", } }
  59. Connections: GraphQLRange { 'client:1000': { __dataID__: 'client:1000', __filterCalls__: [], __range__:

    GraphQLRange(...), }, }
  60. Queued and cached data RelayStore Store data Queued data Cached

    data
  61. Queued and cached data RelayStore Store data Queued data Cached

    data
  62. Relay.Environment.primeCache Data-fetching pipeline Diff Split deferred Subtract Print

  63. Relay.Environment.primeCache Diffing Diff Split deferred Subtract Print

  64. Diffing Root

  65. Diffing Root Store -

  66. Diffing Root Store - =

  67. RelayStore.primeCache Splitting deferred queries Diff Split deferred Subtract Print

  68. Deferred fragments Data we already have (from feed): essential to

    display permalink page. "Nice-to-have" data that we need only render "eventually".
  69. Deferring a fragment fragment on Story { actors { name

    } body title ${StoryMenu.getFragment('story')} ${Comments.getFragment('story').defer()} }
  70. Deferring a fragment fragment on Story { actors { name

    } body title ${StoryMenu.getFragment('story')} ${Comments.getFragment('story').defer()} }
  71. Splitting deferred fragments Root

  72. Splitting deferred fragments Root + = Root

  73. Nested deferred fragments Root

  74. Nested deferred fragments Root

  75. Nested deferred fragments Root

  76. Nested deferred fragments Root

  77. Nested deferred fragments export type SplitQueries = { __parent__: ?SplitQueries;

    __path__: NodePath; __refQuery__: ?RelayRefQueryDescriptor; deferred: Array<SplitQueries>; required: ?RelayQuery.Root; };
  78. Nested deferred fragments export type SplitQueries = { __parent__: ?SplitQueries;

    __path__: NodePath; __refQuery__: ?RelayRefQueryDescriptor; deferred: Array<SplitQueries>; required: ?RelayQuery.Root; };
  79. Nested deferred fragments { required: topLevelQuery, deferred: [{ required: nestedQuery,

    deferred: [{ required: innermostQuery, deferred: [], }], }], }
  80. Nested deferred fragments { required: topLevelQuery, deferred: [{ required: nestedQuery,

    deferred: [{ required: innermostQuery, deferred: [], }], }], }
  81. Nested deferred fragments { required: topLevelQuery, deferred: [{ required: nestedQuery,

    deferred: [{ required: innermostQuery, deferred: [], }], }], }
  82. Nested deferred fragments { required: topLevelQuery, deferred: [{ required: nestedQuery,

    deferred: [{ required: innermostQuery, deferred: [], }], }], }
  83. Nested deferred fragments { required: topLevelQuery, deferred: [{ required: nestedQuery,

    deferred: [{ required: innermostQuery, deferred: [], }], }], }
  84. Nested deferred fragments { required: topLevelQuery, deferred: [{ required: nestedQuery,

    deferred: [{ required: innermostQuery, deferred: [], }], }], }
  85. Traversals

  86. RelayQuery.* Node Root Field Fragment Operation Subscription Mutation

  87. RelayQuery.* Node Root Field Fragment Operation Subscription Mutation

  88. RelayQuery.* viewer fragment on User name hometown fragment on Page

    description ...
  89. RelayQuery.* viewer fragment on User name hometown fragment on Page

    description ...
  90. RelayQuery.* viewer fragment on User name hometown fragment on Page

    description ...
  91. RelayQuery.* viewer fragment on User name hometown fragment on Page

    description ...
  92. RelayQuery.* viewer fragment on User name hometown fragment on Page

    description ...
  93. RelayQueryVisitor class RelayQueryVisitor { visit(node, nextState) {} visitField(node, nextState) {}

    visitFragment(node, nextState) {} visitRoot(node, nextState) {} traverse(node, nextState) {} }
  94. RelayQueryVisitor class RelayQueryVisitor { visit(node, nextState) {} visitField(node, nextState) {}

    visitFragment(node, nextState) {} visitRoot(node, nextState) {} traverse(node, nextState) {} }
  95. RelayQueryVisitor class RelayQueryVisitor { visit(node, nextState) {} visitField(node, nextState) {}

    visitFragment(node, nextState) {} visitRoot(node, nextState) {} traverse(node, nextState) {} }
  96. RelayQueryVisitor class RelayQueryVisitor { visit(node, nextState) {} visitField(node, nextState) {}

    visitFragment(node, nextState) {} visitRoot(node, nextState) {} traverse(node, nextState) {} }
  97. RelayQueryVisitor class RelayQueryVisitor { visit(node, nextState) {} visitField(node, nextState) {}

    visitFragment(node, nextState) {} visitRoot(node, nextState) {} traverse(node, nextState) {} }
  98. RelayQueryVisitor class RelayQueryVisitor { visit(node, nextState) {} visitField(node, nextState) {}

    visitFragment(node, nextState) {} visitRoot(node, nextState) {} traverse(node, nextState) {} }
  99. RelayQueryTransform class RelayQueryTransform extends RelayQueryVisitor { traverse(node, nextState) {} }

  100. RelayQueryTransform class RelayQueryTransform extends RelayQueryVisitor { traverse(node, nextState) {} }

  101. splitDeferredRelayQueries visitFragment(node, splitQueries) { if (node.isDeferred()) { const childSplitQueries =

    {}; const required = this.traverse(node, childSplitQueries); if (required) { const wrapped = wrapNode(required); childSplitQueries.required = wrapped; } if (required || childSplitQueries.deferred.length) { splitQueries.deferred.push(childSplitQueries); } return null; } else if (node.hasDeferredDescendant()) { return this.traverse(node, splitQueries); } else { return node; } }
  102. splitDeferredRelayQueries visitFragment(node, splitQueries) { if (node.isDeferred()) { const childSplitQueries =

    {}; const required = this.traverse(node, childSplitQueries); if (required) { const wrapped = wrapNode(required); childSplitQueries.required = wrapped; } if (required || childSplitQueries.deferred.length) { splitQueries.deferred.push(childSplitQueries); } return null; } else if (node.hasDeferredDescendant()) { return this.traverse(node, splitQueries); } else { return node; } }
  103. splitDeferredRelayQueries visitFragment(node, splitQueries) { if (node.isDeferred()) { const childSplitQueries =

    {}; const required = this.traverse(node, childSplitQueries); if (required) { const wrapped = wrapNode(required); childSplitQueries.required = wrapped; } if (required || childSplitQueries.deferred.length) { splitQueries.deferred.push(childSplitQueries); } return null; } else if (node.hasDeferredDescendant()) { return this.traverse(node, splitQueries); } else { return node; } }
  104. splitDeferredRelayQueries visitFragment(node, splitQueries) { if (node.isDeferred()) { const childSplitQueries =

    {}; const required = this.traverse(node, childSplitQueries); if (required) { const wrapped = wrapNode(required); childSplitQueries.required = wrapped; } if (required || childSplitQueries.deferred.length) { splitQueries.deferred.push(childSplitQueries); } return null; } else if (node.hasDeferredDescendant()) { return this.traverse(node, splitQueries); } else { return node; } }
  105. splitDeferredRelayQueries visitFragment(node, splitQueries) { if (node.isDeferred()) { const childSplitQueries =

    {}; const required = this.traverse(node, childSplitQueries); if (required) { const wrapped = wrapNode(required); childSplitQueries.required = wrapped; } if (required || childSplitQueries.deferred.length) { splitQueries.deferred.push(childSplitQueries); } return null; } else if (node.hasDeferredDescendant()) { return this.traverse(node, splitQueries); } else { return node; } }
  106. splitDeferredRelayQueries visitFragment(node, splitQueries) { if (node.isDeferred()) { const childSplitQueries =

    {}; const required = this.traverse(node, childSplitQueries); if (required) { const wrapped = wrapNode(required); childSplitQueries.required = wrapped; } if (required || childSplitQueries.deferred.length) { splitQueries.deferred.push(childSplitQueries); } return null; } else if (node.hasDeferredDescendant()) { return this.traverse(node, splitQueries); } else { return node; } }
  107. splitDeferredRelayQueries visitFragment(node, splitQueries) { if (node.isDeferred()) { const childSplitQueries =

    {}; const required = this.traverse(node, childSplitQueries); if (required) { const wrapped = wrapNode(required); childSplitQueries.required = wrapped; } if (required || childSplitQueries.deferred.length) { splitQueries.deferred.push(childSplitQueries); } return null; } else if (node.hasDeferredDescendant()) { return this.traverse(node, splitQueries); } else { return node; } }
  108. splitDeferredRelayQueries visitFragment(node, splitQueries) { if (node.isDeferred()) { const childSplitQueries =

    {}; const required = this.traverse(node, childSplitQueries); if (required) { const wrapped = wrapNode(required); childSplitQueries.required = wrapped; } if (required || childSplitQueries.deferred.length) { splitQueries.deferred.push(childSplitQueries); } return null; } else if (node.hasDeferredDescendant()) { return this.traverse(node, splitQueries); } else { return node; } }
  109. splitDeferredRelayQueries visitFragment(node, splitQueries) { if (node.isDeferred()) { const childSplitQueries =

    {}; const required = this.traverse(node, childSplitQueries); if (required) { const wrapped = wrapNode(required); childSplitQueries.required = wrapped; } if (required || childSplitQueries.deferred.length) { splitQueries.deferred.push(childSplitQueries); } return null; } else if (node.hasDeferredDescendant()) { return this.traverse(node, splitQueries); } else { return node; } }
  110. RelayStore.primeCache Query subtraction Diff Split deferred Subtract Print

  111. Query subtraction New query

  112. Query subtraction New query - In-flight queries

  113. Query subtraction New query - In-flight queries = Final query

  114. RelayStore.primeCache Query printing Diff Split deferred Subtract Print

  115. Query printing AST query ViewerQuery { viewer { actor {

    name } isEmployee timezone zodiac } }
  116. RelayStore.primeCache Data-fetching pipeline Diff Split deferred Subtract Print

  117. Data-fetching pipeline

  118. Data-fetching pipeline Server

  119. Data-fetching pipeline Server

  120. Data-fetching pipeline Server

  121. Data-fetching pipeline Server Write payload

  122. Data-fetching pipeline Server Write payload Notify subscribers

  123. Data-fetching pipeline Server Write payload Notify subscribers Read query data

  124. Data-fetching pipeline Server Write payload Notify subscribers Read query data

    Render
  125. visitFragment(node, state) { if (node.isContainerFragment()) { const dataID = getComponentDataID(state);

    const fragmentPointer = new GraphQLFragmentPointer( dataID, node ); this._setDataValue(state, fragmentPointer); } else { this.traverse(node, state); } } Reading query data
  126. visitFragment(node, state) { if (node.isContainerFragment()) { const dataID = getComponentDataID(state);

    const fragmentPointer = new GraphQLFragmentPointer( dataID, node ); this._setDataValue(state, fragmentPointer); } else { this.traverse(node, state); } } Reading query data
  127. Reading query data visitFragment(node, state) { if (node.isContainerFragment()) { const

    dataID = getComponentDataID(state); const fragmentPointer = new GraphQLFragmentPointer( dataID, node ); this._setDataValue(state, fragmentPointer); } else { this.traverse(node, state); } }
  128. Reading query data visitFragment(node, state) { if (node.isContainerFragment()) { const

    dataID = getComponentDataID(state); const fragmentPointer = new GraphQLFragmentPointer( dataID, node ); this._setDataValue(state, fragmentPointer); } else { this.traverse(node, state); } }
  129. Reading query data visitFragment(node, state) { if (node.isContainerFragment()) { const

    dataID = getComponentDataID(state); const fragmentPointer = new GraphQLFragmentPointer( dataID, node ); this._setDataValue(state, fragmentPointer); } else { this.traverse(node, state); } }
  130. Trees

  131. Other topics • Garbage collection • Mutations • Tracked queries

    & "Fat" queries • GraphQL Subscriptions • Routing • Server rendering • And many others...