RelayJs to ReasonRelay
Be sure to have checked out Part 1 in the series. The starting point for what we do here is at the master branch of this idkjs/relay-graphql-quickstart.
We are going to follow the steps from the ReasonRelay docs
If you are following along, this will get you to our starting point for Part 2.
git clone https://github.com/idkjs/relay-graphql-quickstart.git
rm -rf .git
git init .
git add . && git commit -m "js"
git checkout -b with-reason-relay
First thing we need to do is add our reasonml deps. We need bs-platform
, reason-react
,bs-fetch
and a ./bsconfig.json
file. I'm going to use bs-platform@7.0.2-dev.1
just to keep this experimental but you can use the stable version you get at bs-platform
to. Also, I will be using pnpm because I dont have a lot of room on this machine and that helps by only installing one version of any package on this machine.
We are using bs-fetch
to bind to the fetch
function we used in the js
version.
For bsconfig.json
we are going to steal the one from reason-relay/example and paste it in our root directory.
touch bsconfig.json
pnpm i -D bs-platform@7.0.2-dev.1
pnpm i reason-react bs-fetch
The reason-relay quickstart is using experimental features on react
so we will install the versions the suggest and also install the reason-relay dependencies.
# Add React and ReactDOM experimental versions
pnpm i react@0.0.0-experimental-f6b8d31a7 react-dom@0.0.0-experimental-f6b8d31a7
# Add reason-relay and dependencies to the project
# We currently depend on Relay version 8, so install that exact version
pnpm i reason-relay graphql relay-runtime@8.0.0 relay-compiler@8.0.0 react-relay@0.0.0-experimental-5f1cb628 relay-config@8.0.0
After we have done this we need to add our reason deps to bsconfig.json
. Since we ripped the file from the repo this should already be there:
...
"ppx-flags": ["reason-relay/ppx"],
"bs-dependencies": ["reason-react", "reason-relay","bs-fetch"],
...
Add reason
related files to .gitignore
.vscode
.merlin
_build
_esy
_release
*.byte
*.native
*.install
lib/bs
reason-relay-compiler
Using experimental React versions
You may need to tell yarn to prefer the experimental versions of React and ReactDOM by adding an entry to resolutions in package.json. This is because reason-react (and possibly other dependencies in your project) will depend on a stable React version, and we want to force everyone to use the experimental React versions, or you might start getting nasty bugs and weird errors about conflicting React versions.
Ensure that only the experimental versions are used by doing the following:
Open package.json and look for react and react-dom. In the versions field you'll see something like 0.0.0-experimental-f6b8d31a7 - copy that version number.
Add an entry for both react and react-dom with that version number to your resolutions. The final configuration should look something like this:
...
"resolutions": {
"react": "0.0.0-experimental-f6b8d31a7",
"react-dom": "0.0.0-experimental-f6b8d31a7"
}
}
Configuring ReasonRelay
Using ReasonRelay, we need a relay.config.js
file to tell the compiler where to look for the schema.graphl
file, where to create the required artifacts dir
and src
directory. We did not need this in js
version. We already have a schema.graphql
file from the js
project so we will point to it below. With the following command we are creating the two files we need to configure reason-relay.
touch relay.config.js
// paste this into relay.config.js
module.exports = {
src: "./src", // Path to the folder containing your Reason files
schema: "./schema.graphql", // Path to the schema.graphql you've exported from your API. Don't know what this is? It's a saved introspection of what your schema looks like. You can run `npx get-graphql-schema http://path/to/my/graphql/server > schema.graphql` in your root to generate it
artifactDirectory: "./src/__generated__" // The directory where all generated files will be emitted and is created by the relay-compiler when you run `yarn relay`
};
Then add scripts to package.json
to run relay
and reason
commands.
Our old scripts was:
"scripts": {
"start": "yarn run relay && react-scripts start",
"build": "yarn run relay && react-scripts build",
"relay": "yarn run relay-compiler --schema schema.graphql --src ./src/ --watchman false $@"
},
and our new one is:
"scripts": {
"start": "yarn run relay && react-scripts start",
"bs:build": "bsb -make-world",
"watch": "bsb -make-world -w",
"clean": "bsb -clean-world",
"relay": "reason-relay-compiler",
"relay:watch": "reason-relay-compiler --watch"
}
From docs:
Notice that we're calling reason-relay-compiler, and not relay-compiler. This is because ReasonRelay adds a thin layer on top of the regular relay-compiler. Read more about the Relay compiler and how ReasonRelay uses it here.
Now you have two scripts set up; one for running the compiler once, and one for running it in watch-mode.
You can go ahead and start it in watch mode right away (yarn relay:watch) in a separate terminal. Please note that you'll need to be aware of the output from the compiler as it will tell you when there are issues you'll need to fix.
Setting up the Relay environment
ReasonRelay's equivalent of our RelayEnvironment.js
file is RelayEnv.re
. It uses the bs-fetch
package we installed at the earlier which binds to node's fetch
. This is the example from the docs modified to include our github token.
/* This is just a custom exception to indicate that something went wrong. */
exception Graphql_error(string);
exception REACT_APP_GITHUB_AUTH_TOKEN_REQUIRED;
/**
* A standard fetch that sends our operation and variables to the
* GraphQL server, and then decodes and returns the response.
*/
/**
* A standard fetch that sends our operation and variables to the
* GraphQL server, and then decodes and returns the response.
*/
// get our github token throw exception if not there
let react_app_github_auth_token = Sys.getenv("REACT_APP_GITHUB_AUTH_TOKEN");
try(Sys.getenv("REACT_APP_GITHUB_AUTH_TOKEN")) {
| Not_found => raise(REACT_APP_GITHUB_AUTH_TOKEN_REQUIRED)
};
/**
* Create a Bearer string to pass to authorization in `fetchWithInit`
*/
let authorization = "Bearer " ++ react_app_github_auth_token;
Js.log2("authorization", authorization);
let fetchQuery: ReasonRelay.Network.fetchFunctionPromise =
(operation, variables, _cacheConfig) =>
Fetch.(
fetchWithInit(
"https://api.github.com/graphql",
RequestInit.make(
~method=Post,
~body=
Js.Dict.fromList([
("query", Js.Json.string(operation.text)),
("variables", variables),
])
|> Js.Json.object_
|> Js.Json.stringify
|> BodyInit.make,
~headers=
HeadersInit.make({
"authorization": authorization,
"content-type": "application/json",
"accept": "application/json",
}),
(),
),
)
|> Js.Promise.then_(resp =>
if (Response.ok(resp)) {
Response.json(resp);
} else {
Js.Promise.reject(
Graphql_error(
"Request failed: " ++ Response.statusText(resp),
),
);
}
)
);
let network =
ReasonRelay.Network.makePromiseBased(~fetchFunction=fetchQuery, ());
let environment =
ReasonRelay.Environment.make(
~network,
~store=
ReasonRelay.Store.make(~source=ReasonRelay.RecordSource.make(), ()),
(),
);
Converting index.js
to Index.re
Now we need to create a reason version of our index.js
file. Create a file called src/Index.re
and add the following code:
ReactExperimental.renderConcurrentRootAtElementWithId(
<ReasonRelay.Context.Provider
environment=RelayEnv.environment>
<App />
</ReasonRelay.Context.Provider>,
"root",
);
Here we are passing RelayEnv.re
environment to ReasonRelay's context provider to get access to our ReasonRelay assets throughout the app. We still don't have App.re
so if you ran yarn watch
in a terminal, the reason compiler will be giving us an error. Run yarn watch
to start compiling your reason code to see it.
Let create App.re
to fix it. Run touch src/App.re
then add this code to it:
[@react.component]
let make = () => {
<ReactExperimental.Suspense
fallback={<div> {React.string("Loading...")} </div>}>
<Main />
</ReactExperimental.Suspense>;
};
While in our js
example we inlined the second component to our Suspense
div, the relay-compiler complained about it. Apparently she wants a React Component here so I broke out the response to Main.re
and passed it as the second component. As soon as I figure out the details about this, I will get back here with an update. If you know why this is, please go ahead and leave a comment.
And now the reason-compiler is complaining about Main.re
so let create that too:
touch Main.re
and add the following:
module Query = [%relay.query
{|
query MainQuery {
repository(owner: "facebook", name: "relay") {
name
}
}
|}
];
This is our query which is the reason version of our js
query in App.js
. You might notice that in App.js
its called AppRepositoryNameQuery
and in Main.re
its called MainQuery
. That is because, in ReasonRelay, its required that the Query name be the same as the name of the file its found in followed by what kind of query it is. That is, is it a Mutation
, Query
, Subscription
, etc. I am not a genius. The reason-relay compiler told me to fix this when I copied the js
version of the query into the Main.re
. So the query name is this_file_name
+ type_of_graphql_operation
or MainQuery
. The reason and reason-relay compilers are your friends. Be nice to them.
Another thing to note is that in your src/__generated__
directory, there is now a file called MainQuery_graphql.re
that reason-compiler generated once your wrote that query in App.re
.
We will use the result of the query in our react component like this:
[@react.component]
let make = () => {
let query = Query.use(~variables=(), ());
<div className="App">
<header className="App-header">
{switch (query.repository) {
| Some(repository) => <p> {React.string(repository.name)} </p>
| None => <p> {React.string("Nothing to see here")} </p>
}}
</header>
</div>;
};
In our component, we create a variable to hold the query response here:
let query = Query.use(~variables=(), ());
Our query
variable returns an option
which we handle with a switch
statement:
{switch (query.repository) {
| Some(repository) => <div> {React.string(repository.name)} </div>
| None => <div> {React.string("Nothing to see here")} </div>
}}
An important note is that ReasonRelay uses React.Suspense
by default,e, which means that it'll suspend your component if the data's not already there.
So all together, Main.re
should look like this:
[%bs.raw {|require("./App.css")|}];
module Query = [%relay.query
{|
query MainQuery {
repository(owner: "facebook", name: "relay") {
name
}
}
|}
];
[@react.component]
let make = () => {
/* Query.use is a React hook that will dispatch the query to the server and then deliver the data to the component. */
let query = Query.use(~variables=(), ());
<div className="App">
<header className="App-header">
{switch (query.repository) {
| Some(repository) => <p> {React.string(repository.name)} </p>
| None => <p> {React.string("Nothing to see here")} </p>
}}
</header>
</div>;
};
Preloaded Queries
The astute observer will have noticed that the js
example used a preloaded query
and our Main.re
component did not. That is because I am just not figuring this out with you as I am writing this and that is the version that I got working first. But being stubborn I had to get the preloaded query
to work with ReasonRelay. As an aside, my goto way to figure out how something works is to got to the project repository and search the tests for the function call. That is what I did here and figured it out with the help of Test_query.re.
The first I did was touch src/AppWithPreload
then added the same query as we used in Main.re
taking care to change the name so that it matches the file_name
+ graphql_operation
format.
module Query = [%relay.query
{|
query AppWithPreloadQuery {
repository(owner: "zth", name: "reason-relay") {
name
}
}
|}
];
We then add a module that has a react component that take a preloadToken
prop. TestPreloaded
will then use the preloadToken
in executing the graphql query defined in this file. Per the reason-relay docs on preloaded queries, "preloaded queries means that you start preloading your query as soon as you can, rather than waiting for UI to render just to trigger a query".
Please read this section of the Relay docs for a more thorough overview of preloaded queries.
In ReasonRelay, every [%relay.query] node automatically generates a preload function that you can call with the same parameters as the use hook (plus passing your environment, as preload runs outside of React's context). Preload gives you back a reference, which you can then pass to Query.usePreloaded(reference). This will either suspend the component (if the data's not ready) or render it right away if the data's already there.
So ⬆️ is why you do it.
module TestPreloaded = {
[@react.component]
let make = (~preloadToken) => {
let query = Query.usePreloaded(preloadToken);
let repositoryName =
switch (query.repository) {
| Some(repository) => repository.name
| None => "Nothing Preloaded"
};
<div> {React.string("Preloaded " ++ repositoryName)} </div>;
};
};
The Query.preload
function has the following type:
(
~environment: ReasonRelay.Environment.t,
~variables: Query.Operation.variables,
~fetchPolicy: option(
ReasonRelay.fetchPolicy
),
~fetchKey: option(string),
~networkCacheConfig: option(
ReasonRelay.cacheConfig
),
unit
) => Query.UseQuery.preloadToken
So we are going to have to pass it at least the required arguments of environment
of type ReasonRelay.Environment.t
and variables
of type Query.Operation.variables
.
We will get the ReasonRelay.Environemnt
by calling its usseEnvironmentFromContext()
method.
We do that in AppWithPreload's
default component.
[@react.component]
let make = () => {
let environment = ReasonRelay.useEnvironmentFromContext();
let (preloadToken, setPreloadToken) = React.useState(() => None);
React.useEffect0(() => {
setPreloadToken(_ =>
Some(Query.preload(~environment, ~variables=(), ()))
)
|> ignore;
None;
});
<>
{switch (preloadToken) {
| Some(preloadToken) => <TestPreloaded preloadToken />
| None => React.null
}}
</>;
};
What are we doing here?
We set a state variable to handle our preloadToken value:
let (preloadToken, setPreloadToken) = React.useState(() => None);
We get the token with a React.useEffect
call which sets the preloadToken
state variable to the result of Query.preload(~environment, ~variables=(), ())
React.useEffect0(() => {
setPreloadToken(_ =>
Some(Query.preload(~environment, ~variables=(), ()))
)
|> ignore;
None;
});
Then if preloadToken
is ever Some(value)
, we pass it to TestPreload
which will wait for the data to be ready before it renders the div.
<>
{switch (preloadToken) {
| Some(preloadToken) => <TestPreloaded preloadToken />
| None => React.null
}}
</>;
Something about this seems redundant to me but this works for now.
Tell me why I am wrong and how this could be improved in the comments.
I hope this helps you as much as it helped me.
Not for nothing, the work on ReasonRelay
by Gabriel Nordeborn is so serious. Thanks for sharing, sir.
Top comments (0)