This project was initially developed at Kiwi.com for education purposes and was agreed upon developing it further outside of this company.
This repository contains examples of common patterns used in real-world applications, so you don't have to re-invent the wheel every time. It currently contains following examples:
@adeira/relay
package usage- simple fetching using
useFragment
hook API - bi-directional pagination using
createRefetchContainer
(also known as window pagination) - "load more" pagination using
createRefetchContainer
ANDcreatePaginationContainer
- query polling (live queries) with simple A/B test
- example of local schema via
commitLocalUpdate
with local storage - server side rendering
Additional learning resources:
- official documentation: https://relay.dev/docs/en/introduction-to-relay
- advanced and experimental features: https://mrtnzlml.com/docs/relay
- source-code: https://github.com/facebook/relay
yarn install
yarn start
You should regenerate Relay files in case you are changing Relay fragments:
yarn relay
This is necessary because Relay is not working with the GraphQL code you write directly. Instead, it generates optimized metafiles to the __generated__
folder, and it's working with these files. It's a good idea to check what files are being regenerated and sometimes even look inside and read them. You'll eventually learn a lot about how it actually works and what optimizations are actually being done.
Run this command to get fresh GraphQL schema:
yarn schema
We use @adeira/relay
internally to help with some difficult Relay tasks and to share knowledge via code across all our teams. It exposes high-level API very similar to Facebook Relay. The key element is so-called Query Renderer. This renderer expects root query which will be automatically fetched and function to call (with the new data) to render some UI:
import * as React from 'react';
import { graphql, QueryRenderer } from '@adeira/relay';
import type { AppQueryResponse } from '__generated__/AppQuery.graphql';
function handleResponse(props: AppQueryResponse) {
const edges = props.allLocations?.edges ?? [];
return (
<ol>
{edges.map((edge) => (
<li key={edge?.node?.id}>{edge?.node?.name}</li>
))}
</ol>
);
}
export default function App(props) {
return (
<QueryRenderer
query={graphql`
query AppQuery {
allLocations(first: 20) {
edges {
node {
id
name
}
}
}
}
`}
onResponse={handleResponse}
/>
);
}
The package @adeira/relay
exposes correct Flow types, so you can just require it and use it. There are other key elements helping us to build the applications well: @adeira/eslint-config
and @adeira/babel-preset-adeira
. The eslint config prevents you from using Relay incorrectly, and the Babel preset helps us to write modern JS including the graphql ...
syntax and using optional chain (a?.b
) which is very common in our applications.
It is correct to write the whole query into Query Renderer. However, as application grows it's necessary to decompose the root application component into smaller parts. Relay copies React components exactly so when you write new component then you should specify there data requirements as well. This is how we could refactor the previous example - first, let's move the whole query into separate component using useFragment
hook:
import { graphql, useFragment } from '@adeira/relay';
export default function AllLocations(props) {
const data = useFragment(
graphql`
fragment AllLocations on RootQuery
allLocations(first: 20) {
edges {
node {
id
name
}
}
}
}
`,
props.data,
);
// TODO: move the React code here (iterate `data` variable)
}
Please note: you could decompose it on many levels and it all depends on your needs and experience where to cut the line. This is handy for the future steps around pagination but otherwise it would be OK to just decompose the single location with id
and name
.
Now, we have to modify the original application to use our new component:
import * as React from 'react';
import { graphql, QueryRenderer } from '@adeira/relay';
import type { AppQueryResponse } from '__generated__/AppQuery.graphql';
import AllLocations from './AllLocations';
function handleResponse(props: AppQueryResponse) {
return <AllLocations data={props} />;
}
export default function App(props) {
return (
<QueryRenderer
query={graphql`
query AppQuery {
...AllLocations
}
`}
onResponse={handleResponse}
/>
);
}
And that's it - we have two components and they describe what data they need exactly. Our first component needs to iterate all locations and requires id
and name
. Our second component requires data for AllLocations
but doesn't care more about what data is it actually. This is very important concept in Relay and in GraphQL in general: always describe what you need in the component itself. It is important because it's 1) very explicit and you can be sure that you are not gonna break anything when refactoring the component and 2) you can easily use the component somewhere and just spread the requirements using ...AllLocations
. This is crucial for composing UI from many many React components.
The best fit for bi-directional (sometimes known as "window" or "next/prev" pagination) is createRefetchContainer
. This container is the best when you are changing variables in the component fragment (which is exactly our use-case). Pagination in GraphQL world works like this (try in https://graphql.kiwi.com/):
{
allLocations(first: 5) {
edges {
cursor
node {
name
}
}
}
page: allLocations(first: 1, after: "YXJyYXljb25uZWN0aW9uOjI=", last: null, before: null) {
edges {
node {
name
}
}
}
}
This query returns 5 results and let's say the middle one has ID YXJyYXljb25uZWN0aW9uOjI=
. To get page after this page you have to query it with first/after
combo. To get a previous page you have to use last/before
combo. It can be a bit burdensome to work with the cursor manually, so you can also use pageInfo
field (that's exactly how it works in our Relay example). There are only few steps you have to do in order to make it work in Relay:
- Export component using
createRefetchContainer
. This component accepts the raw React component as a first argument, regular GraphQL fragment as a second argument (start with the same fragment as from theuseFragment
hook) and last argument is a query which is going to be called during the refetch. - Describe what variables your fragment needs using
@argumentDefinitions(argName: { type: "Int!", defaultValue: null })
. - Pass down all the variables from the query to the fragment using
@arguments(argName: $argName)
and finally: - Call
props.relay.refetch
with the variables necessary for the refetch query.
Tip: you don't have to specify the defaultValue
in arguments definition. It can be a bit difficult because GraphQL strings cannot contain string substitutions. It's a good idea to pass it down from the parent component using @arguments
just like you do in the refetch query.
Check LocationsPaginatedBidirectional.js
for the implementation.
Load more pagination works almost exactly the same but there are two important differences:
- we paginate only forward (or backwards) and only loading new records
- old records are not being updated because new records are only appended (or prepended)
To do this we can easily use createRefetchContainer
as well and just annotate the fragment with @connection
directive. This annotation implements cursor based pagination automatically (best practice in GraphQL these days) and it merges the edges from subsequent fetches into the store after the previous edges. This is exactly what we need for the "load more" feature.
There is also createPaginationContainer
which simplifies this one particular flow so you don't have to manage pageInfo
manually. The difference is minimal and all the containers are to some extend interchangeable. These steps are necessary in order to make the createPaginationContainer
work:
- Export component using
createPaginationContainer
with standard API: first argument is the raw React component and second argument is a fragment. - Add object to the 3rd argument with two fields: refetch
query
andgetVariables
which is a function to get the variables for this query. - Annotate the fragment with
@argumentDefinitions
and refetch query with@arguments
. - And lastly add
@connection(key: " ... ")
to the fragment to signify that we want to append the records and not replace them.
Check these examples for the actual implementation:
Relay supports subscriptions and experimental live queries via polling to allow modifications to the store whenever a payload is received. Query polling is a simple (but very powerful) way how to achieve live data updates without any change to infrastructure or complicated changes to your code. All you need to do is to instruct your query renderer to update Relay cache every few seconds using cacheConfig.poll
:
import React from 'react';
import { graphql, QueryRenderer } from '@adeira/relay';
export default function Polling() {
return (
<QueryRenderer
query={graphql`
query PollingQuery {
currency(code: "czk") {
code
rate
}
}
`}
cacheConfig={{
poll: 5000, // update UI every 5 seconds
}}
onResponse={(data) => {
// this callback is gonna be called every time your data change
console.log(data);
}}
/>
);
}
This is preferable solution over subscriptions in many cases because:
- it's extremely simple (we added only one property and it's good to go)
- there is no need to update the server code
- auth works by default correctly since it's the same like query fetching
- it reconnects to the failed server by design
- it doesn't open expensive persistent websockets (and you don't need such an infrastructure)
We currently do not have docs or example in this repository. Would you like to contribute?
We currently do not have docs or example in this repository. Would you like to contribute?
We currently do not have docs or example in this repository. Would you like to contribute?
We currently do not have docs or example in this repository. Would you like to contribute?
We currently do not have example in this repository. Would you like to contribute?
Local updates are handy in cases you'd like to extend GraphQL schema provided by server and add some additional fields relevant only for your client application. First, you have to define your local schema in file with extension *.graphql
. This file must be located somewhere in the scope of Relay Compiler:
"""
Extend type: https://graphql.github.io/graphql-spec/draft/#sec-Object-Extensions
"""
extend type Article {
draft: String!
}
"""
Or add new query: https://github.com/facebook/relay/issues/1656#issuecomment-382461183
"""
extend type Query {
errors: [Error!]
}
type Error {
id: ID!
message: String!
}
Now, just use commitLocalUpdate
from @adeira/relay
to update the local store:
Relay.commitLocalUpdate(environment, store => {
const articleID = 'f9496862-4fb7-4a09-bc05-a9a3ce2cb7b3'; // ID of the `Article` type you want to update
store.get(articleID).setValue('My custom draft text', 'draft');
// or create new types:
const root = store.getRoot();
const errRecord = store.create('ID_1', 'Error');
errRecord.setValue('ID_1', 'id');
errRecord.setValue('My custom error message', 'message');
root.setLinkedRecords([errRecord, ...], 'errors');
});