RELAY EXPERIMENTAL
2019-09-01
The relay-experimental
package was recently merged into the Relay master
branch,
providing us with some much needed insight into how the Relay hooks API will look once it has been released.
Although we're told an actual release might be a couple of months away, we don't have to wait. I for one, have never been known as a particularly patient individual.
Building Relay from source
First up, we need to tell Gulp to build the relay-experimental
package.
My local fork of Relay is located at
~/relay/
.
Open up ~/relay/gulpfile.js
, and find the const builds = [...]
declaration.
At the time of this writing, you will find it at line 110
.
Inside the builds
array, we'll add the relay-experimental
package.
It should look something like this:
// ~/relay/gulpfile.js
const builds = [
{
package: 'relay-experimental',
exports: {
index: 'index.js',
},
bundles: [
{
entry: 'index.js',
output: 'relay-experimental',
libraryName: 'RelayExperimental',
libraryTarget: 'umd',
},
],
},
...
];
Once done, save the file and close it.
Running git status
should now output the following:
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: gulpfile.js
no changes added to commit (use "git add" and/or "git commit -a")
Resolving broken imports
EDIT SEPTEMBER 14: The submitted PR was merged, so these changes are no longer required!
There are currently three files containing broken imports:
useRelayEnvironment.js
RelayEnvironmentProvider.js
useRefetchableFragmentNode.js
But no need to worry! These are easily solved. We will simply open these files and make a quick change.
useRelayEnvironment.js
// ~/relay/packages/relay-experimental/useRelayEnvironment.js
// line 14
const ReactRelayContext = require("react-relay/ReactRelayContext");
// =>
const { ReactRelayContext } = require("react-relay");
RelayEnvironmentProvider.js
// ~/relay/packages/relay-experimental/RelayEnvironmentProvider.js
// line 15
const ReactRelayContext = require("react-relay/ReactRelayContext");
// =>
const { ReactRelayContext } = require("react-relay");
useRefetchableFragmentNode.js
First, add RelayModernRecord
as a public export from relay-runtime
.
// ~/relay/packages/relay-runtime/index.js
// add to imports
const RelayModernRecord = require('./store/RelayModernRecord');
// add to exports
module.exports = {
Record: RelayModernRecord,
...
}
Then update the imports as usual.
// ~/relay/packages/relay-experimental/useRefetchableFragmentNode.js
// line 536
const RelayModernRecord = require("relay-runtime/store/RelayModernRecord");
// =>
const { Record: RelayModernRecord } = require("relay-runtime");
// line 563
const RelayModernRecord = require("relay-runtime/store/RelayModernRecord");
// =>
const { Record: RelayModernRecord } = require("relay-runtime");
// line 594
const { ID_KEY } = require("relay-runtime/store/RelayStoreUtils");
// =>
const { ID_KEY } = require("relay-runtime");
And that's it!
Running git status
again should now output the following:
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: gulpfile.js
modified: packages/relay-experimental/RelayEnvironmentProvider.js
modified: packages/relay-experimental/useRefetchableFragmentNode.js
modified: packages/relay-experimental/useRelayEnvironment.js
modified: packages/relay-runtime/index.js
no changes added to commit (use "git add" and/or "git commit -a")
I have opened a pull request, so these changes will hopefully become unnecessary in the near future. Merged!
Compilation
Now we're ready to compile the source code into consumable production code.
Navigate to the root of the Relay project and run the following command:
$ npm run build
This command will run gulp
with the gulpfile
configuration we previously edited,
and output compiled files in the dist
directory, located at the very root of the project.
If you've built Relay from source before, you may need to run the cleanup script first:
$ npm run build:clean
Packing for distribution
relay-experimental
is now ready for distribution.
Navigate to the folder containing the build, and run npm pack
:
$ cd ~/relay/dist/relay-experimental
$ npm pack
At Facebook, most (if not all?) projects are part of one big monorepo, which essentially means they're all running the latest master
build.
Since relay-experimental
was recently ported to Relay open source, it relies on the most recent build.
Therefore, installing relay-experimental
alone is insufficient,
due to its incompatibility with the overall version 5.0.0
of Relay.
We need to pack react-relay
,relay-runtime
and relay-compiler
as well.
Packing react-relay
:
$ cd ~/relay/dist/react-relay
$ npm pack
Packing relay-runtime
:
$ cd ~/relay/dist/relay-runtime
$ npm pack
Packing relay-compiler
:
$ cd ~/relay/dist/relay-compiler
$ npm pack
Running the npm pack
command creates a tarball .tgz
,
producing the exact file that would've been published to npm, had we run npm publish
instead.
This file can be copied, moved and uploaded however you please.
Installing to project
Now that we've built and packed relay-experimental
, react-relay
, relay-runtime
and relay-compiler
, they're ready for installation.
To install them locally, simply navigate to your project and install the packages from path:
$ npm install ~/relay/dist/relay-experimental/relay-experimental-5.0.0.tgz $ npm install ~/relay/dist/react-relay/react-relay-5.0.0.tgz $ npm install ~/relay/dist/relay-runtime/relay-runtime-5.0.0.tgz $ npm install ~/relay/dist/relay-compiler/relay-compiler-5.0.0.tgz
If you're using third party Relay libraries, you may need to enforce package resolutions for your project:
// package.json
{
"resolutions": {
"react-relay": "file:../relay/dist/react-relay/react-relay-5.0.0.tgz",
"relay-runtime": "file:../relay/dist/relay-runtime/relay-runtime-5.0.0.tgz",
"relay-compiler": "file:../relay/dist/relay-compiler/relay-compiler-5.0.0.tgz",
"relay-experimental": "file:../relay/dist/relay-experimental/relay-experimental-5.0.0.tgz"
},
...
}
Followed by running npm install
.
Ofcourse, if you're deploying to another machine, you may need to host these packages elsewhere. Although beyond the scope of this post, suffice to say that any registry (i.e. npmjs,proget) will do.
Relay version "not 6.0.0"
Now that Relay has been upgraded to "not 6.0.0", some import paths have been altered.
I only encountered this problem with RelayQueryResponseCache
, which as an example has been changed from
relay-runtime/lib/RelayQueryResponseCache
to relay-runtime/lib/network/RelayQueryResponseCache
.
Luckily, it has also been added as a module export:
import { QueryResponseCache } from "relay-runtime";
Should you encounter a similar issue (but with a different API), I advise you to first try importing it as a module,
and then looking for the new path at ~/relay/dist/<package>/lib/
in case it doesn't exist as a module export.
API
- fetchQuery
- RelayEnvironmentProvider
- useRelayEnvironment
- useQuery
- useFragment
- useRefetchableFragment
- usePaginationFragment
fetchQuery
Not unlike the v5 iteration of fetchQuery
, it fetches the given operation in an imperative manner - outside of React.
This version however, also implements query de-duplication by checking for in-flight requests with matching parameters (query, variables) upon each request.
A RelayObservable is returned by default, which is a limited implementation of the ESObservable proposal. The primary benefit of Observable, is the ability to subscribe to updates with a callback in a synchronous manner.
This implementation of fetchQuery
function returns a disposable, which can be called to
cancel an in-flight request.
Example usage:
const dispose = fetchQuery(environment, query, variables).subscribe({
// Called when network requests starts
start: (subsctiption) => {},
// Called after a payload is received and written to the local store
next: (payload) => {},
// Called when network requests errors
error: (error) => {},
// Called when network requests fully completes
complete: () => {},
// Called when network request is unsubscribed
unsubscribe: (subscription) => {},
});
// cancel the in-flight request
dispose();
The RelayObservable from fetchQuery
can be converted to a Promise, which will instead
resolve to a snapshot of the query data when the first (and only first) response is received from the server.
Converting RelayObservable to a Promise will nullify the returned disposer function.
Example usage:
fetchQuery(environment, query, variables).then((data) => {
// do something with data
});
It's important to know that unlike useQuery
, fetchQuery
does NOT retain query data, meaning that it is not guaranteed
that the fetched data will remain in the Relay store after the request has been completed.
To clarify, fetchQuery
from relay-experimental
is just like fetchQuery
from relay-runtime
,
with the addition of de-duplicating requests and returning an observable which can be cancelled or converted to a promise.
RelayEnvironmentProvider
Before we can use any hooks, Relay requires access to the Environment
via a built-in context provider.
This is meant to be done only once, at the very root of your application.
As an example, I've created an arbitrary Providers
component,
which wraps the entire application with RelayEnvironmentProvider
.
Example usage:
// Providers.js
import environment from "./environment";
import { RelayEnvironmentProvider } from "relay-experimental";
function Providers() {
return (
<RelayEnvironmentProvider environment={environment}>
<App />
</RelayEnvironmentProvider>
);
}
This is the same pattern as implemented in relay-fns. (Yeah it's a shameless plug - sue me.)
useRelayEnvironment
If you're familiar with React's Context API, useRelayEnvironment
should require no explanation as to what it does.
Just in case you aren't; It's basically a function that "consumes",
or "captures" the Context value from a parent provider - in this case environment
from RelayEnvironmentProvider
.
Example usage:
import { useRelayEnvironment } from "relay-experimental";
function App() {
const environment = useRelayEnvironment();
// do something with environment
}
useQuery
The hook implementation of QueryRenderer.
Instead of the render
callback, this hook will suspend the component upon a request,
which means it will throw a Promise and render a fallback component until data is received.
It takes three unnamed parameters:
- The graphql tagged query. (i.e. graphql``)
- The GraphQL query variables, in the shape of an object mapping from variable name to value.
- An object containing a set of options:
fetchKey
: String or a number. Acts as a custom cacheKey.fetchPolicy
: Enum. Setting for how a query may be fetched.'store-only'
: Returns local data. No request is made.'store-or-network'
: Returns local data if available, otherwise suspends and makes a request.'store-and-network'
: Returns local data and then makes a request.'network-only'
: Always suspends and sends a request, even if data is available locally.
networkCacheConfig
: An object containing settings for how a query response may be cached.force
: causes a query to be issued unconditionally, irrespective of the state of any configured response cache.poll
: causes a query to live update by polling at the specified interval in milliseconds. (This value will be passed to setTimeout.)liveConfigId
: causes a query to live update by calling GraphQLLiveQuery, it represents a configuration of gateway when doing live querymetadata
: user-supplied metadata.transactionId
: a user-supplied value, intended for use as a unique id for a given instance of executing an operation.
Example usage:
Make sure we have a React.Suspense
boundary in place.
// App.js
function App() {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<TodoList />
</React.Suspense>
);
}
And then use useQuery
to request some data.
// TodoList.js
import { graphql } from "react-relay";
import { useQuery } from "relay-experimental";
function TodoList(props) {
const data = useQuery(
graphql`
query TodoListQuery {
viewer {
todos {
id
...TodoItemFragment
}
}
}
`
);
return data.todos.map((todo) => <TodoItem todo={todo} />);
}
Because of suspension, we never have to worry about data being null.
Once the component is rendered, the request sent by useQuery
will have been resolved.
I'm happy to say that useQuery
replaces the need for useLocalQuery
from react-relay-local-query.
Example usage:
If you recall, fetchPolicy
value store-only
does not send a request.
We will use this to acquire local data from the Relay store.
// UserSettings.js
import { graphql } from "react-relay";
import { useQuery } from "relay-experimental";
function UserSettings(props) {
const data = useQuery(
graphql`
query UserSettingsQuery {
someClientSchemaField
}
`,
null,
{ fetchPolicy: "store-only" }
);
return <div>{data.someClientSchemaField}</div>;
}
useFragment
The hook implementation of createFragmentContainer.
It takes two unnamed parameters:
- The graphql tagged fragment. (i.e. graphql``)
- The data prop containing the fragment spread.
Example usage:
The fragment is spread inside a query, and is then passed to a child.
// TodoList.js
import { graphql } from "react-relay";
import { useQuery } from "relay-experimental";
function TodoList(props) {
const data = useQuery(
graphql`
query TodoListQuery {
viewer {
todos {
id
...TodoItemFragment
}
}
}
`
);
return data.todos.map((todo) => <TodoItem todo={todo} />);
}
The component requesting this fragment will then consume the data fragment from props (referred to as a fragmentRef), and subscribe to future data.
// TodoItem.js
import { graphql } from "react-relay";
import { useFragment } from "relay-experimental";
function TodoItem(props) {
const todo = useFragment(
graphql`
fragment TodoItemFragment on Todo {
id
name
}
`,
props.todo
);
}
The subscribed fragment will update the component either if it receives an update from the Relay store indicating that the data the component is directly subscribed to has changed, or if the fragment refs point to different records, OR if the context environment has changed.
useRefetchableFragment
The hook implementation of createRefetchContainer.
Just like useFragment
, it takes two unnamed parameters:
- The graphql tagged fragment. (i.e. graphql``)
- The data prop containing the fragment spread.
A useRefetchableFragment
fragment requires the @refetchable
GraphQL directive.
This directive takes the queryName
parameter, which is the name of the generated query used by Relay when refetching the fragment.
The @refetchable
directive can only be used on the Query type, Viewer type, Node type, or types implementing Node.
useRefetchableFragment
returns an array which contains two values.
- The data consumed and returned by the fragment.
refetch
: Function to restart the pagination on the connection. Disposes in-flight pagination queries before refetching. Takes two parameters.- The GraphQL query variables, in the shape of an object mapping from variable name to value.
- An object containing a set of options:
fetchPolicy
: Enum. Setting for how a query may be fetched.'store-only'
: Returns local data. No request is made.'store-or-network'
: Returns local data if available, otherwise suspends and makes a request.'store-and-network'
: Returns local data and then makes a request.'network-only'
: Always suspends and sends a request, even if data is available locally.
onComplete
Function called when the new page has been fetched. If an error occurred during refetch, this function will receive that error as an argument.
Example usage:
// TodoItem.js
import { graphql } from "react-relay";
import { useRefetchableFragment } from "relay-experimental";
function TodoItem(props) {
const [todo, refetch] = useRefetchableFragment(
graphql`
fragment TodoItemFragment on Todo
@refetchable(queryName: "TodoItemFragmentRefetchQuery") {
text
isComplete
}
`,
props.todo
);
// refetch the fragment
refetch(
{ id: "id:2" },
{ onComplete: (error) => console.info("Maybe success!") }
);
}
Upon a refetch call, this component will suspend, which currently results in a very sketchy user experience. It is expected that the upcoming release of React will ship with a solution to this issue (Which is probably also why this version of Relay has yet to be officially released).
When the refetch request completes, the query fragment is extracted from the response, which will point to the refetch query as its owner.
Note: The path to the refetch query generated by Merged!relay-compiler
is currently broken, but is hopefully fixed very soon.
This pull request has been submitted as a possible solution.
usePaginationFragment
The hook implementation of createPaginationContainer.
Just like useFragment
and useRefetchableFragment
it takes two unnamed parameters:
- The graphql tagged fragment. (i.e. graphql``)
- The data prop containing the fragment spread.
Unlike createPaginationContainer
, usePaginationFragment
will provide most of the pagination functionality for you.
No need for a large, somewhat complicated and obscure configuration object. Rejoice!
The usePaginationFragment
function returns data along with a set of additional functions and values, which provide both backward and forward pagination functionality.
data
: The data consumed and returned by the fragment.loadNext
: A function to load the next n items, in theforward
direction. Takes two parameters.- The amount n of items to fetch.
- An object containing a set of options:
onComplete
Function called when the new page has been fetched. If an error occurred during refetch, this function will receive that error as an argument.
loadPrevious
: Same asloadNext
, except the direction isbackward
.hasNext
: Whether or not there are more items in theforward
direction.hasPrevious
: Whether or not there are more items in thebackward
direction.isLoadingNext
: Whether or notloadNext
is in-flight.isLoadingPrevious
: Whether or notloadPrevious
is in-flight.refetch
: Function to restart the pagination on the connection. Disposes in-flight pagination queries before refetching. Takes two parameters.- The GraphQL query variables, in the shape of an object mapping from variable name to value.
- An object containing a set of options:
fetchPolicy
: Enum. Setting for how a query may be fetched.'store-only'
: Returns local data. No request is made.'store-or-network'
: Returns local data if available, otherwise suspends and makes a request.'store-and-network'
: Returns local data and then makes a request.'network-only'
: Always suspends and sends a request, even if data is available locally.
onComplete
Function called when the new page has been fetched. If an error occurred during refetch, this function will receive that error as an argument.
Example usage:
useQuery
is responsible for fetching the UserFragment
and passes the user fragmentRef to a child.
// User.js
import { graphql } from "react-relay";
import { useQuery } from "relay-experimental";
function User() {
const { node } = useQuery(
graphql`
query UserQuery(
$id: ID!
$last: Int
$first: Int
$after: ID
$before: ID
) {
node(id: $id) {
...UserFragment
}
}
`,
{
id: "1",
first: 1,
last: null,
before: null,
after: "cursor:1",
}
);
return <UserTodos user={node} />;
}
The UserFragment
is marked with the @refetchable
directive, and todos
with the @connection
directive.
// UserTodos.js
import { graphql } from "react-relay";
import { usePaginationFragment } from "relay-experimental";
function UserTodos(props) {
const {
data,
loadNext,
loadPrevious,
hasNext,
hasPrevious,
isLoadingNext,
isLoadingPrevious,
refetch,
} = usePaginationFragment(
graphql`
fragment UserFragment on User
@refetchable(queryName: "UserFragmentPaginationQuery") {
id
name
todos(first: $first, last: $last, after: $after, before: $before)
@connection(key: "UserFragment_todos") {
edges {
node {
id
...TodoFragment
}
}
}
}
`,
props.user
);
}
As with useRefetchableFragment
, this component will suspend upon a request.
In closing
So, is this commit the best thing that has ever happened to Relay?
Without a doubt.
The API has been greatly simplified, and with the introduction of suspense for data fetching, you no longer have to infest your code with defensive precautions against nullable data from props.
With this upcoming release, I'd argue that Relay's barrier to entry will have been lowered quite significantly, which has been a major shortcoming of Relay since it was first released.
That being said, relay-experimental
isn't ready for use in production just yet.
For the time being, all we can do is wait patiently for the next release of React.