react-apollo with ReasonML
yarn add reason-apollo
# Add graphql_ppx
yarn add @baransu/graphql_ppx_re --dev
Add reason-apollo
to your bs-dependencies
and
@baransu/graphql_ppx_re/ppx
to your ppx-flags
bsconfig.json
"bs-dependencies": [
"reason-react",
"reason-apollo"
],
"ppx-flags": [
"@baransu/graphql_ppx_re/ppx"
]
This will generate a graphql_schema.json
which will be used to safely type your GraphQL queries/mutations.
npx get-graphql-schema ENDPOINT_URL -j > graphql_schema.json
Watch its usage in this video:
Client.re
/* Create an InMemoryCache */
let inMemoryCache = ApolloInMemoryCache.createInMemoryCache();
/* Create an HTTP Link */
let httpLink =
ApolloLinks.createHttpLink(~uri="http://localhost:3010/graphql", ());
let instance =
ReasonApollo.createApolloClient(~link=httpLink, ~cache=inMemoryCache, ());
let inMemoryCache = ApolloInMemoryCache.createInMemoryCache ()
let httpLink =
ApolloLinks.createHttpLink ~uri:"http://localhost:3010/graphql" ()
let instance =
ReasonApollo.createApolloClient ~link:httpLink ~cache:inMemoryCache ()
Index.re
/*
Enhance your application with the `ReasonApollo.Provider`
passing it your client instance
*/
ReactDOMRe.renderToElementWithId(
<ReasonApollo.Provider client=Client.instance>
<App />
</ReasonApollo.Provider>,
"index",
);
let _ =
ReactDOMRe.renderToElementWithId
((ReasonApollo.Provider.createElement ~client:Client.instance
~children:[((App.createElement ~children:[] ())[@JSX ])] ())[@JSX ])
"index"
MyQuery.re
/* Create a GraphQL Query by using the graphql_ppx */
module GetUserName = [%graphql
{|
query getUserName($id: ID!){
user(id: $ID) {
id
device {
id
brand {
id
name
}
}
}
}
|}
];
module GetUserNameQuery = ReasonApollo.CreateQuery(GetUserName);
[@react.component]
let make = () => {
let userNameQuery = GetUserName.make(~id="42", ());
<GetUserNameQuery variables=userNameQuery##variables>
...{({result}) =>
switch (result) {
| Loading => <div> {ReasonReact.string("Loading")} </div>
| Error(error) => <div> {ReasonReact.string(error##message)} </div>
| Data(response) =>
<div>
{/* Handles a deeply nested optional response */
response##user
->Belt.Option.flatMap(user => user##device)
->Belt.Option.flatMap(device => device##brand)
->Belt.Option.mapWithDefault("", brand => brand##name)}
</div>
}
}
</GetUserNameQuery>;
};
3195: syntax error, consider adding a `;' before
MyMutation.re
module AddUser = [%graphql
{|
mutation addUser($name: String!) {
addUser(name: $name) {
id
name
}
}
|}
];
module AddUserMutation = ReasonApollo.CreateMutation(AddUser);
[[@react.component]
let make = () => {
<AddUserMutation>
...{(mutation /* Mutation to call */, _ /* Result of your mutation */) => {
let addNewUserQuery = AddUser.make(~name="Bob", ());
<div>
<button
onClick={_mouseEvent =>
mutation(
~variables=addNewUserQuery##variables,
~refetchQueries=[|"getAllUsers"|],
(),
)
|> ignore
}>
{ReasonReact.string("Add User")}
</button>
</div>;
}}
</AddUserMutation>;
};
1389: let is a reserved keyword, it cannot be used as an identifier. Try `let_' instead
MySubscription.re
module UserAdded = [%graphql {|
subscription userAdded {
userAdded {
id
name
}
}
|}];
module UserAddedSubscription = ReasonApollo.CreateSubscription(UserAdded);
[@react.component]
let make = () => {
<UserAddedSubscription>
...{({result}) => {
switch (result) {
| Loading => <div> {ReasonReact.string("Loading")} </div>
| Error(error) => <div> {ReasonReact.string(error##message)} </div>
| Data(_response) =>
<audio autoPlay=true>
<source src="notification.ogg" type_="audio/ogg" />
<source src="notification.mp3" type_="audio/mpeg" />
</audio>
}
}}
</UserAddedSubscription>;
};
module UserAdded =
[%graphql
{|
subscription userAdded {
userAdded {
id
name
}
}
|}]
module UserAddedSubscription = ReasonApollo.CreateSubscription(UserAdded)
let make () =
((UserAddedSubscription.createElement
~children:(fun { result } ->
match result with
| Loading ->
((div ~children:[ReasonReact.string "Loading"] ())
[@JSX ])
| ((Error (error))[@explicit_arity ]) ->
((div
~children:[ReasonReact.string (## error message)]
())[@JSX ])
| ((Data (_response))[@explicit_arity ]) ->
((audio ~autoPlay:true
~children:[((source ~src:"notification.ogg"
~type_:"audio/ogg" ~children:[] ())
[@JSX ]);
((source ~src:"notification.mp3"
~type_:"audio/mpeg" ~children:[] ())
[@JSX ])] ())[@JSX ])) ())[@JSX ])
[@@react.component ]
If you simply want to have access to the ApolloClient, you can use the ApolloConsumer
<ApolloConsumer>
...{apolloClient => {/* We have access to the client! */}}
</ApolloConsumer>;
let _ =
((ApolloConsumer.createElement
~children:(fun apolloClient -> object (this) end) ())[@JSX ])
If for this query
query {
user {
device {
brand {
name
}
}
}
}
you end up with that kind of code:
let deviceName =
switch (response##user) {
| None => ""
| Some(user) =>
switch (user##device) {
| None => ""
| Some(device) =>
switch (device##brand) {
| None => ""
| Some(brand) => brand##name
}
}
};
let deviceName =
match ## response user with
| None -> ""
| ((Some (user))[@explicit_arity ]) ->
(match ## user device with
| None -> ""
| ((Some (device))[@explicit_arity ]) ->
(match ## device brand with
| None -> ""
| ((Some (brand))[@explicit_arity ]) -> ## brand name))
Belt
open Belt.Option;
let deviceName =
response##user
->flatMap(user => user##device)
->flatMap(device => device##brand)
->mapWithDefault("", brand => brand##name);
3195: syntax error, consider adding a `;' before
@bsRecord
The @bsRecord
modifier is an extension of the graphql syntax for BuckleScipt/ReasonML. It allows you to convert a reason object to a reason record and reap the benefits of pattern matching, but you need to defined the record by yourself.
type brand = {
name: string
};
type device = {
brand: option(brand)
};
type user = {
device: option(device)
};
type response = user;
query {
user @bsRecord {
device @bsRecord {
brand @bsRecord {
name
}
}
}
}
type brand = {
name: string;}
type device = {
brand: brand option;}
type user = {
device: device option;}
type response = user
let _ =
query (user @ (bsRecord (device @ (bsRecord (brand @ (bsRecord name))))))
This time we can pattern match more precisely.
let deviceName =
switch (response##user) {
| Some({device: Some({brand: {name}})}) => name
| _ => ""
};
let deviceName =
match ## response user with
| ((Some
({ device = ((Some ({ brand = { name } }))[@explicit_arity ]) }))
[@explicit_arity ]) -> name
| _ -> ""
get_in_ppx
npm install get_in_ppx
and in bsconfig.json
"ppx-flags": ["get_in_ppx/ppx"]
you can write
let deviceName = response##user#??device#??brand#?name;
let deviceName = #? (#?? (#?? (## response user) device) brand) name
There's a blogpost from Jared Forsyth (author of this ppx) for more explanation.
You might find yourself consuming an API with field names like Field
. Currently, reason object field names are required to be camel case. Therefore if you have a request like this:
{
Object {
id
title
}
}
967: syntax error, consider adding a `;' before
You will attempt to access the response object but it will throw an error:
response##Object; /* Does not work :( */
let _ = ## response Object
Instead, use an alias
to modify the response:
{
object: Object {
id
title
}
}
3321: <UNKNOWN SYNTAX ERROR>
Then you can access the object like this:
response##object
571: object is a reserved keyword, it cannot be used as an identifier. Try `object_' instead
You can create a generic error and Loading component and compose them like this example:
module QueryView = {
[@react.component]
let make =
(
~result: ReasonApolloTypes.queryResponse('a),
~accessData: 'a => option('b),
~render: ('b, 'c) => React.element,
~onLoadMore: ('b, 'unit) => unit=(_, ()) => (),
) => {
switch (result) {
| Error(error) => <Error />
| Loading => ReasonReact.null
| Data(response) =>
switch (accessData(response)) {
| Some(data) => render(data, onLoadMore(data))
| _ => <Error error="" />
}
};
};
};
module QueryView =
struct
let make ~result:(result : 'a ReasonApolloTypes.queryResponse)
~accessData:(accessData : 'a -> 'b option)
~render:(render : 'b -> 'c -> React.element)
?onLoadMore:((onLoadMore : 'b -> 'unit -> unit)=
fun _ -> fun () -> ()) =
match result with
| ((Error (error))[@explicit_arity ]) ->
((Error.createElement ~children:[] ())[@JSX ])
| Loading -> ReasonReact.null
| ((Data (response))[@explicit_arity ]) ->
(match accessData response with
| ((Some (data))[@explicit_arity ]) ->
render data (onLoadMore data)
| _ -> ((Error.createElement ~error:"" ~children:[] ())[@JSX ]))
[@@react.component ]
end
In some cases, it seems like there are some differences between the provided send-introspection-query
and output from tools you might be using to download the schema (such as apollo-codegen
or graphql-cli
).
If your build is failing, please make sure to try with the provided script. In your project root, run:
npx get-graphql-schema ENDPOINT_URL -j > graphql_schema.json