Relay 2: simpler, faster, more predictable
Greg Hurrell
@wincent
What we'll be covering
- Relay today
- "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
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
}
}
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
- Building large query strings at runtime.
- Uploading large query strings at runtime.
- "Diffing".
- Computing and storing query paths (for refetchability).
- Maintaining "tracked queries" (for mutations).
- Splitting off deferred queries.
"Reserved for future expansion"
- Rendering directly off the network.
- Pre-normalized responses.
- Native record storage.
A world without diffing
- Simplified "boolean" diffs
- No more partial fetches
- No more
setVariables
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
- Name: Identifies the mutation to run.
- Input: Variable.
- Query: Data to be fetched.
- Configuration: How to process the response.
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
- Client fields get stripped out from server interaction
- Handlers can synthesize client fields
- Cache-reading code, garbage collection and mutations are client-field-aware
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
- Compiler: Done.
- Connections: Prototyped.
- Mutations: In progress.
- Client fields: Exploration underway.